跳到主要内容

串成一条链 — 检索问答,以及第一次跑出来的失败

这一章讲三件事: 怎么把前面六章的零件串成一条能跑的链; 这条链的第一次运行为什么失败;以及这次失败比成功更值钱在哪。

它在全书链条里的位置: 这是第 01 章那张八步图里的第 ③ 步「搭建整体架构」。 做完这一章,你手上第一次有了一个从提问到回答全程能跑通的东西。

本章主走查的输入:「什么是南瓜书?」 它会走完取块、填模板、问模型三步,拿回一句话。 那句话是「抱歉,我不知道南瓜书是什么。谢谢你的提问!」—— 这是原书真实跑出来的输出,不是我们编的。

1. 一条链上串着三样东西

这一节先给全景,免得你在四个参数里迷路。

到目前为止你手上的零件是散的:一个能取块的库(第 06 章)、 一个能调模型的函数(第 02 章)、一段留了两个空的话(第 01 章)。 把它们串起来的东西,框架里叫「链」。

用户问「什么是南瓜书?」

├─ ① 拿这句话去库里取 k 块 ← 第 06 章那个库

├─ ② 把取回的块填进 {context},
│ 把问题填进 {question} ← 第 01 章那段模板

└─ ③ 把填好的整段话发给模型 ← 第 02 章那次调用
← 回来一句话

图说:三步全自动,你只调一次链、传一个问题字符串。
中间那两次(取块、调模型)你都看不见。

这三样在建链时对应三个参数,第 4 节会逐个对上。

2. 框架没接的那家模型,自己接上去

这一节和第 06 章第 3 节形状一样,但接的是另一样东西——别把两者搞混。

第 06 章接的是向量模型(把文字算成一串数的那个),要写三个方法; 这一节接的是对话模型(读一段话、回一段话的那个),要写的是另外两样

书的处境同样是:框架内置了常见的几家,但没有内置所有的, 它通过允许用户自定义,来提供扩展性1。书为文心和智谱各写了一个2

做法是继承一个基类,补两样东西3:

要补的干什么
_call真正去发那次网络请求的地方:收一段提示词,还一段回答
_llm_type一个字符串,说明这是哪一家——框架记运行流水、报错时用它

这两样比向量那三个少,原因很朴素:对话模型没有「一次处理一批」这回事。 一次一段话进、一段话出,没有可合并的余地。

顺带说一句版本:书自己提醒,新版框架已经能直接调文心了, 它更推荐用现成的那个,而不是自己写的这个2——自定义那一节留着是为了教你方法,不是让你照抄。

3. 那段模板:两个空,谁来填

这一节把第 01 章那段话正式接进链里。

框架里管这种带空的话叫提示词模板——就是一段留了占位符的文字, 用的时候把占位符换成真正的内容4。这门课那一份是5:

template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说"谢谢你的提问!"。
{context}
问题: {question}
"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context", "question"],
template=template)

注意 input_variables 那一行:你要明确告诉它有哪两个空。

这两个空谁来填?

谁填填什么
{context}链自动填上一步从库里取回来的那几块的正文,拼在一起
{question}链自动填用户传进来的那句话,原样

你一个字都不用拼。 这正是「链」这个东西存在的理由。

4. 建链:四个参数

这一节是主走查的第 1 步。

建链的写法是一句话,四个参数6:

qa_chain = RetrievalQA.from_chain_type(
llm, # ① 用哪个模型
retriever=vectordb.as_retriever(), # ② 去哪儿取块
return_source_documents=True, # ③ 把用到的块也还给我
chain_type_kwargs={"prompt": QA_CHAIN_PROMPT} # ④ 用我这段模板
)

第 ② 个参数需要多说一句。 vectordb.as_retriever() 是把第 06 章那个库 包成一个「只负责按问题取块」的东西——链不认识库,只认识这个取块的角色。 取几块用的是默认值 4,你也可以自己指定7

书还提到第五个可选参数 chain_type,管的是「取回好几块之后怎么合并」—— 默认是全部拼在一起塞进 {context},也可以换成别的合并方式6这门课只用默认那种。

5. 跑一次,它答不上来

这一节是主走查的终点,而它是一次失败。

原书拿两个问题跑了这条链8:

question_1 = "什么是南瓜书?"
question_2 = "王阳明是谁?"

大模型+知识库后回答 question_1 的结果:
抱歉,我不知道南瓜书是什么。谢谢你的提问!

大模型+知识库后回答 question_2 的结果:
我不知道王阳明是谁。谢谢你的提问!

