检索式问答链怎么搭:从相关文档到可追溯答案
围绕问答章节,讲清 RetrievalQA 如何把问题、检索结果和 Prompt 模板组合起来,为什么要返回 source_documents,以及 Stuff、MapReduce、Refine、MapRerank 这些链路策略适合什么场景。
相关工具
检索结束后,RAG 才真正开始回答
前面几篇已经把资料加载、切分、Embedding、向量数据库和检索讲完了。到这一步,系统能根据用户问题找回几段相关文档。但找到文档还不是回答。相关的第六章把这个阶段说得很清楚:LangChain 访问外部数据通常经过 Document Loading、Splitting、Storage、Retrieval、Output 五步;前四步是在准备资料,第五步才是把资料交给语言模型生成答案。
这个顺序很重要。很多人做知识库问答时,会把“检索到了相关文档”和“系统已经能准确回答”混在一起。实际并不是这样。检索器只负责把可能有用的 chunk 找出来,语言模型还要读这些 chunk,理解用户问题,再把答案组织成一句能给人看的话。如果检索结果、提示模板和输出约束没有配好,模型拿到正确资料也可能答得散、答得长,甚至把没有依据的内容顺手补进去。
所以,检索式问答链要解决的是一个连接问题:用户问题怎么进入检索器,相关文档怎么进入提示词,提示词怎么约束模型,模型的回答又怎么带回来源。文中用 RetrievalQA 演示了这个连接过程,它不是另一个神秘模型,而是一条把检索和生成串起来的工作流。

用户问题先做向量检索,相关文档和来源信息进入 Prompt 模板,再由 LLM 生成带依据的回答。
RetrievalQA 的基础版本很短,但里面事情不少
资料 先加载之前持久化好的 Chroma 向量库,里面有 27 个 Matplotlib 文档片段。然后用 similarity_search 测试一个问题:“这节课的主要话题是什么”,向量库返回 3 个相关文档。接着进入 RetrievalQA:创建 ChatOpenAI,温度设为 0,再把 vectordb.as_retriever() 传给 RetrievalQA.from_chain_type。最后用 qa_chain({ query: question }) 调用,得到回答。
示例里的回答说,这节课主要介绍 Matplotlib,包括 Python 2D 绘图库、静态和交互式图表、基本概念、最简单的绘图例子、Figure 的组成和 Axis 属性。这个答案之所以能说到这些,不是模型凭空记住了那份 资料,而是检索器把 Matplotlib 的课程资料片段放进了上下文。
从工程角度看,RetrievalQA 做了三件事。第一,把用户问题交给 retriever,拿回相关文档。第二,把相关文档拼进 prompt 的 context 位置。第三,把完整 prompt 交给 LLM,拿回 result。它的代码很短,是因为 LangChain 把这些步骤封装好了;但调试时不能只看最后的 result,还要能看到中间到底检索了哪些文档。
Prompt 模板决定模型怎么使用资料
后文没有停在默认链路,而是手动定义了 PromptTemplate。模板里的话很直接:使用以下上下文片段回答最后的问题;如果不知道答案,只说不知道,不要试图编造;答案最多三个句子,尽量简明;最后说“感谢您的提问!”。模板下面有两个占位符:{context} 和 {question}。
这段模板值得拆开看。{context} 是检索回来的文档片段,{question} 是用户的问题。模型并不知道哪些文字是资料、哪些文字是指令,除非模板把边界说清楚。好的模板不是堆一串漂亮规则,而是把回答范围、拒答条件、长度、语气和引用要求写明白。尤其是“不要编造答案”这类约束,在知识库问答里比礼貌话术重要得多。
不过模板也不能弥补所有问题。如果检索回来的 context 本身没有答案,模型最多只能按模板说不知道;如果 context 混入了旧资料或错误资料,模板也很难保证模型完全避开。所以 PromptTemplate 是生成层的护栏,不是检索层的替代品。它要和资料清洗、metadata 过滤、MMR、source_documents 一起工作。
source_documents 是知识库问答的底线配置
示例在构造 RetrievalQA 时开启了 return_source_documents=True。这样调用链路后,result 里不仅有 result['result'],还会有 result['source_documents']。示例打印第一个 source document,可以看到它包含 Matplotlib 的正文片段,从“第一回:Matplotlib 初相识”开始,里面有 Matplotlib 定义、Figure、Axes、plot 示例等内容。
这个配置看起来只是多返回一点数据,实际很关键。没有 source_documents,用户只能看到一段回答,却不知道它来自哪份资料。使用者也很难排查:答案错了,到底是没检索到正确文档,还是模型没有按文档回答?打开来源后,问题会清楚很多。你能看到命中的原文、source、page,能判断检索是否跑偏,也能给前端做“引用来源”或“查看依据”。
在真实产品里,source_documents 不一定全部展示给用户,但后台一定要留。客服知识库、企业制度问答、研发文档助手都需要追溯依据。尤其是涉及政策、价格、合同、权限的内容,只给一个流畅回答是不够的。回答越像人说的,越需要让人能点回原文确认。
Stuff 简单直接,但会撞上下文窗口
文中提到,默认情况下会把所有文档切片放进同一个上下文窗口,也就是一次语言模型调用中。这种方式通常叫 Stuff。它很好理解:检索到几段资料,就把它们一起塞进 prompt,让模型一次读完后回答。资料不多、chunk 不长、问题简单时,Stuff 很省事。
但它的边界也明显。检索返回的文档一多,或者每个 chunk 本来就很长,prompt 很快会接近上下文窗口上限。到了这个时候,你要么减少 k,要么压缩文档,要么换一种链路策略。硬塞的结果通常不好:重要片段可能被截断,无关片段占用位置,模型读到一堆材料后回答反而变散。
所以 Stuff 适合第一版原型,也适合资料片段短、召回数量可控的问答。它不是低级方案,而是一个适合小上下文的直接方案。真正的问题不是“能不能用 Stuff”,而是你有没有评估过:每次平均塞进去多少字,Top-K 里是否重复,答案是否真的需要这么多文档。

