人工智能实战RAG 多轮对话越问越偏Query Rewrite、历史压缩与会话记忆的工程化方案一、问题场景第一轮答得很好第二轮开始跑偏做 RAG 知识库问答时单轮问题往往比较容易处理。例如用户问一线城市住宿费最多报销多少系统可以直接检索差旅报销制度然后回答。但一旦进入多轮对话问题就变复杂了。真实用户不会每次都把问题说完整。他们会这样问用户销售去一线城市拜访客户住宿费最多多少 助手最多650元/天。 用户那二线城市呢第二轮问题那二线城市呢如果直接拿这句话去检索系统很可能召回不到正确内容。因为它缺少关键信息销售 客户拜访 住宿费 二线城市这就是 RAG 多轮对话里最常见的问题用户问题依赖上下文但检索系统只看到当前句子。二、真实问题表现多轮 RAG 常见错误包括1. 第二轮、第三轮问题召回不到资料 2. 模型把上一轮答案当成事实继续发挥 3. 历史对话越长Prompt 越长成本越来越高 4. 用户一句“那这个呢”系统完全不知道指什么 5. 多轮追问后回答偏离原始主题一开始我也尝试过一个简单做法把所有历史对话全部拼进 Prompt结果问题更多1. token 成本暴涨 2. 噪声变多 3. 检索仍然不准 4. 模型容易被旧问题干扰后来才意识到多轮 RAG 不能只靠“拼历史”。它需要三件事1. Query Rewrite把当前问题改写成完整问题 2. History Compression压缩历史对话 3. Session Memory保留关键会话状态三、核心原因分析多轮 RAG 失败的根因是检索需要完整问题但用户输入经常是不完整问题。例如用户当前输入那二线城市呢直接检索关键词二线城市召回结果可能很多差旅报销 城市补贴 销售制度 办公地点但如果改写成销售部门因客户拜访去二线城市出差住宿费最多报销多少检索命中率会明显提升。所以多轮 RAG 的第一步不是检索而是问题改写。四、目标架构用户输入 ↓ 读取会话历史 ↓ Query Rewrite ↓ RAG 检索 ↓ Rerank ↓ 上下文压缩 ↓ LLM 生成 ↓ 更新会话记忆与单轮 RAG 相比多了1. 会话历史管理 2. 问题改写 3. 记忆更新五、可复现项目结构multi-turn-rag-demo/ ├── app.py ├── memory.py ├── rewrite.py ├── retriever.py ├── rag.py └── requirements.txt安装依赖pipinstallfastapi uvicorn pydantic这里为了方便复现检索部分先用简单关键词模拟真实项目可以替换成向量数据库。六、实现会话记忆 memory.pyfromcollectionsimportdefaultdictclassSessionMemory:def__init__(self,max_turns:int6):self.sessionsdefaultdict(list)self.max_turnsmax_turnsdefadd_message(self,session_id:str,role:str,content:str):self.sessions[session_id].append({role:role,content:content})iflen(self.sessions[session_id])self.max_turns:self.sessions[session_id]self.sessions[session_id][-self.max_turns:]defget_history(self,session_id:str):returnself.sessions.get(session_id,[])defbuild_history_text(self,session_id:str):historyself.get_history(session_id)lines[]formsginhistory:lines.append(f{msg[role]}:{msg[content]})return\n.join(lines)这个版本只保留最近 N 轮避免历史无限膨胀。七、Query Rewrite 实现 rewrite.py生产环境建议用 LLM 改写这里先写一个可替换结构。defbuild_rewrite_prompt(history:str,current_query:str):returnf 你是一个问题改写助手。 请根据历史对话将用户当前问题改写成一个完整、独立、适合检索知识库的问题。 要求 1. 保留用户真实意图 2. 补全省略的主语、对象和条件 3. 不要引入历史中没有的信息 4. 只输出改写后的问题 【历史对话】{history}【当前问题】{current_query}本地模拟改写defmock_rewrite(history:str,current_query:str):if二线城市incurrent_queryand销售inhistory:return销售部门因客户拜访去二线城市出差住宿费最多报销多少returncurrent_query真实项目中替换为defrewrite_query(llm,history:str,current_query:str):promptbuild_rewrite_prompt(history,current_query)returnllm(prompt)八、检索模块 retriever.pydocs[{id:policy_001,title:通用差旅报销制度,content:一线城市住宿费每天不超过500元二线城市住宿费每天不超过350元。},{id:policy_002,title:销售部门客户拜访报销制度,content:销售部门因客户拜访产生的住宿费一线城市每天不超过650元二线城市每天不超过450元。},{id:policy_003,title:实习生差旅制度,content:实习生住宿费每天不超过200元。}]defkeyword_retrieve(query:str):results[]fordocindocs:score0forwordin[销售,客户拜访,二线城市,住宿费,报销]:ifwordinqueryandwordindoc[content]doc[title]:score1ifscore0:itemdoc.copy()item[score]score results.append(item)returnsorted(results,keylambdax:x[score],reverseTrue)九、RAG 生成模块 rag.pydefbuild_context(docs):blocks[]fordocindocs:blocks.append(f [资料ID:{doc[id]}] 标题:{doc[title]}内容:{doc[content]}.strip())return\n\n.join(blocks)defbuild_answer_prompt(query:str,context:str):returnf 请严格根据资料回答问题。 如果资料中没有答案请回答根据现有资料无法确定。 【资料】{context}【问题】{query}【回答格式】 直接答案 依据 模拟生成defmock_answer(query:str,docs:list[dict]):if销售inqueryand二线城市inquery:return 直接答案 销售部门因客户拜访去二线城市出差住宿费最多报销450元/天。 依据 资料ID: policy_002。 return根据现有资料无法确定。十、FastAPI 主流程 app.pyfromfastapiimportFastAPIfrompydanticimportBaseModel,FieldfrommemoryimportSessionMemoryfromrewriteimportmock_rewritefromretrieverimportkeyword_retrievefromragimportmock_answer appFastAPI(titleMulti-turn RAG Demo)memorySessionMemory(max_turns6)classChatRequest(BaseModel):session_id:strField(...,min_length1)query:strField(...,min_length1,max_length1000)app.post(/chat)defchat(req:ChatRequest):history_textmemory.build_history_text(req.session_id)rewritten_querymock_rewrite(history_text,req.query)retrieved_docskeyword_retrieve(rewritten_query)answermock_answer(rewritten_query,retrieved_docs)memory.add_message(req.session_id,user,req.query)memory.add_message(req.session_id,assistant,answer)return{origin_query:req.query,rewritten_query:rewritten_query,answer:answer,references:[doc[id]fordocinretrieved_docs]}启动uvicorn app:app--port8000十一、验证多轮效果第一轮curl-XPOSThttp://127.0.0.1:8000/chat\-HContent-Type: application/json\-d{ session_id: u001, query: 销售去一线城市拜访客户住宿费最多多少 }第二轮curl-XPOSThttp://127.0.0.1:8000/chat\-HContent-Type: application/json\-d{ session_id: u001, query: 那二线城市呢 }返回{origin_query:那二线城市呢,rewritten_query:销售部门因客户拜访去二线城市出差住宿费最多报销多少,references:[policy_002]}这说明系统没有拿“那二线城市呢”直接检索而是先改写成完整问题。十二、为什么不能保存所有历史很多人做多轮对话时会直接historyall_messages然后全部塞进 Prompt。问题很明显1. token 成本越来越高 2. 历史噪声越来越多 3. 旧话题干扰新问题 4. 响应速度越来越慢正确做法短期记忆最近几轮对话 长期记忆结构化关键信息例如保存session_state{current_topic:销售部门客户拜访报销,current_city_type:一线城市,current_policy:policy_002}而不是保存所有废话。十三、会话状态提取可以在每轮回答后更新状态defupdate_session_state(state:dict,query:str,answer:str):if销售inqueryor销售inanswer:state[department]销售部门if客户拜访inqueryor客户拜访inanswer:state[scene]客户拜访if二线城市inquery:state[city_type]二线城市returnstate真实项目可以让 LLM 输出结构化 JSON{department:销售部门,scene:客户拜访,city_type:二线城市,topic:差旅报销}这样下一轮改写会更稳定。十四、多轮 RAG 的关键指标单轮 RAG 重点看召回命中率 回答准确率多轮 RAG 还要看1. Query Rewrite 准确率 2. 多轮上下文继承正确率 3. 历史压缩后信息保留率 4. 会话漂移率其中“会话漂移”很常见。例如用户本来问报销几轮后系统开始回答年假。这说明历史上下文管理失控。十五、踩坑记录坑 1直接用当前问题检索对于多轮对话这是最常见错误。用户说那这个呢检索系统根本不知道“这个”是什么。坑 2把所有历史塞进 Prompt短期看省事长期一定出问题。历史越长噪声越多成本越高。坑 3改写时引入新信息Query Rewrite 必须只补全上下文不能编造条件。Prompt 里要明确不要引入历史中没有的信息。坑 4不返回 rewritten_query调试多轮 RAG 时一定要返回或记录origin_query rewritten_query retrieved_docs否则你不知道系统到底检索了什么。坑 5会话不分 session_id如果所有用户共享历史结果会灾难。必须按session_id user_id conversation_id隔离。十六、适合收藏的多轮 RAG Checklist问题改写 [ ] 是否对省略问题做 rewrite [ ] 是否记录 rewritten_query [ ] 是否禁止引入新信息 [ ] 是否基于历史补全主语和条件 历史管理 [ ] 是否限制历史轮数 [ ] 是否压缩历史 [ ] 是否提取结构化状态 [ ] 是否按 session_id 隔离 检索 [ ] 是否用 rewritten_query 检索 [ ] 是否记录召回文档 [ ] 是否处理多轮主题漂移 生成 [ ] 是否严格基于资料回答 [ ] 是否引用资料ID [ ] 是否允许回答无法确定 评估 [ ] 是否有多轮测试集 [ ] 是否评估 rewrite 准确率 [ ] 是否评估上下文继承正确率十七、经验总结多轮 RAG 的核心不是“记住所有历史”而是在当前问题中恢复用户真实意图。如果用户问那二线城市呢系统真正要理解的是销售部门因客户拜访去二线城市出差住宿费最多报销多少所以多轮 RAG 的关键链路是历史理解 → 问题改写 → 检索 → 生成一句话总结多轮 RAG 不是把历史越塞越多而是把问题改写得越来越准。十八、后续优化建议可以继续增强1. 使用 LLM 做 Query Rewrite 2. 建立多轮问答评测集 3. 引入结构化 Session State 4. 对历史对话做摘要压缩 5. 对改写问题做置信度判断 6. 低置信度时反问用户 7. 区分任务型对话和知识型对话 8. 对不同 session 做隔离和过期最后一句经验RAG 多轮问答的质量取决于你能不能把一句“不完整的问题”还原成一个“完整的检索问题”。