编程与 AI12 分钟阅读更新于 2026-07-31

RAG 对话检索链:让追问能接住上下文

围绕ConversationBufferMemory、ConversationalRetrievalChain、generated_question、db_query、source_documents 与聊天机器人 UI 的内容,讲清单轮 RAG 为什么接不住追问,以及对话型 RAG 应该怎么设计和调试。

相关工具

单轮 RAG 最怕用户追问

单轮 RAG 处理“这门课会学习 Python 吗”这类问题时,流程很清楚:拿当前问题去检索,取回相关文档,再把文档交给模型回答。问题一旦变成追问,事情就不一样了。用户说“为什么需要这个前提”,这句话里没有 Python,没有课程名,也没有明确检索词。

相关概念 里做过这个实验:第一次问“这门课会学习 Python 吗”,普通 RetrievalQA 能给出答案;接着追问“为什么需要这一前提”,单轮链没有状态,不记得前一个问题和答案,结果就会把“前提”理解错。它不是不会写答案,而是不知道用户在指哪件事。

这就是对话型 RAG 要解决的问题。用户的第二句话、第三句话经常依赖前文。客服里会说“那这个怎么处理”,学习场景会说“为什么要先学这个”,技术支持会说“上面那个报错还有别的原因吗”。如果系统只看当前句子,检索阶段就已经偏了。

单轮 RAG 与对话 RAG 的差别图
单轮 RAG 与对话 RAG 的差别

单轮 RAG 只看当前问题,容易丢失指代;对话 RAG 会把历史对话和当前追问合并后再检索。

对话检索链的第一步是改写问题

资料 对 ConversationalRetrievalChain 的流程描述很直接:先把之前的对话与新问题合并,生成一个完整查询语句;再在向量数据库中搜索相关文档;拿到结果后回答,并把答案写入对话记忆。这里最重要的不是“多了记忆”,而是“检索前先改写问题”。

比如用户追问“为什么需要这个前提”,系统要结合上一轮,把它改写成“为什么学习 Python 是这门课的前提”。只有改成完整查询,向量库才知道该查 Python、课程前置知识、Matplotlib 学习条件这些材料。

很多对话 RAG 的错误都发生在这一步。改写得太窄,会漏掉相关文档;改写得太宽,会检索出一堆泛泛材料;改写时把历史理解错,后面的答案看起来再顺也没用。所以工程上要把生成后的查询暴露出来,而不是只看最终回答。

对话检索链工作流程图
对话检索链工作流程

追问先和历史合并,改写成完整查询,再进入向量检索和答案生成。

ConversationBufferMemory 只负责保存历史,不负责判断历史是否有用

文中使用 `ConversationBufferMemory` 保存聊天消息历史,并设置 `memory_key="chat_history"`,同时让它以消息列表的形式返回历史记录。它的作用很朴素:把用户和助手之前说过的话留下来,供后续链路使用。

但保存历史不等于历史都应该进入检索。一次完整对话可能包含闲聊、确认、错误尝试、跑偏回答和真正有价值的上下文。Buffer Memory 会保存这些内容,却不会替你判断哪几句对当前追问最关键。

所以早期项目可以先用 Buffer Memory 跑通流程,但上线时要控制历史长度,必要时加摘要、窗口或实体记忆。否则对话越长,改写问题越容易受噪声影响,检索也会被旧信息带偏。

return_generated_question 是调试对话 RAG 的关键开关

资料 在定义私人文档聊天机器人时,创建 `ConversationalRetrievalChain.from_llm`,同时设置了 `return_source_documents=True` 和 `return_generated_question=True`。前者返回源文档,后者返回最终发送给数据库的改写问题。

这个字段非常值得保留。用户看到的是“为什么需要这个前提”,数据库真正收到的可能是“为什么学习 Python 是课程前提”。如果答案错了,你要先看 generated_question。它如果错了,后面检索和生成都只是顺着错误往下走。