Stuff、MapReduce、Refine、MapRerank 都是在处理同一个问题:检索回来的资料太多时,怎么让模型更稳地生成答案。
MapReduce、Refine、MapRerank 解决的是长文档压力
资料 把 MapReduce、Refine、MapRerank 作为三种应对短上下文窗口的方法。MapReduce 的思路是分批处理:先让模型分别根据不同文档片段生成局部答案,再把这些局部答案合并成最终答案。它适合资料多、可以拆开看的场景,比如总结多份会议记录、汇总多个章节对同一问题的说明。
Refine 更像逐步修订。模型先根据第一批资料生成一个初始答案,然后读后面的资料,不断补充、修正、收紧。它适合答案需要随着新证据逐渐完善的情况。比如一个产品功能在多个版本文档里都有描述,Refine 可以把后续文档里的限制条件补进去。
MapRerank 则把重点放在评分和选择上。模型会基于不同文档生成候选答案,并给这些候选答案打分或排序,最后选出更可信的一条。它适合召回结果里噪声比较多、需要挑选最佳依据的场景。三种策略没有绝对优劣,选择时先看资料量、问题类型、延迟预算和可解释性要求。
做成聊天机器人时,要把中间状态展示出来
聊天机器人示例很有参考价值。它用 ConversationalRetrievalChain 做文档问答,用 ConversationBufferMemory 保存聊天历史,又在界面里做了几个 tab:Conversation 显示对话,Database 显示最后发送到数据库的问题和检索结果,Chat History 显示当前历史记录,Configure 用来上传 资料、Load DB、Clear History。
这个设计比单纯做一个输入框更适合调试。用户看到的是聊天体验,使用者需要看到的是检索链路。尤其是对话检索链会把新问题和历史信息合并成一个完整查询,再拿这个查询去向量库里找资料。如果第二轮问题是“为什么需要这个前提?”,系统要知道“这个前提”指的是上一轮提到的 Python。相关的示例里,模型虽然回答内容有些偏,但它确实识别出了指代关系。
因此,文档问答界面最好别只展示最终答案。至少要能在调试区看到 generated_question、source_documents、chat_history 和当前加载的文件。上线后这些信息可以藏到后台,但开发阶段一定要看得到。RAG 系统的很多问题,不打开中间状态根本排不出来。

一个实用的 RAG 聊天界面不只显示答案,还应该能查看数据库查询、来源文档、聊天历史和当前配置。
落地时按链路排查,不要只改提示词
检索式问答出错时,最常见的反应是改 prompt。prompt 当然要改,但别急着把所有问题都推给它。更稳的排查顺序是:先看用户原始问题,再看 retriever 实际查了什么,再看 source_documents 是否包含答案,再看 prompt 是否把资料和问题放清楚,最后才看模型生成是否按要求回答。
如果 source_documents 里没有正确资料,应该回到检索层:调整 chunk、k、metadata 过滤、MMR、压缩检索或重排。如果 source_documents 正确但回答啰嗦,可以改模板里的长度和输出格式。如果答案没有引用来源,要检查链路是否返回了 source_documents,以及前端是否把它展示出来。如果多轮对话指代错了,要看 generated_question 是否把历史问题改写对。
这也是 文中这些示例真正有用的地方。它不是只给一段能跑通的代码,而是把链路的几个观察点露出来:向量库数量、相似性检索结果、PromptTemplate、source_documents、chat_history、db_query。把这些点留下,RAG 才不是一个只能祈祷答案正确的黑盒,而是一套能被调试、能被解释、能逐步变准的问答系统。
常见问题
RetrievalQA 和普通调用大模型有什么区别?
普通调用只把用户问题交给模型;RetrievalQA 会先检索相关文档,再把文档和问题一起放进 prompt,让模型基于外部资料回答。
为什么一定要返回 source_documents?
它能让答案可追溯,也方便使用者判断错误来自检索还是生成。没有来源文档,知识库问答很难排查和复核。
Stuff、MapReduce、Refine、MapRerank 应该怎么选?
资料少、上下文够用时先用 Stuff;资料多时考虑 MapReduce;需要逐步补充答案时用 Refine;召回噪声多、需要挑最佳依据时考虑 MapRerank。