news 2026/5/2 19:44:37

13.人工智能实战:RAG 多轮对话越问越偏?Query Rewrite、历史压缩与会话记忆的工程化方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
13.人工智能实战:RAG 多轮对话越问越偏?Query Rewrite、历史压缩与会话记忆的工程化方案

人工智能实战: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.py

fromcollectionsimportdefaultdictclassSessionMemory:def__init__(self,max_turns:int=6):self.sessions=defaultdict(list)self.max_turns=max_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):history=self.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):prompt=build_rewrite_prompt(history,current_query)returnllm(prompt)

八、检索模块 retriever.py

docs=[{"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:score=0forwordin["销售","客户拜访","二线城市","住宿费","报销"]:ifwordinqueryandwordindoc["content"]+doc["title"]:score+=1ifscore>0:item=doc.copy()item["score"]=score results.append(item)returnsorted(results,key=lambdax:x["score"],reverse=True)

九、RAG 生成模块 rag.py

defbuild_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.py

fromfastapiimportFastAPIfrompydanticimportBaseModel,FieldfrommemoryimportSessionMemoryfromrewriteimportmock_rewritefromretrieverimportkeyword_retrievefromragimportmock_answer app=FastAPI(title="Multi-turn RAG Demo")memory=SessionMemory(max_turns=6)classChatRequest(BaseModel):session_id:str=Field(...,min_length=1)query:str=Field(...,min_length=1,max_length=1000)@app.post("/chat")defchat(req:ChatRequest):history_text=memory.build_history_text(req.session_id)rewritten_query=mock_rewrite(history_text,req.query)retrieved_docs=keyword_retrieve(rewritten_query)answer=mock_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-XPOST"http://127.0.0.1:8000/chat"\-H"Content-Type: application/json"\-d'{ "session_id": "u001", "query": "销售去一线城市拜访客户,住宿费最多多少?" }'

第二轮:

curl-XPOST"http://127.0.0.1:8000/chat"\-H"Content-Type: application/json"\-d'{ "session_id": "u001", "query": "那二线城市呢?" }'

返回:

{"origin_query":"那二线城市呢?","rewritten_query":"销售部门因客户拜访去二线城市出差,住宿费最多报销多少?","references":["policy_002"]}

这说明系统没有拿“那二线城市呢?”直接检索,而是先改写成完整问题。


十二、为什么不能保存所有历史?

很多人做多轮对话时,会直接:

history=all_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 多轮问答的质量,取决于你能不能把一句“不完整的问题”,还原成一个“完整的检索问题”。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/2 19:43:25

告别官方数据集!手把手教你用自定义点云数据训练RandLA-Net(附数据预处理完整代码)

从零构建自定义点云数据集:RandLA-Net实战迁移指南 当我在第一次尝试将激光雷达采集的工地现场数据导入RandLA-Net时,系统报出的KeyError: semantic_kitti错误让我意识到——官方数据集与真实业务数据的鸿沟远比想象中更深。这个在学术界表现优异的点云分…

作者头像 李华
网站建设 2026/5/2 19:38:19

别只刷题了!用这5个心理学模型,真正看懂你的情绪与行为模式

解码情绪与行为:5个心理学模型帮你跳出思维陷阱 1. 情绪ABC模型:重新定义你的情绪触发点 情绪ABC模型由心理学家阿尔伯特艾利斯提出,它彻底改变了我们对情绪反应的理解方式。这个模型将情绪产生过程分解为三个关键环节: A&#xf…

作者头像 李华
网站建设 2026/5/2 19:32:29

我用 ChatGPT 新功能“走进”了三个房间,出来后沉默了五分钟

——360 视图器实测,AI 画图这次真的不一样了 你上次觉得"这个 AI 功能有点吓到我",是什么时候? 我上次是今天。 ChatGPT 悄悄上线了一个新功能:360 视图器。生成的不再是一张图,而是一个可以转动、可以环顾四周的立体空间。 我测了三个场景,截图留着,等你看完…

作者头像 李华