事情是这样的。
有个团队做了一个企业知识库问答机器人,接了向量库,也接了公司内部文档。
第一轮用户问:
我们公司的报销流程是什么?
机器人答得还不错,列了发票、审批、付款几个步骤。
第二轮用户又问:
那差旅报销呢?
机器人也能接住,补充了出差申请、酒店标准、交通票据。
到了第三轮,用户说:
我刚才说的那种情况,如果发票丢了怎么办?
机器人开始一本正经地胡说八道。
它先说可以走遗失发票证明,然后又说要找行政补开,最后甚至引用了一段根本不属于财务制度的办公用品采购文档。
你看,问题就来了。
明明接了知识库,为什么越聊越不准?
明明上下文越来越多,为什么系统反而越来越糊涂?
很多多轮 RAG 项目,demo 第一轮很亮眼,真实用户多问几句就开始变形。
不是模型突然变笨了。
更常见的情况是:系统从第二轮开始,就已经不知道自己到底在检索什么了。
单轮 RAG 的路径很简单。
用户问一个问题,系统拿这个问题去检索,再把检索结果塞给模型回答。
大概是这样:
def answer(question):
docs = vector_db.search(question)
prompt = build_prompt(question, docs)
return llm.generate(prompt)
这段代码在单轮场景里看起来没什么问题。
但一旦进入多轮对话,它就开始危险了。
因为用户不会每一轮都把完整问题重新说一遍。
用户会说:
这些问题单独拿出来,几乎没有任何检索价值。
向量库看到「这个」「刚才」「那种情况」,并不知道用户指的是差旅报销、发票丢失,还是审批流程。
所以多轮 RAG 越聊越蠢,第一层原因通常不是生成问题。
而是检索问题已经坏了。
很多系统的第一版实现都很朴素。
def chat(question, history):
docs = vector_db.search(question)
prompt = build_prompt(history, docs, question)
return llm.generate(prompt)
注意这个细节。
history 只进了 prompt,没有参与检索。
也就是说,模型回答时能看到历史,但向量库检索时看不到历史。
这就会导致一个很典型的问题:
用户轮次 | 用户问题 | 检索系统实际看到的问题 |
|---|---|---|
第一轮 | 差旅报销流程是什么 | 差旅报销流程是什么 |
第二轮 | 如果发票丢了怎么办 | 如果发票丢了怎么办 |
第三轮 | 那审批人不在呢 | 那审批人不在呢 |
第三轮的问题,对人来说很清楚。
因为人知道「审批人不在」说的是差旅报销里的审批环节。
但检索系统不知道。
它可能检索到:
这些文档看起来都和「审批人不在」相似。
但它们不一定和当前对话相关。
向量相似,不等于对话相关。
这句话是多轮 RAG 里很容易被忽略的坑。
有人会说,那我把历史对话全部塞进 prompt 不就行了?
听起来合理。
但线上系统最怕的就是这种「看起来合理」。
假设 prompt 是这么拼的:
def build_prompt(history, docs, question):
return f"""
你是企业知识库助手,请根据资料回答用户问题。
历史对话:
{history}
参考资料:
{docs}
用户问题:
{question}
"""
第一轮没问题。
第二轮还行。
第六轮、第八轮、第十轮以后,问题开始变复杂。
因为 history 里面会混进很多低密度信息:
这些东西如果不清理,会一起进入 prompt。
模型看到的不是一个干净的问题,而是一堆杂乱的对话残片。
更麻烦的是,很多系统没有标记信息来源。
历史对话:
用户:差旅报销怎么走?
助手:需要先提交申请,再上传发票。
用户:发票丢了呢?
助手:可以提交遗失说明。
参考资料:
1. 办公用品采购管理办法
2. 差旅费用报销制度
3. 固定资产申请流程
如果没有来源、时间、轮次、可信度标记,模型很难判断:
所以多轮 RAG 不是简单的「上下文越多越好」。
很多时候,上下文越长,噪声越多,答案越飘。
向量检索的目标不是理解业务。
它只是找语义上相近的文本。
在单轮问题里,这个误差还可以接受。
但多轮对话里,用户的问题越来越短,指代越来越多,向量检索的误差会被放大。
比如第三轮用户问:
那主管不批怎么办?
向量库可能召回这些文档:
候选文档 | 相似原因 | 是否适合回答 |
|---|---|---|
差旅报销审批规则 | 包含主管、审批、报销 | 适合 |
请假审批异常处理 | 包含主管、不批、流程 | 不适合 |
采购付款审批制度 | 包含审批、主管、付款 | 不适合 |
合同用印审批说明 | 包含审批、驳回、主管 | 不适合 |
如果系统直接把这些候选文档塞给模型,模型就会被污染。
它不是不知道怎么回答。
它是被迫在一堆半相关资料里猜。
比较稳的做法,是在向量召回后增加 rerank。
def retrieve(question, history_summary):
rewritten_query = rewrite_query(question, history_summary)
candidates = vector_db.search(rewritten_query, top_k=20)
docs = reranker.rank(
query=rewritten_query,
candidates=candidates,
top_k=5
)
return docs
这里有两个关键点。
第一,先把当前问题改写成独立问题。
原问题:
那主管不批怎么办?
改写后:
差旅报销流程中,如果直属主管不审批或审批被拒绝,员工应该如何处理?
第二,rerank 不能只看当前问题,最好还要看历史摘要和候选文档。
否则系统依然可能把「请假审批」这种半相关文档排到前面。
很多多轮系统里的 memory,本质上就是一个不断追加的数组。
messages.append({"role": "user", "content": question})
messages.append({"role": "assistant", "content": answer})
这很容易写。
也很容易坏。
因为真实对话不是所有内容都同样重要。
多轮 RAG 至少要把 memory 分成几类:
记忆类型 | 保存内容 | 用途 |
|---|---|---|
最近对话 | 最近 3 到 5 轮原文 | 保留语气、指代、连续追问 |
任务状态 | 当前用户正在问什么业务问题 | 帮助 query rewrite |
关键事实 | 已确认条件,比如员工类型、报销类型、地区 | 防止重复追问 |
长期摘要 | 更早对话的压缩总结 | 控制 prompt 长度 |
无效信息 | 被否定、过期、低价值内容 | 不进入主 prompt |
如果所有历史都平等进入 prompt,模型就会把垃圾信息也当成上下文。
这就是为什么有些系统聊得越久越离谱。
不是它没有记忆。
而是它什么都记。
多轮 RAG 最危险的一点是,它不是一开始就坏。
它通常是慢慢坏。
第一轮准确率 90%。
第三轮掉到 75%。
第五轮只剩 55%。
第八轮开始出现自信胡说。
如果没有评估,你只能靠用户反馈和肉眼感觉。
多轮 RAG 至少要记录这些指标:
指标 | 观察什么 | 价值 |
|---|---|---|
query rewrite 准确率 | 改写后的问题是否保留原意 | 判断检索入口是否正确 |
检索命中率 | top_k 里是否有正确文档 | 判断知识库召回质量 |
rerank 命中率 | 正确文档是否排在前面 | 判断排序是否有效 |
引用覆盖率 | 回答是否基于命中文档 | 判断模型有没有脱离资料 |
拒答率 | 资料不足时是否拒答 | 判断系统是否过度自信 |
轮次质量曲线 | 第几轮开始明显变差 | 定位多轮退化点 |
这里最关键的是轮次质量曲线。
因为单轮测试好,不代表多轮可用。
你要专门构造这种测试集:
第 1 轮:差旅报销流程是什么?
第 2 轮:如果发票丢了怎么办?
第 3 轮:那主管不批呢?
第 4 轮:如果我是外地员工呢?
第 5 轮:这种情况财务会退回吗?
然后观察每一轮:
没有这套评估,多轮 RAG 的优化很容易变成玄学。
多轮 RAG 不要指望一个技巧解决。
更稳的做法是组合设计。
把用户当前问题改写成一个独立可检索问题。
历史摘要:
用户正在咨询差旅报销流程,已确认问题涉及发票丢失和主管审批。
当前问题:
那主管不批怎么办?
改写后问题:
差旅报销中,如果主管不审批或审批被拒绝,员工应该如何处理?
但它有副作用。
Query Rewrite 可能改错用户意图。
所以改写结果最好可观测、可回放,并且在高风险场景里保留原问题一起检索。
只靠向量检索容易召回语义相似但业务不相关的内容。
更稳的是混合检索:
比如「差旅报销」这种业务词,不应该在多轮里丢掉。
召回 top 20,不代表都能塞进 prompt。
多轮场景里,rerank 至少要看三样东西:
这样才能把「差旅报销审批规则」排在「请假审批规则」前面。
不要把 history 当垃圾桶。
可以按这个结构做:
{
"recent_turns": "最近 3 轮完整对话",
"task_state": "用户正在咨询差旅报销异常处理",
"confirmed_facts": ["发票丢失", "主管未审批"],
"long_summary": "用户之前询问了差旅报销基础流程和票据要求",
"discarded": ["已经被否定的采购流程资料"]
}
这不是为了炫技。
而是为了让 prompt 里的每一段信息都有身份。
不要把所有资料混成一坨。
参考资料最好带上来源、时间、可信度和业务域。
[资料 1]
来源:财务制度中心
业务域:差旅报销
更新时间:2025-12-10
可信度:高
内容:……
[资料 2]
来源:OA 帮助文档
业务域:审批代理
更新时间:2024-09-18
可信度:中
内容:……
模型不一定真的理解你的公司制度。
但你至少要把信息优先级摆清楚。
多轮 RAG 的评估不能只看最终回答。
要看链路上的每一步:
只看答案对不对,定位不了问题。
链路拆开,才知道是检索坏了、排序坏了、记忆坏了,还是模型生成坏了。
如果面试官问你:
多轮 RAG 为什么会越聊越差?你会怎么优化?
不要只回答「加长上下文」「优化 prompt」「换更好的模型」。
可以这么说。
我会先把问题拆成四层。
第一层看 query。
多轮里用户经常使用「这个」「刚才那个」这种指代,如果不做 query rewrite,检索系统根本不知道当前问题的完整语义。
第二层看 retrieval 和 rerank。
向量相似不等于对话相关,所以召回后要结合历史摘要和当前问题做二次排序,避免半相关文档污染 prompt。
第三层看 memory。
history 不能一直 append,要分成最近对话、任务状态、关键事实和长期摘要。否则上下文越长,噪声越多。
第四层看 eval。
多轮质量不是一下子崩的,要按轮次观察 query rewrite、检索命中率、引用覆盖率和拒答率,找到第几轮开始衰减。
如果想再补一句工程判断,可以这么说:
我不会直接认为是模型能力不够。
我会先看系统有没有把当前问题变成一个可检索、可排序、可约束的问题。
很多多轮 RAG 的失败,本质上不是回答失败,而是上下文治理失败。
这个回答比背概念更像真实做过项目。
因为它不是把 RAG 当成一个框架名,而是把它拆成了一条工程链路。
用户问「我刚才说的那种情况,如果发票丢了怎么办」。
人能听懂。
因为人会自动补全上下文。
但系统不会自动懂。
如果你没有 query rewrite,它不知道「那种情况」是什么。
如果你没有 rerank,它会把半相关文档塞进 prompt。
如果你没有 memory 分层,它会把历史里的噪声一起喂给模型。
如果你没有多轮 eval,你甚至不知道系统是从第几轮开始变差的。
所以多轮 RAG 越聊越蠢,通常不是因为模型突然不聪明。
而是你的系统没有认真管理三件事:
把这三件事做好,多轮 RAG 才有机会从 demo 走向生产。
否则它第一轮像专家,第三轮像猜谜,第五轮就开始一本正经地乱答。
这不是智能不够。
这是工程没收口。