Spring AI RAG 进阶:对话式 RAG、RAG 评估、混合检索实战

发布时间:2026/7/19 16:06:52
Spring AI RAG 进阶:对话式 RAG、RAG 评估、混合检索实战 Spring AI RAG 进阶对话式 RAG、RAG 评估、混合检索实战本文是《Spring AI 实战》系列第 8 章。第 7 章我们搭了一个能跑的基础 RAG 系统但在真实业务场景中你会遇到更多挑战用户会追问、文档会更新、检索结果质量参差不齐。本章讲解三个进阶主题对话式 RAG支持多轮追问、RAG 评估量化系统效果、混合检索向量关键词互补以及 7 个最常见的踩坑和解决方案。一、开篇基础 RAG 够用吗第 7 章的系统有个硬伤每次问答都是独立的不支持追问。真实场景中用户的提问往往是多轮的用户年假有几天 AI根据《员工手册》入职满 1 年的员工享有 5 天年假…… 用户最长能请多久 ← 最长指什么年假病假事假 AI如果系统没有对话记忆它会单独搜索最长能请多久搜不到此外基础 RAG 还有其他问题需要解决文档更新后怎么同步不能每次更新文档都清空重建整个向量库怎么知道 RAG 效果好不好不能只靠主观感觉需要量化指标精确匹配搜不到怎么办比如BUG-2024-001这种编号向量搜索可能搜不到本章逐一解决这些问题。二、对话式 RAG让 AI 记住上下文2.1 为什么需要对话记忆在基础 RAG 中用户的每一句话都是独立处理的。QuestionAnswerAdvisor直接把用户的问题拿去向量化搜索。但用户经常会用省略语和指代词追问第一句“年假有几天” → 第二句“怎么申请”“怎么申请省略了年假”第一句“报销需要什么材料” → 第二句“审批流是什么”审批流指报销的审批流如果单独对怎么申请做向量搜索搜出来的可能和年假完全无关。解决思路用ChatMemory记住对话历史在检索前先让大模型理解当前问题的完整意图结合历史对话然后用改写后的问题去做搜索。2.2 ChatMemory QuestionAnswerAdvisor 完整代码packagecom.example.springaidemo.config;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;importorg.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.memory.InMemoryChatMemory;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/** * 对话式 RAG 配置 * * 核心思路ChatMemory QuestionAnswerAdvisor 组合 * * 执行流程以用户追问为例 * 1. MessageChatMemoryAdvisor 把对话历史拼入上下文 * 2. 大模型结合对话历史理解怎么申请 年假怎么申请 * 3. QuestionAnswerAdvisor 用完整问题做向量搜索 * 4. 基于搜索结果生成回答 */ConfigurationpublicclassConversationalRagConfig{/** * 对话内存 * InMemoryChatMemory 基于内存重启丢失 * 生产环境可以用 Redis / JDBC / MongoDB 实现持久化 */BeanpublicChatMemorychatMemory(){returnnewInMemoryChatMemory();}/** * 配置带对话记忆的 RAG ChatClient * * Advisor 执行顺序很重要 * - MessageChatMemoryAdvisor 在外层先执行请求处理 * - QuestionAnswerAdvisor 在内层后执行请求处理 * * 这样执行流程是 * 请求阶段加入对话历史 → 检索相关文档 → 拼入 Prompt → 调用大模型 * 响应阶段大模型回答 → 保存到对话历史 */BeanpublicChatClientconversationalRagClient(ChatClient.Builderbuilder,ChatMemorychatMemory,VectorStorevectorStore){returnbuilder.defaultSystem( 你是一个企业知识库问答助手。 请严格基于提供的参考信息回答问题。 如果参考信息中没有相关内容请说知识库中未找到相关信息。 回答要简洁准确不超过 300 字。 ).defaultAdvisors(// 对话记忆 Advisor外层先处理newMessageChatMemoryAdvisor(chatMemory),// RAG 检索增强 Advisor内层后处理newQuestionAnswerAdvisor(vectorStore)).build();}}2.3 对话式 RAG Servicepackagecom.example.springaidemo.service;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.stereotype.Service;/** * 对话式 RAG 问答服务 * * 核心能力 * 1. 基于知识库回答问题RAG * 2. 记住对话上下文支持追问ChatMemory * * 使用示例 * 用户年假有几天 * AI根据《员工手册》入职满 1 年的员工享有 5 天年假... * 用户怎么申请 ← 不需要重复说年假 * AI年假申请流程如下1. 在 OA 系统提交申请... */ServicepublicclassConversationalRagService{privatefinalChatClientconversationalRagClient;privatefinalChatMemorychatMemory;publicConversationalRagService(ChatClientconversationalRagClient,ChatMemorychatMemory){this.conversationalRagClientconversationalRagClient;this.chatMemorychatMemory;}/** * 对话式问答 * * param conversationId 会话 ID区分不同用户/会话 * param question 用户问题 * return AI 回答 */publicStringchat(StringconversationId,Stringquestion){returnconversationalRagClient.prompt().user(question)// 指定本次对话使用哪个会话的内存.advisors(a-a.param(chat_memory_conversation_id,conversationId)).call().content();}/** * 清除指定会话的对话历史 * * 场景用户点击新对话按钮时调用 */publicvoidclearHistory(StringconversationId){chatMemory.clear(conversationId);}}2.4 ControllerRestControllerRequestMapping(/api/knowledge)publicclassKnowledgeController{privatefinalConversationalRagServiceragService;publicKnowledgeController(ConversationalRagServiceragService){this.ragServiceragService;}/** * 对话式问答 */PostMapping(/chat)publicResponseEntityMapString,Objectchat(RequestBodyMapString,Stringrequest){StringconversationIdrequest.getOrDefault(conversationId,default);Stringquestionrequest.get(question);StringanswerragService.chat(conversationId,question);returnResponseEntity.ok(Map.of(code,200,data,Map.of(conversationId,conversationId,question,question,answer,answer)));}/** * 清除对话历史 */DeleteMapping(/chat/{conversationId})publicResponseEntityMapString,StringclearHistory(PathVariableStringconversationId){ragService.clearHistory(conversationId);returnResponseEntity.ok(Map.of(code,200,message,对话历史已清除));}}2.5 测试多轮对话# 第一轮curl-XPOSThttp://localhost:8080/api/knowledge/chat\-HContent-Type: application/json\-d{conversationId: user-001, question: 公司的年假有几天}# 第二轮追问不需要重复年假curl-XPOSThttp://localhost:8080/api/knowledge/chat\-HContent-Type: application/json\-d{conversationId: user-001, question: 怎么申请}# 第三轮继续追问curl-XPOSThttp://localhost:8080/api/knowledge/chat\-HContent-Type: application/json\-d{conversationId: user-001, question: 需要提前几天申请}# 清除对话历史curl-XDELETEhttp://localhost:8080/api/knowledge/chat/user-001踩坑提醒InMemoryChatMemory在 JVM 重启后数据丢失。生产环境应该用RedisChatMemory或自定义实现把对话历史存到 Redis / MySQL。另外对话历史会不断增长消耗越来越多 Token。建议设置最大轮数比如保留最近 10 轮。三、RAG 评估怎么知道系统好不好用RAG 系统上线后你不能只靠感觉还行来判断效果。需要一套量化的评估体系。业界最常用的是RAGASRAG Assessment框架。3.1 RAGAS 四大核心指标指标英文名衡量什么理想值计算方式忠诚度FaithfulnessAI 的回答是否严格基于检索到的文档有没有编造越高越好0.8把回答拆成多个声明逐一检查每个声明是否能在检索文档中找到依据回答相关性Answer Relevancy回答是否切题有没有答非所问越高越好0.7用 Embedding 计算回答和问题的语义相似度上下文精确率Context Precision检索到的文档中有多大比例是真正相关的越高越好0.7相关文档数 / 总检索文档数上下文召回率Context Recall所有应该被检索到的相关文档中实际检索到了多少越高越好0.6实际检索到的相关文档数 / 所有相关文档总数3.2 如何用这些指标优化系统指标低可能的原因优化方向Faithfulness 低System Prompt 没有约束 AI只基于文档回答或检索到的文档质量差优化 System Prompt增加禁止编造的约束Answer Relevancy 低检索到的文档和问题不相关或 top-K 太大引入了噪音减小 top-K、提高相似度阈值、优化 Embedding 模型Context Precision 低检索出了太多不相关的文档提高 similarityThreshold、优化 chunk 质量Context Recall 低相关文档没被检索到chunk 分割时把关键信息切碎了增大 top-K、调整 chunk 大小和 overlap、换更好的 Embedding 模型3.3 简易评估 Servicepackagecom.example.springaidemo.service;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.document.Document;importorg.springframework.ai.vectorstore.SearchRequest;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.stereotype.Service;importjava.util.List;importjava.util.stream.Collectors;/** * RAG 评估服务 * * 提供简易的评估方法帮助定位 RAG 系统的问题 */ServicepublicclassRagEvalService{privatefinalVectorStorevectorStore;privatefinalChatClientchatClient;publicRagEvalService(VectorStorevectorStore,ChatClientchatClient){this.vectorStorevectorStore;this.chatClientchatClient;}/** * 评估单次问答的检索质量 * * 返回检索到的文档内容和相似度信息 */publicEvalResultevaluateRetrieval(Stringquestion,inttopK){varrequestSearchRequest.builder().query(question).topK(topK).build();ListDocumentresultsvectorStore.similaritySearch(request);// 打印检索结果人工判断相关性System.out.println( 检索评估 );System.out.println(问题: question);System.out.println(检索到 results.size() 条文档);for(inti0;iresults.size();i){Documentdocresults.get(i);Stringpreviewdoc.getText().length()80?doc.getText().substring(0,80)...:doc.getText();System.out.println( [(i1)] doc.getMetadata().get(file_name) | preview.replace(\n, ));}System.out.println();returnnewEvalResult(question,results.size(),results.stream().map(d-(String)d.getMetadata().getOrDefault(file_name,unknown)).collect(Collectors.joining(, )));}/** * 使用 LLM 评估回答的忠诚度是否基于文档 */publicStringevaluateFaithfulness(Stringquestion,Stringanswer,ListDocumentcontext){StringcontextTextcontext.stream().map(Document::getText).collect(Collectors.joining(\n---\n));returnchatClient.prompt().system( 你是一个 RAG 评估专家。请评估以下回答是否严格基于参考信息。 评估标准 1. 回答中的每个事实性陈述是否都能在参考信息中找到依据 2. 是否存在编造或凭空添加的信息 输出格式 - 忠诚度评分高/中/低 - 问题分析具体指出哪些内容是编造的如果有 ).user( 问题question 参考信息 contextText AI 回答answer ).call().content();}publicrecordEvalResult(Stringquestion,intretrievedCount,StringsourceFiles){}}四、增量索引文档更新后怎么办当知识库的文档更新后你不能每次都清空重建——成本太高而且重建期间系统不可用。4.1 三种增量索引策略策略原理优点缺点适用场景追加写入新文档直接 add旧文档不删除实现最简单旧版本文档会残留可能返回过时信息文档只增不改的场景如日志、新闻文件级更新同名文件先 delete 旧数据再 add 新数据保证同名文件数据最新需要维护文件名和向量 ID 的映射关系最常用的策略全量重建清空向量库重新处理所有文档数据最干净耗时长重建期间系统不可用文档量小 1000 个文件或定期维护4.2 文件级更新实现思路/** * 文件级更新先删旧数据再写新数据 * * 实现要点 * 1. 用 file_name 作为唯一标识 * 2. 先根据 file_name 从元数据中查出旧的向量 ID * 3. 调用 vectorStore.delete() 删除旧数据 * 4. 再调用 vectorStore.add() 写入新数据 */publicvoidupdateDocument(MultipartFilefile,Stringdepartment){StringfileNamefile.getOriginalFilename();// 1. 搜索旧文档通过元数据过滤varsearchRequestSearchRequest.builder().query(fileName).topK(1000).filterExpression(file_name fileName).build();ListDocumentoldDocsvectorStore.similaritySearch(searchRequest);// 2. 删除旧文档if(!oldDocs.isEmpty()){ListStringoldIdsoldDocs.stream().map(Document::getId).toList();vectorStore.delete(oldIds);System.out.println(删除旧文档: oldIds.size() 条);}// 3. 处理并写入新文档复用 DocumentIngestService 的逻辑ingestService.ingest(file,department,policy);}五、混合检索向量 关键词互补5.1 为什么纯向量检索不够向量检索擅长语义相似的搜索但对以下场景无能为力场景示例向量搜索的问题精确编号“BUG-2024-001”Embedding 会把编号变成一堆数字向量语义搜索可能搜不到专有名词“Spring AI 2.0.0”模型可能没见过这个版本号向量化不准缩写/代号“P0 事故”“P0” 的语义向量可能和事故距离很远短查询“报错 403”文本太短Embedding 捕捉不到足够的语义信息混合检索 向量检索语义匹配 关键词检索精确匹配取两者结果的并集或加权融合。5.2 混合检索原理用户问题BUG-2024-001 的修复方案是什么 向量检索路径 问题 → Embedding → [0.23, -0.15, ...] → 搜索向量库 → 返回语义相关的 5 条 关键词检索路径 问题 → 分词 → [BUG-2024-001, 修复, 方案] → 全文搜索 → 返回包含关键词的 3 条 结果融合 向量结果 ∪ 关键词结果 8 条候选 去重 重排序Rerank → 最终返回 top-55.3 Spring AI 中的实现方式Spring AI 的VectorStore接口本身只支持向量检索。要实现混合检索有两条路径路径一结合 Redis VectorStore 的全文搜索能力Redis Stack 同时支持向量搜索和全文搜索。路径二在 VectorStore 之外额外维护一个 Elasticsearch / Redis 全文索引在 Service 层做结果融合。/** * 简易混合检索实现思路伪代码展示融合逻辑 */publicListDocumenthybridSearch(Stringquestion,inttopK){// 1. 向量检索语义匹配varvectorRequestSearchRequest.builder().query(question).topK(topK*2)// 多取一些后面会融合去重.build();ListDocumentvectorResultsvectorStore.similaritySearch(vectorRequest);// 2. 关键词检索精确匹配// 实际项目中这里用 Elasticsearch / Redis 全文搜索ListDocumentkeywordResultskeywordSearchService.search(question,topK*2);// 3. 结果融合去重 打分MapString,DocumentmergednewLinkedHashMap();// 向量结果权重 0.7vectorResults.forEach(doc-{Stringiddoc.getId();merged.putIfAbsent(id,doc);});// 关键词结果补充向量没搜到的keywordResults.forEach(doc-{Stringiddoc.getId();if(!merged.containsKey(id)){merged.put(id,doc);// 向量没搜到但关键词搜到了}});// 4. 取 top-Kreturnmerged.values().stream().limit(topK).toList();}实战建议如果你的知识库以中文问答类文档为主纯向量检索已经能覆盖 90% 的场景。只有当你发现大量精确搜索搜不到的问题时才需要引入混合检索。混合检索增加了系统复杂度不要过早引入。六、踩坑总结7 个常见问题问题原因解决方案回答不准确像是编的检索到的文档和问题不相关System Prompt 没有约束 AI 只基于文档回答1. 优化 System Prompt增加禁止编造指令2. 提高 similarityThreshold 到 0.73. 检查 chunk 质量是否切碎了追问时 AI “失忆”没有配置 ChatMemory每轮对话独立处理配置MessageChatMemoryAdvisor并传入conversationId区分会话回答太短或答非所问top-K 太小只检索了 1-2 条不相关文档或 chunk 太小丢失了上下文1. 增大 top-K 到 8-102. 增大 chunkSize 到 1000-12003. 检查是否 overlap 为 0上传新文档后搜不到文档只在上传时入库一次后续更新没有触发重新入库实现文件级增量更新先 delete 旧数据再 add 新数据中文效果差Embedding 模型中文能力弱如 OpenAI 的模型切换为通义 text-embedding-v3 或智谱 embedding-3只改配置不改代码精确编号/专有名词搜不到纯向量检索对精确匹配无能为力引入混合检索向量 关键词Elasticsearch / Redis 全文搜索对话历史越来越长响应变慢ChatMemory 没有限制轮数历史对话不断累积设置ChatMemory的最大消息数如保留最近 10 轮或定期清理过期会话七、小结本章从三个维度提升了 RAG 系统的能力对话式 RAG通过ChatMemoryQuestionAnswerAdvisor组合让 AI 能理解追问的上下文。这是从能用到好用的关键一步。RAG 评估用 RAGAS 四大指标Faithfulness、Answer Relevancy、Context Precision、Context Recall量化系统效果有针对性地优化。混合检索向量检索 关键词检索互补解决纯向量搜索在精确匹配场景下的盲区。结合第 7 章的完整 RAG 系统和本章的进阶能力你已经具备了搭建生产级知识库问答系统的全部技术基础。后续章节将进入 Function Calling 和 Agent 的世界。Spring AI 实战 — 第8章完整内容与源码