对话检索链怎么记住上下文:从追问到私有文档聊天机器人
围绕Chat 章节,讲清普通 RetrievalQA 为什么没有状态,ConversationBufferMemory 保存什么,ConversationalRetrievalChain 如何把追问改写成完整查询,以及私有 资料 聊天机器人应该暴露哪些调试信息。
相关工具
普通 RetrievalQA 不会记得上一轮说了什么
资料 在进入 Chat 章节前,先展示了一个很典型的问题。用户先问“这门课会学习 Python 吗?”,系统回答会涉及 Python,特别是在数据可视化方面。紧接着用户追问“为什么需要这一前提?”,普通 RetrievalQA 给出的回答却转到了 Matplotlib 的 Figure、Axes、Artist 等概念上。它看起来回答得完整,实际已经偏题。
偏题的原因不是模型完全不懂,而是链路没有状态。普通 RetrievalQA 每次调用只看到当前 query,它不知道上一轮问题是什么,也不知道“这一前提”指的是 Python。对人来说,追问里的“这个”“它”“前面那个要求”很自然;对一个无状态问答链来说,这些词缺少可检索对象,系统只能拿当前这句话去向量库里找相似内容。
这也是文档问答从“问答”走向“聊天”时必须补的一环。RAG 不是只要能回答单轮问题就够了。真实用户会追问,会省略主语,会把上一轮答案里的词拿来继续问。如果系统没有聊天历史,第二轮问题经常会退化成一个模糊查询,检索错了,后面的回答再顺也没用。

普通 RetrievalQA 每轮独立处理问题;对话检索链会把 chat_history 纳入上下文,先生成完整查询再检索。
Memory 保存的是对话历史,不是长期知识库
资料 接着引入 ConversationBufferMemory。它的作用很直接:保存聊天消息历史记录列表,并在回答问题时把这些历史和当前问题一起传给聊天机器人。示例里设置 memory_key="chat_history",并且 return_messages=True,也就是用消息列表的形式返回历史,而不是把历史压成一个长字符串。
这里要分清两个概念。向量数据库保存的是外部资料,比如 资料、课程讲义、产品手册;Memory 保存的是当前会话里用户和助手刚刚说过什么。前者更像资料库,后者更像对话上下文。用户问“第二讲的 Figure 怎么理解”,需要检索知识库;用户接着问“那它和 Axes 有什么区别”,就需要聊天历史帮系统知道“它”指的是 Figure。
ConversationBufferMemory 也不是越长越好。它把历史带进上下文,历史越多,占用的上下文窗口越多。如果用户聊了很久,旧消息可能让 prompt 变长,也可能把当前主题带偏。第一版可以先用 buffer memory 跑通多轮追问,后面再考虑摘要记忆、窗口记忆或把长期偏好另存为结构化信息。
对话检索链先把追问改写成可检索问题
文中对 ConversationalRetrievalChain 的流程做了拆解:先把之前的对话与新问题合并,生成一个完整查询语句;再用这个查询去向量数据库里搜索相关文档;拿到结果后,把答案存回对话记忆;用户也可以在 UI 中查看完整对话流程。
这个“生成完整查询”是关键。用户问“为什么这门课需要这个前提?”时,系统不应该直接拿这句话去检索。它要结合上一轮“这门课会学习 Python 吗?”和回答里的 Python,把追问改成更明确的问题,比如“为什么学习这门 Matplotlib 课程需要 Python 前提?”改写后的 generated_question 才更适合进入向量库。
相关的中文示例里,第二轮回答内容仍有些不对劲,但它至少准确判断了“这个前提”指的是学习 Python。英文示例更清楚:先问 probability 是否是课程主题,再追问 why are those prerequisites needed,系统能把 those prerequisites 理解为 basic probability and statistics。这说明对话链解决的是指代和上下文衔接问题,不保证资料召回和答案生成一定完美。
load_db 把私有 资料 变成一个可聊天对象
后文给了一个 load_db 函数,用来定义适用于私人文档的聊天机器人。流程很完整:先用 Py资料Loader 载入 资料;再用 RecursiveCharacterTextSplitter 按 chunk_size=1000、chunk_overlap=150 切分文档;然后用 OpenAIEmbeddings 生成向量;接着用 DocArrayInMemorySearch.from_documents 建立内存向量库;最后把 retriever 交给 ConversationalRetrievalChain。
这个函数里有两个参数很值得保留:return_source_documents=True 和 return_generated_question=True。前者让系统返回检索到的来源文档,方便引用和排查;后者让系统返回真正发送给数据库的改写查询,方便确认“追问”有没有被理解对。没有这两个返回值,聊天机器人看起来也能运行,但出了错很难知道错在哪里。
示例还把 Memory 放在外部管理。这个选择很实用。文档加载、切分、建库和检索器是一条资料链;聊天历史是会话状态。把它们混在一起,上传新 资料、清空历史、切换文件时很容易乱。分开管理后,用户换文档可以重建知识库,用户开启新话题可以清空 chat_history,两件事互不干扰。