图说:两句都答不上来,而且原因是同一个:库里都没有。
区别在于「该不该没有」——王阳明本来就不在这份资料的范围里;
而南瓜书是这门课的主角资料,它本该在库里,却被挡在了外面。

先确认链本身是通的。 证据在那两句回答里: 「谢谢你的提问!」这七个字是我们模板里写的,模型自己不会加。 它出现了,说明模板被填了、被发出去了、回答被拿回来了。三步一步没少。

那为什么南瓜书没答上来? 回到第 06 章第 2 节那个 [:20]:库里只有前 20 块, 而那 20 块全部来自知识库里的另一份资料——那份提示工程教程南瓜书的内容一块都没进库(证据是那一章第 4 节取回的三块,没有一块出自南瓜书)。

所以这次失败只有一笔账:库里根本没有答案。 第 06 章第 7 节那条「配对不是因果」在这里还用不上——它管的是「候选里有答案却没被排到前面」, 而这里连候选都没有。 那条性质要到第 11 章评检索时才真正派上用场。

这笔账一处都不在这条链上。 链是对的,料没备。

6. 但这次失败比成功更值钱

这一节是本章唯一的另起走查,而且它是本章最重要的一节。

原书紧接着做了对照:同样两个问题,不接知识库、直接问模型9:

直接问「什么是南瓜书?」:
南瓜书是指一种关于南瓜的书籍,通常是指介绍南瓜的种植、养护、烹饪等方面知识的书籍。
南瓜书也可以指一种以南瓜为主题的文学作品。

直接问「王阳明是谁?」:
王阳明(1472年-1529年),字宪,号阳明,浙江绍兴人,明代著名的哲学家、军事家、
教育家、政治家。他提出了"致良知""格物致知"等重要思想……

图说:第二句它答对了(王阳明是常识);第一句它编了。
而接了知识库那一版,两句都说了「不知道」。

把四个格子摆成一张表,这一章的价值就出来了:

直接问模型接了知识库
「什么是南瓜书?」编了一段南瓜种植书说不知道
「王阳明是谁?」答对了说不知道

左上角那一格是最危险的:它错了,而且你看不出它错了。 左下变右下是一次退步(本来能答的常识不答了),但左上变右上是一次巨大的进步—— 因为「我不知道」是一个你能处理的答案,而一段编造的介绍不是。

原书自己的总结是:它对近几年的知识以及非常识性的专业问题回答得并不好, 而加上本地知识就可以帮助它做出更好的回答,另外也有助于缓解幻觉问题9

代价也要说清楚:接了知识库之后,它连王阳明都不肯答了。 因为模板里那句「如果你不知道答案,就说你不知道」是对着 {context} 说的—— 库里没有,它就当作不知道。这是你自己写进去的规矩,不是它变笨了。

7. 把用到的原文块要回来

这一节兑现第 05 章第 2 节欠下的那笔账。

第 05 章讲过:每一块文字都别着一张卡片,上面记着 source(从哪个文件来)和 page(第几页)。 现在这张卡片终于有用了。

建链时那个第 ③ 个参数 return_source_documents=True,作用就是: 除了那句回答,把这次真正用到的那几块原文连同它们的卡片一起还给你6。 拿法是 result["source_documents"]

这件事的价值有两层:

第一层,调试。 答案不对时,你能立刻分清是取块取错了还是模型答歪了—— 这正是第 11 章那七个打分维度里的头一个。

第二层,给用户看出处。 有了 sourcepage, 你可以在回答后面附一句「依据:南瓜书 第 2 页」。

第 11 章会把这四块真的打印出来,你会在那里看到卡片上那个记页码的格子分别是 1、1、0、1。

一处要澄清的:第 10 章还有一种「标来源」的做法,和这个不是一回事。 那一种是在提示词里加一句「请你附上回答的来源原文」,让模型自己从 {context} 里抄一段; 而这一节这个参数是程序拿到的、准确的元数据。 前者会跟着模型一起出错,后者不会。

8. 边界:书这一章的代码已经过时

这一节交代照抄会遇到的问题。

第一,满屏警告。 弃用就是「还能用,但官方已经宣布哪个版本要删掉它」。 书里印出来的运行结果中夹着框架的这类提醒: __call__ 这种调法在 0.1.0 版被弃用,0.2.0 会删掉,请改用 invoke10。 书里所有 qa_chain({"query": …}) 的写法都属于这一类。

第二,还有一种更新的串法书只顺带提了一句。 框架后来推出一种用竖线把零件串起来的写法,形状就像 Unix 管道: chain = prompt | model | output_parser——左边的输出直接当右边的输入; 这种竖线写法的名字叫 LCEL,你在 LangChain 今天的文档里到处会撞见它11