一个常见排查顺序是:先看原始追问,再看 generated_question,再看 source_documents,最后看答案。generated_question 对,source_documents 错,说明检索或 metadata 有问题;source_documents 对,答案错,才轮到 Prompt 或生成模型。

source_documents 仍然是对话答案的底线

对话 RAG 不能因为有历史,就放松来源要求。历史对话解决的是指代和上下文,source_documents 解决的是答案依据。用户问“为什么这个前提重要”,系统可以从历史里知道“这个前提”指 Python,但关于原因,仍然应该回到课程资料或知识库。

相关的聊天机器人示例把 `result["source_documents"]` 存到 `db_response`,并在界面里提供“Result of DB lookup”区域。这个设计很实用:用户看到聊天,使用者看到每轮检索到了哪些源文档。

如果只保存聊天记录,不保存源文档,对话越长越难查错。你不知道某轮答案依据哪份 资料,也不知道是第几页材料被召回。对话 RAG 要有连续性,也要有可追溯性,两者缺一边都会变脆。

调试面板应该展示四个东西

后文的 Panel UI 示例把调试思路讲得很明白:界面不只是一个聊天窗口,还能查看最后发送到数据库的问题、数据库返回的源文件、当前聊天历史。实际应用中,可以把它整理成四个区域:聊天记录、DB query、source_documents、最终答案。

聊天记录让你看到用户真正说了什么;DB query 让你检查问题改写;source_documents 让你核对检索来源;最终答案让你判断生成是否用了证据。四个区域放在一起,错误会清楚很多。

不要等线上出问题才临时翻日志。早期开发和内容验收时,就应该用这样的面板跑测试问题。每次遇到追问答错,先看它是哪一层错了。这样比反复改 Prompt 更稳,也更省时间。

对话 RAG 调试面板 UI 草图
对话 RAG 调试面板

调试面板同时展示聊天记录、改写后的 DB query、命中的 source_documents 和最终答案。

清空历史不是小按钮,是产品能力

相关的聊天机器人 UI 里有 `Clear History` 按钮,用来清除聊天记录并开始新话题。这个细节很容易被忽略,但在实际产品里很重要。用户换了一个问题域,如果系统还带着上一段历史,问题改写就可能错。

比如刚才一直在问 Matplotlib,下一句突然问“这个政策什么时候生效”,系统如果还把课程历史带进去,就会查错资料。客服、内部知识库、学习助手都一样:话题切换时,用户需要一个明确的方式告诉系统“从这里重新开始”。

清空历史也不一定只能靠用户手动点。系统可以根据文件切换、专题切换、长时间无交互、用户明确说“换个问题”等信号,提示是否开启新会话。对话记忆有用,但它应该有边界。

别把对话记忆当长期记忆

对话检索链里的 chat_history 主要服务当前会话。它帮助系统理解“这个”“刚才那个”“为什么”等追问。长期记忆则是跨会话保存偏好、实体和历史摘要。两者都叫记忆,但用途不同。

如果把当前对话里每一句都写入长期记忆,系统很快会记下大量临时信息。用户今天为了测试问了一个例子,系统以后可能还当成真实偏好。反过来,如果完全没有对话记忆,系统连同一轮里的追问都接不住。

比较稳的分层是:当前会话用 Buffer 或窗口记忆;跨会话只保存经过筛选的偏好、实体和摘要;知识库事实仍然走文档检索和 source_documents。这样对话能连贯,长期记忆也不至于变成杂物堆。

常见问题

为什么普通 RetrievalQA 接不住追问?

因为它通常只看当前 query,没有保存和使用上一轮问题与答案。追问里的“这个”“前提”等指代无法被正确还原。

generated_question 有什么用?

它能显示系统最终拿什么问题去查向量库。答案错了时,先看 generated_question,能快速判断问题改写是否跑偏。

对话 RAG 要不要保留 source_documents?

要。对话历史只负责理解上下文,source_documents 才是答案依据,缺少它就很难核对和排查。

RAG、知识库与长期记忆

继续阅读

返回专题