前言
在人工智能领域,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());
ret;
} (IOException e) {
(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)
数据处理完成后,实现问答 chat 命令:
@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:大模型返回的内容格式。
- 调用大模型:实现 SSE 流式调用输出。
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= ();
factory.newEventSource(request, (countDownLatch));
countDownLatch.await();
} (Exception e) {
log.error(, e.getMessage());
}
}
SSE 流式的调用使用了 okhttp-sse 组件提供的功能快速实现。
总结
通过以上步骤,我们完成了基于 Java 语言的最小可用级别 RAG 大模型应用开发。该实践展示了如何利用 Spring Boot、ElasticSearch 以及商业大模型 API 构建一个能够理解私有知识库并进行准确问答的系统。
对于 Java 开发者而言,虽然 AI 领域目前 Python 生态更为成熟,但通过选择合适的中间件和框架,Java 完全有能力承担企业级 AI 应用的开发与集成工作。随着 Java 版本迭代(如 Java 21 引入的向量 API),未来 Java 在 AI 领域的地位将更加稳固。