书在讲接入模型那一节演示过它,但真正搭检索问答链时又回到了老写法,两处不一致。

第三,这一章有一个数没有被交代:取几块。 as_retriever() 不传参数时取 4 块,书正文里没有写出这个 4—— 它只在第三部分那个项目的代码注释里出现过一次7

判断(我们的,不是书里的): 这一章印出来的那次失败,是这本书里最有教学价值的一次事故, 但书没有把它当事故处理——原文只用一句「加上本地知识就可以帮助它做出更好的回答」带过, 读者很容易以为「不知道」就是接了知识库之后的正常表现。 本组拆解把这笔账钉死在「库里没有南瓜书」上,是为了让你知道该去改哪儿。 如果错,会错在: 如果错,会错在归因方向——调 k 是没用的: as_retriever() 从 4 块调到 10 块,取回的仍然是那 20 块提示工程教程里的十块,里面没有答案。 必须动库。 你可以自己验:把第 06 章那个 [:20] 去掉,让南瓜书也进库,再问同一句。 如果这时它答上来了,这条判断成立;如果照样答不上来,那说明真正的病在检索(取几块、或者配对不是因果), 这条判断就要改。

9. 可带走的

  1. 链把三步包成一次调用:取块 → 填模板 → 问模型;
  2. 建链四个参数:用哪个模型、去哪儿取块、要不要把块还给我、用哪段模板;
  3. as_retriever() 把库包成一个只管取块的角色,默认取 4 块;
  4. 接一个框架不认识的对话模型,只要补两样:_call(真正发请求)和 _llm_type(报自己是谁);比接向量模型少一样,因为它没有「一次处理一批」这回事;
  5. 书自己说:新版已经能直接调文心,自定义那一节是教方法,不是让你照抄;
  6. 第一次跑就失败了:问「什么是南瓜书?」,答「抱歉,我不知道南瓜书是什么。谢谢你的提问!」;
  7. 链是通的——证据是那句「谢谢你的提问!」,它来自你自己的模板;
  8. 失败只有一笔账:库里只有 20 块,而那 20 块全是另一份资料,南瓜书压根没进库;调 k 没用,必须动库;
  9. 最值钱的对照:不接知识库时它编了一段南瓜种植书,接了之后它说不知道——「不知道」是你能处理的答案,编造不是;
  10. 代价:接了之后它连王阳明都不答了,因为模板里那句「不知道就说不知道」是对着取回的块说的;
  11. return_source_documents=True 能把用到的块连同来源和页码还给你——调试和给用户看出处都靠它;
  12. 书这一章的代码已被标记为将要移除,新写法是用竖线把零件串起来,它叫 LCEL

10. 原文地图