从上传 资料 到构建 ConversationalRetrievalChain,load_db 把加载、切分、向量化、检索器和对话链串起来。
界面别只留聊天框,调试面板也要有
示例用 Panel 和 Param 做了一个简单界面,分成 Conversation、Database、Chat History、Configure 四个 tab。Conversation 显示用户和 ChatBot 的对话;Database 显示最后发送到数据库的问题和检索结果;Chat History 显示当前聊天历史;Configure 负责上传 资料、Load DB 和 Clear History。
这个界面设计看着朴素,但对 RAG 开发很有用。Conversation 是给用户看的,Database 和 Chat History 是给使用者看的。用户问一句话后,系统到底生成了什么 db_query,向量库返回了哪些 source_documents,当前 chat_history 里有几轮上下文,这些都应该能看见。否则你只能盯着最终答案猜。
上线时不一定把这些调试信息直接暴露给普通用户,但后台最好保留。尤其是 return_generated_question 和 source_documents,可以帮助你判断:是追问改写错了,还是检索错了,还是模型没有按资料回答。RAG 聊天系统能不能稳定,很多时候取决于这些中间状态有没有被记录下来。

一个可调试的文档聊天界面,应该同时显示对话、改写查询、来源文档、聊天历史和当前配置。
多轮对话的失败点通常在三处
第一处是追问改写。用户说“它”“这个前提”“刚才那个方法”,系统要把这些词还原成具体对象。如果 generated_question 没有改写对,后面检索就会走偏。调试时不要只看 answer,要先看 generated_question 是否像一个独立问题。
第二处是历史污染。ConversationBufferMemory 会把历史放进上下文,但历史里可能有旧主题、错误回答、用户临时改变方向。用户从 Matplotlib 聊到报销制度,如果系统还把前面的 Figure、Axes 带进去,检索和回答都可能变脏。Clear History 不是装饰按钮,它是切换话题时的必要操作。
第三处是检索结果不足。即使追问改写正确,向量库也可能没有找到合适文档,或者 source_documents 里只有相近但不准确的片段。这时不能怪 Memory。Memory 只负责让问题更完整,不能替代资料质量、metadata 过滤、MMR 或压缩检索。多轮 RAG 仍然要按“问题改写、资料召回、答案生成”分层排查。
第一版可以轻,但要把边界讲清楚
如果要做一个私有文档聊天机器人,第一版不必复杂。按 相关的思路,先支持上传一个 资料,切成 chunk,放进内存向量库,用 ConversationalRetrievalChain 处理多轮问题,再把 source_documents、generated_question 和 chat_history 显示在调试区。这个版本已经能覆盖很多学习和内部资料问答场景。
但边界要说清楚。它记住的是当前对话,不是永久记住用户偏好;它回答的是已上传资料,不是自动知道所有业务系统;它能处理追问,但不能保证每次追问都改写正确;它能返回来源,但来源是否可信还取决于 资料 本身是否干净、是否过期、是否切分合理。
把这些边界讲清楚,反而会让产品更可靠。用户知道什么时候该清空历史,使用者知道什么时候该看 generated_question,运营知道资料更新后要重新建库。对话检索链的价值不在于让机器人显得更会聊天,而在于让它在连续提问里尽量不丢上下文,还能把答案追溯回资料。
常见问题
ConversationBufferMemory 会把资料永久存进知识库吗?
不会。它保存的是当前会话的聊天历史,和向量数据库里的外部资料不是一回事。关闭会话或清空历史后,这部分上下文就不再参与回答。
为什么要看 generated_question?
多轮追问通常需要先改写成完整查询。generated_question 能告诉你系统到底拿什么问题去检索,方便判断指代是否理解正确。
什么时候应该清空 Chat History?
用户切换话题、换文档、前面回答明显错误,或者历史太长影响当前问题时,都应该清空或缩短聊天历史。