跳到主要内容Java 开发者大模型应用开发实战:基于 RAG 的检索增强生成实践 | 极客日志JavaAIjava算法
Java 开发者大模型应用开发实战:基于 RAG 的检索增强生成实践
综述由AI生成Java 开发者如何基于 RAG 架构构建大模型应用系统。文章详细阐述了 RAG 的核心流程,包括文档切片、向量化、向量数据库存储及检索增强生成。技术栈采用 Spring Boot、ElasticSearch、OkHTTP 及智谱 AI 大模型 API。通过 Docker 部署 ES,实现了命令行交互的问答功能,重点讲解了 Embedding 模型调用、余弦相似度检索及 Prompt 构建逻辑,展示了 Java 在企业级 AI 应用开发中的可行性。
霸天29 浏览 前言
在人工智能领域,RAG(检索增强生成)是目前工程实践中应用最广泛的模式之一。对于 Java 开发者而言,构建 AI 大模型应用并非 Python 的专属,通过合理的架构设计,Java 同样可以高效实现大模型应用的开发。
本文从 Java 开发者的立场出发,构建并实现一个最基础的大模型应用系统。主要需求点在于:让大模型理解文本(知识库)内容,并基于知识库范围内的内容进行回答对话。
基于知识库的回答主要解决以下问题:
- 节省大模型训练成本:通用大模型的知识截止于训练数据结束时间,无法快速学习最新知识。检索增强生成解决了大模型无法快速更新的问题,避免了昂贵的训练代价。
- 提升回答准确性:企业内部的私有数据通常未包含在基座模型中。通过 RAG 方式,可以让大模型在特定知识库范围内进行回答,避免'幻觉'或胡说八道,使应用系统更加精准可靠。
技术栈
考虑到目标读者为 Java 开发者,本文选择的技术栈及中间件均为 Java 生态中耳熟能详的组件:
-
开发框架:Spring Boot、Spring Shell
Spring Boot 是 Java 开发生态的核心,非常熟悉。
Spring Shell 用于演示命令行交互问答效果,作为最小雏形的产品交互体验。
-
HTTP 组件:OkHTTP、OkHTTP-SSE
- 用于对接商业大模型的 API 接口。本文以智谱 AI 开放的 ChatGLM 系列为主。若条件允许,也可本地部署开源大模型开放 API 调试。
- 使用 OkHTTP 处理 HTTP 请求,OkHTTP-SSE 支持流式输出。
-
工具包:Hutool
- 封装了字符、文件、时间、集合等常用工具类方法,简化开发。
- 文中主要使用其文本读取和切割方法。
-
向量数据库:ElasticSearch
- 向量数据库是 RAG 应用程序的基础中间件,所有文本 Embedding 向量需存储其中进行召回计算。
- Java 领域缺乏类似 Python
numpy 的本地化矩阵计算组件,因此选择独立部署的中间件。
- ElasticSearch 在 Java 领域用途广泛,且具备存储向量数据的功能(
dense_vector 类型)。
- 其他可选方案包括 Milvus、Qdrant、Postgres(pgvector)、Chroma 等,可根据实际需求选择。
-
LLM 大模型:ChatGLM-Std
- 为了演示方便,直接使用开放 API 接口的商业大模型。
RAG 工程的基本处理流程
在 RAG 检索增强生成领域中,核心处理流程包含两部分:
-
问答流程:
- 对用户提问的问题通过向量
Embedding 模型处理。
- 查询向量数据库(ElasticSearch)进行相似度计算,获取与用户问题最相似的知识库段落。
- 获取成功后,构建
Prompt,发送给大模型获取最终答案。
-
数据处理流程:
- 提取用户私有数据(结构化及非结构化数据,如 PDF/Word/Text 等)。
- 提取文本数据后进行分割处理。
- 通过向量
Embedding 模型将分割后的段落向量化。
向量数据存储到向量数据库中间件中,供后续问答使用。
- 向量 Embedding 模型:对本地知识的向量表征处理,将文本转化为计算机理解的向量表示。
- LLM 问答大模型:负责理解语义召回的段落 + 用户问题结合构建的
Prompt,进行精准回答。
环境准备
Java:JDK 1.8
ElasticSearch:7.16.1
对于 ElasticSearch 的安装,可以通过 docker-compose 在本地快速部署。编写 docker-compose.yml 配置文件,当前部署目录建 data 文件夹挂载数据目录:
version: "3"
services:
elasticsearch:
image: elasticsearch:7.16.1
ports:
- "9200:9200"
- "9300:9300"
environment:
node.name: es
cluster.name: elasticsearch
discovery.type: single-node
ES_JAVA_OPTS: -Xms4096m -Xmx4096m
volumes:
- ./data:/usr/share/elasticsearch/data
deploy:
resources:
limits:
cpus: "4"
memory: 5G
reservations:
cpus: "1"
memory: 2G
restart: always
启动 Es:docker-compose up -d
应用初体验
程序启动后,在命令行终端可以看到可交互的界面。通过 add 和 chat 两个命令完成整个流程。
- 加载文档:使用
add 命令加载文档。在 data 目录下存储 txt 文件,通过命令加载向量处理。
- 对话测试:日志显示保存向量成功后,使用
chat 命令进行对话。
- 问题 1:苏州 2022 年全市的 GDP 是多少?
- 问题 2:吉林省宣传部部长现在是谁?
对比通用大模型(如 ChatGPT),基于现有知识回答的内容(RAG)能够有效避免大模型胡说八道,且回答更精准。这是因为通用大模型训练成本高,无法按周、月甚至年的频率更新,而 RAG 允许应用系统基于最新的私有数据进行回答。
技术实现
新建 Spring Boot 项目,根据 RAG 流程图,主要分为数据的向量处理和问答两步。由于通过 Spring Shell 实现,代码分为两个 Command 命令:
- add:在 data 目录下存放 txt 内容,通过
add file 名称 实现文档向量化流程加载处理。实际生产中可通过定时任务、MQ 消息等方式异步处理。
- chat:通过命令
chat 问题 在 Spring Shell 命令行终端进行对话。
1. 初始化向量数据库索引
public boolean initCollection(String collectionName,int dim){
log.info("collection:{}", collectionName);
IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(IndexCoordinates.of(collectionName));
if (!indexOperations.exists()) {
log.info("index not exists,create");
Document document = Document.from(this.elasticMapping(dim));
indexOperations.create(new HashMap<>(), document);
return true;
}
return true;
}
Es 中的 Index Mapping 结构中,vector 字段类型为 dense_vector,并且指定向量维度为 1024。向量维度的长度与最终向量 Embedding 模型息息相关,不同模型维度不同(如 ChatGPT 为 1536,百度文心一言为 368)。本文选择智谱 AI 的向量模型,返回维度为 1024,故设置为 1024。
2. 文档向量化处理 (Add Command)
add 命令实现文档的向量化过程处理,主要步骤如下:
- 加载文档并分割:读取 data 目录下的文件流,按固定字数(如 256)分割,得到分割集合
chunkResults。
@Slf4j
@Component
@AllArgsConstructor
public class TxtChunk {
public List<ChunkResult> chunk(String docId){
String path="data/"+docId+".txt";
log.info("start chunk---> docId:{},path:{}",docId,path);
ClassPathResource classPathResource=new ClassPathResource(path);
try {
String txt=IoUtil.read(classPathResource.getInputStream(), StandardCharsets.UTF_8);
String[] lines=StrUtil.split(txt,256);
log.info("chunk size:{}", ArrayUtil.length(lines));
List<ChunkResult> results=new ArrayList<>();
AtomicInteger atomicInteger=new AtomicInteger(0);
for (String line:lines){
ChunkResult chunkResult=new ChunkResult();
chunkResult.setDocId(docId);
chunkResult.setContent(line);
chunkResult.setChunkId(atomicInteger.incrementAndGet());
results.add(chunkResult);
}
return results;
} catch (IOException e) {
log.error(e.getMessage());
}
return new ArrayList<>();
}
}
- Embedding 向量化:将分块的集合通过智谱 AI 提供的向量
Embedding 模型进行向量化处理。
public List<EmbeddingResult> embedding(List<ChunkResult> chunkResults){
log.info("start embedding,size:{}",CollectionUtil.size(chunkResults));
if (CollectionUtil.isEmpty(chunkResults)){
return new ArrayList<>();
}
List<EmbeddingResult> embeddingResults=new ArrayList<>();
for (ChunkResult chunkResult:chunkResults){
embeddingResults.add(this.embedding(chunkResult));
}
return embeddingResults;
}
public EmbeddingResult embedding(ChunkResult chunkResult){
String apiKey= this.getApiKey();
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(20000, TimeUnit.MILLISECONDS)
.readTimeout(20000, TimeUnit.MILLISECONDS)
.writeTimeout(20000, TimeUnit.MILLISECONDS)
.addInterceptor(new ZhipuHeaderInterceptor(apiKey));
OkHttpClient okHttpClient = builder.build();
EmbeddingResult embedRequest=new EmbeddingResult();
embedRequest.setPrompt(chunkResult.getContent());
embedRequest.setRequestId(Objects.toString(chunkResult.getChunkId()));
Request request = new Request.Builder()
.url("https://open.bigmodel.cn/api/paas/v3/model-api/text_embedding/invoke")
.post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), GSON.toJson(embedRequest)))
.build();
try {
Response response= okHttpClient.newCall(request).execute();
String result=response.body().string();
ZhipuResult zhipuResult= GSON.fromJson(result, ZhipuResult.class);
EmbeddingResult ret= zhipuResult.getData();
ret.setPrompt(embedRequest.getPrompt());
ret.setRequestId(embedRequest.getRequestId());
return ret;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
- 存储向量:向量处理成功后,将向量数据存储在向量数据库中间件 (
ElasticSearch) 中。
public void store(String collectionName,List<EmbeddingResult> embeddingResults){
log.info("save vector,collection:{},size:{}",collectionName, CollectionUtil.size(embeddingResults));
List<IndexQuery> results = new ArrayList<>();
for (EmbeddingResult embeddingResult : embeddingResults) {
ElasticVectorData ele = new ElasticVectorData();
ele.setVector(embeddingResult.getEmbedding());
ele.setChunkId(embeddingResult.getRequestId());
ele.setContent(embeddingResult.getPrompt());
results.add(new IndexQueryBuilder().withObject(ele).build());
}
List<IndexedObjectInformation> bulkedResult = elasticsearchRestTemplate.bulkIndex(results, IndexCoordinates.of(collectionName));
int size = CollectionUtil.size(bulkedResult);
log.info("保存向量成功-size:{}", size);
}
3. 问答实现 (Chat Command)
@AllArgsConstructor
@Slf4j
@ShellComponent
public class ChatCommand {
final VectorStorage vectorStorage;
final ZhipuAI zhipuAI;
@ShellMethod(value = "chat with files")
public String chat(String question){
if (StrUtil.isBlank(question)){
return "You must send a question";
}
double[] vector=zhipuAI.sentence(question);
String collection= vectorStorage.getCollectionName();
String vectorData=vectorStorage.retrieval(collection,vector);
if (StrUtil.isBlank(vectorData)){
return "No Answer!";
}
String prompt= LLMUtils.buildPrompt(question,vectorData);
zhipuAI.chat(prompt);
return StrUtil.EMPTY;
}
}
- 句子转向量:将用户的问句首先通过向量 Embedding 模型转化得到一个多维的浮点型向量数组。
public double[] sentence(String sentence){
ChunkResult chunkResult=new ChunkResult();
chunkResult.setContent(sentence);
chunkResult.setChunkId(RandomUtil.randomInt());
EmbeddingResult embeddingResult=this.embedding(chunkResult);
return embeddingResult.getEmbedding();
}
- 向量召回:根据向量数据查询向量数据库召回相似的段落内容。
public String retrieval(String collectionName,double[] vector){
Map<String, Object> params = new HashMap<>();
params.put("query_vector", vector);
Script script = new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, "cosineSimilarity(params.query_vector, 'vector')+1", params);
ScriptScoreQueryBuilder scriptScoreQueryBuilder = new ScriptScoreQueryBuilder(QueryBuilders.boolQuery(), script);
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(scriptScoreQueryBuilder)
.withPageable(Pageable.ofSize(3)).build();
SearchHits<ElasticVectorData> dataSearchHits = this.elasticsearchRestTemplate.search(nativeSearchQuery, ElasticVectorData.class, IndexCoordinates.of(collectionName));
List<SearchHit<ElasticVectorData>> data = dataSearchHits.getSearchHits();
List<String> results = new LinkedList<>();
for (SearchHit<ElasticVectorData> ele : data) {
results.add(ele.getContent().getContent());
}
return CollectionUtil.join(results," ");
}
这里利用了 ElasticSearch 提供的 cosineSimilarity 余弦相似性函数。分值会在区间 [0,1] 之间,越接近 1 代表用户输入的句子和之前存储在向量中的句子非常相似。除了余弦相似性,还有 IP 点积、欧几里得距离等算法,可根据实际情况选择。
- 构建 Prompt:向量召回 Top3 得到相似的语义文本内容后,构建
Prompt 发送给大模型。
public static String buildPrompt(String question,String context){
return "请利用如下上下文的信息回答问题:" + "\n" +
question + "\n" +
"上下文信息如下:" + "\n" +
context + "\n" +
"如果上下文信息中没有帮助,则不允许胡乱回答!";
}
构建 Prompt 时可遵循 RTF 框架 (Role-Task-Format):
- R-Role:指定 GPT 大模型担任特定的角色。
- T-Task:任务,需要大模型做的事情。
- F-Format:大模型返回的内容格式。
public void chat(String prompt){
try {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(20000, TimeUnit.MILLISECONDS)
.readTimeout(20000, TimeUnit.MILLISECONDS)
.writeTimeout(20000, TimeUnit.MILLISECONDS)
.addInterceptor(new ZhipuHeaderInterceptor(this.getApiKey()));
OkHttpClient okHttpClient = builder.build();
ZhipuChatCompletion zhipuChatCompletion=new ZhipuChatCompletion();
zhipuChatCompletion.addPrompt(prompt);
zhipuChatCompletion.setTemperature(0.7f);
zhipuChatCompletion.setTop_p(0.7f);
EventSource.Factory factory = EventSources.createFactory(okHttpClient);
ObjectMapper mapper = new ObjectMapper();
String requestBody = mapper.writeValueAsString(zhipuChatCompletion);
Request request = new Request.Builder()
.url("https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke")
.post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody))
.build();
CountDownLatch countDownLatch=new CountDownLatch(1);
EventSource eventSource = factory.newEventSource(request, new ConsoleEventSourceListener(countDownLatch));
countDownLatch.await();
} catch (Exception e) {
log.error("llm-chat 异常:{}", e.getMessage());
}
}
SSE 流式的调用使用了 okhttp-sse 组件提供的功能快速实现。
总结
通过以上步骤,我们完成了基于 Java 语言的最小可用级别 RAG 大模型应用开发。该实践展示了如何利用 Spring Boot、ElasticSearch 以及商业大模型 API 构建一个能够理解私有知识库并进行准确问答的系统。
对于 Java 开发者而言,虽然 AI 领域目前 Python 生态更为成熟,但通过选择合适的中间件和框架,Java 完全有能力承担企业级 AI 应用的开发与集成工作。随着 Java 版本迭代(如 Java 21 引入的向量 API),未来 Java 在 AI 领域的地位将更加稳固。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online