主题原书章原文位置
框架没内置所有模型,可自定义将 LLM 接入 LangChaintext/05-p81-100.txt:477(搜「并没有内置所有」)
书为文心、智谱各写了一个将 LLM 接入 LangChaintext/05-p81-100.txt:687(搜「如何封装 Wenxin_LLM」) · text/05-p81-100.txt:850(搜「zhipuai_llm import ZhipuAILLM」)
自定义对话模型要补的两样天机text/10-p181-188.txt:68(搜「langchain.llms.base import LLM」) · text/10-p181-188.txt:94(搜「def _call」) · text/10-p181-188.txt:110(搜「_llm_type」)
提示词模板是什么将 LLM 接入 LangChaintext/05-p81-100.txt:555(搜「PromptTemplates」)
这门课那段模板构建检索问答链text/06-p101-120.txt:140(搜「不要试图编」) · text/06-p101-120.txt:147(搜「QA_CHAIN_PROMPT = PromptTemplate」)
建链四个参数构建检索问答链text/06-p101-120.txt:154(搜「RetrievalQA.from_chain_type」) · text/06-p101-120.txt:163(搜「指定 chain type」) · text/06-p101-120.txt:168(搜「返回源文档的引」)
取 4 块这个默认值个人知识库助手项目text/09-p161-180.txt:157(搜「search_kwargs={'k': top_k}」)
那次失败的两句回答构建检索问答链text/06-p101-120.txt:174(搜「4.1 基于召回结果和 query 结合起来构建的 prompt 效果」) · text/06-p101-120.txt:187(搜「抱歉」) · text/06-p101-120.txt:194(搜「我不知道王阳明是谁」)
不接知识库的对照构建检索问答链text/06-p101-120.txt:204(搜「llm.predict」) · text/06-p101-120.txt:211(搜「南瓜书是指」) · text/06-p101-120.txt:220(搜「王阳明(1472年-1529年)」) · text/06-p101-120.txt:225(搜「加上我们的本地知识」)
弃用警告构建检索问答链text/06-p101-120.txt:181(搜「LangChainDeprecationWarning」)
竖线写法将 LLM 接入 LangChaintext/05-p81-100.txt:646(搜「chat_prompt

Footnotes

  1. 出处:「将 LLM 接入 LangChain」第 476 至 477 段(text/05-p81-100.txt:477,搜「并没有内置所有」)。注意这句话和第 06 章第 3 节那句几乎一模一样,只是那里说的是向量模型、这里说的是对话模型——原书在两处各写了一遍。

  2. 出处:「将 LLM 接入 LangChain」第 684 至 690 段(text/05-p81-100.txt:687,搜「如何封装 Wenxin_LLM」)与第 841 至 850 段(text/05-p81-100.txt:850,搜「zhipuai_llm import ZhipuAILLM」)。文心那一节的原话是:因为新版 LangChain 可以直接调用文心千帆 API,我们更推荐使用下一部分的代码;智谱那一节的理由不同——框架里提供的那个已经不可用了,所以必须自定义。 2

  3. 出处:「天机」第 68 至 111 段(text/10-p181-188.txt:68,搜「langchain.llms.base import LLM」;两样东西见 text/10-p181-188.txt:94,搜「def _call」与 text/10-p181-188.txt:110,搜「_llm_type」)。这段代码在原书里出现在第三部分那个项目里,不在这一章——原书这一章把自定义对话模型的细节放进了一个单独的附录,而那份附录的正文没有进入我们手上这一版。所以本章拿项目里那份同构的实现来说明形状。「没有批量可优化」这句判断是我们的,不是书里的。

  4. 出处:「将 LLM 接入 LangChain」第 554 至 557 段(text/05-p81-100.txt:555,搜「PromptTemplates」)。原文的说法是:通常不会直接把用户输入传给模型,而是把它添加到一个较大的文本中,这个文本提供有关当前特定任务的附加背景。

  5. 出处:「构建检索问答链」第 138 至 148 段(text/06-p101-120.txt:140,搜「不要试图编」;声明两个空的那一行见 text/06-p101-120.txt:147,搜「QA_CHAIN_PROMPT = PromptTemplate」)。原文的模板文本里「不要试图编造答案」这句被排版拆到了两行,代码块里照录。

  6. 出处:「构建检索问答链」第 152 至 168 段(text/06-p101-120.txt:154,搜「RetrievalQA.from_chain_type」;第五个参数见 text/06-p101-120.txt:163,搜「指定 chain type」;第三个参数见 text/06-p101-120.txt:168,搜「返回源文档的引」)。原文还提到另一个方法 RetrievalQAWithSourceChain(),同样能返回来源。 2 3

  7. 出处:「个人知识库助手项目」第 156 至 158 段(text/09-p161-180.txt:157,搜「search_kwargs={'k': top_k}」)。那段代码的注释写着「默认 similarity,k=4」。这一章的正文里没有出现过这个 4——本组拆解从项目那一章把它借过来,因为不知道取几块,读者就无法判断第 5 节那次失败该归咎于什么。 2

  8. 出处:「构建检索问答链」第 171 至 196 段(text/06-p101-120.txt:174,搜「4.1 基于召回结果和 query 结合起来构建的 prompt 效果」;两句回答见 text/06-p101-120.txt:187,搜「抱歉」与 text/06-p101-120.txt:194,搜「我不知道王阳明是谁」)。图说里对两种「答不上来」的区分是我们的分析,书没有做这个区分。

  9. 出处:「构建检索问答链」第 198 至 226 段(text/06-p101-120.txt:204,搜「llm.predict」;两句回答见 text/06-p101-120.txt:211,搜「南瓜书是指」与 text/06-p101-120.txt:220,搜「王阳明(1472年-1529年)」;书自己的结论见 text/06-p101-120.txt:225,搜「加上我们的本地知识」)。那张四格表是我们排的,书只是把四段输出依次印了出来。 2

  10. 出处:「构建检索问答链」第 180 至 184 段(text/06-p101-120.txt:181,搜「LangChainDeprecationWarning」)。原文那条警告的英文原话是:The function __call__ was deprecated in LangChain 0.1.0 and will be removed in 0.2.0. Use invoke instead.

  11. 出处:「将 LLM 接入 LangChain」第 640 至 677 段(text/05-p81-100.txt:646,搜「chat_prompt | llm | output_parser」;那个比方见 text/05-p81-100.txt:676,搜「Unix 管道运算符」)。原文给这种写法起的名字是 LCEL,并列了它的四条好处:支持异步与流式、有兜底方案、能并行、内置日志记录。