Skip to main content

09 · 知识库问答:RAG 完整链路(老板要的东西终于来了)

开头的现象

小林把零件都备齐了:能回答的模型、能切块的刀、能按意思检索的向量库。他以为自己马上能交货,结果老板发来一条消息:“小林,手册我更新了,以后每年更新一次。你要让机器人永远答最新版,不能我改完了它还念旧版本。” 小林心想:“这怎么可能?模型是训练出来的,我又不能重新训练它。” 他抱着这个想法,去查了一个他早有耳闻但一直没搞懂的词——RAG。

第一幕:小林的”死记硬背”幻想破灭了

小林一开始的设想是”把手册背给模型听”——他试了把手册全文塞进 prompt,模型答得驴唇不对马嘴(第 7 篇已经栽过)。他又想”微调模型”——查了查价格和工期,直接放弃。 他沮丧地坐在工位上,突然看到了一个词:RAG(检索增强生成,Retrieval-Augmented Generation)。文档里写了一句让他豁然开朗的话:
RAG 不改变模型,改变模型”看到什么”。每次用户提问,先从你的文档里检索最相关的段落,把段落塞进 prompt 让模型基于它回答。
小林一拍大腿:“我不是要让模型记住手册,我是要让模型在回答的时候’翻手册’!” 他脑海里浮现出一个画面:模型是个新来的实习生,不懂公司规矩,但每次被问到”年假怎么算”,你就把员工手册翻到对应那页递给他看——他照着念,永远是对的,手册更新了他就念新版。 这就是 RAG 的灵魂:检索(翻到哪页)+ 生成(照着念)。
想验证”翻手册”vs”背手册”的区别吗?跑 09_rag.py 问它”工资什么时候发”——它的回答里带着手册原文的影子,而且手册里没有的信息它会说”手册里没有相关信息”(不编造)。这就是”照着念”的表现。

第二幕:小林画出了 RAG 的”流水线”——四条链串起来

小林把 RAG 拆成四步,画了一张图贴在显示器上:
他用 LangChain 的链式语法(|)把这四步串成了一条”流水线”:
小林盯着这条链,心里第一次有了”掌控感”:| 就像流水线上的传送带,左边输出接到右边输入。{"context": ..., "question": ...} 是并行两条管道——一个去检索资料,一个原样递问题,最后汇合填进模板。 他问了一个手册里没有的问题:“公司食堂中午有什么菜?“——模型回答:“手册里没有相关信息。” 小林满意地点头:“它没编!它知道资料里没有就不瞎说!”
想验证这条流水线吗?跑 09_rag.py,四个问题轮流问:有的命中手册(年假、工资、迟到),有的是手册没有的(食堂菜单)——模型会诚实地说”手册里没有相关信息”。这就是 RAG 的”不说谎”。

第三幕:小林发现了检索的”最后一公里”问题——粗排 vs 精排

小林用了一天,发现了一个让他皱眉的现象:检索器返回的 top-3,有时候不是最相关的 3 块。 他做了个实验:问”年假怎么算?“,让检索器返回 top-8,他一个个看过去:
问题来了:最相关的 [1] 排在第二位。如果只取 top-1,模型看到的会是”年假怎么休”而不是”年假怎么算”——检索对,答案可能还是错。 小林查了下自己用的检索方式,才知道它叫**“粗排”(或叫”召回”):用向量相似度(cosine 距离)快速筛出候选,图的是”快”和”全”——宁可多捞一些,别漏掉相关的。但向量相似 ≠ 语义相关**:两个句子在向量空间离得近,不代表它们真的在讲同一件事。 他想起一个词——重排序(Rerank)。老王告诉他:粗排负责”捞”,精排负责”挑”。 生产环境的 RAG 通常是两段式:
为什么不能只靠粗排? 因为向量检索的相似度是”整体语义近似”,容易把”都提到年假”但”其实在讲别的”的块排前面。重排序模型(reranker)是交叉编码器(CrossEncoder)——它把”问题 + 每一块资料”拼在一起,让模型同时看两边,逐块打分,分数就是”这块对这个问题有多相关”。它比向量检索贵,但对”相关度”的判断准得多。 小林掏出代码验证——他用的是本地中文重排序模型 BAAI/bge-reranker-base(和 embedding 的 bge 是同一个家族),配合 sentence_transformersCrossEncoder
他打印出精排后的结果,和粗排对比:
小林的顿悟:粗排的 [2] 工资(0.08 分)被精排挤出了 top-3,换进来的是 [3] 加班(虽然也不完美,但分数逻辑对了——它把”高相关”的提到前面、“低相关”的压到后面)。粗排负责”别漏”,精排负责”别错”。 这个 0.83 vs 0.08 的分数差,就是 reranker 的价值——它能分清”谁真的在回答这个问题”。
想验证吗?下载 BAAI/bge-reranker-base 模型(约 1.1GB,放 models/bge-reranker-base),跑 09_rag_rerank.py——你会看到同一批 top-8 粗排结果,重排后顺序变了,且每块带一个 relevance 分数。想验证”向量检索会犯错”吗? 在粗排结果里故意找一块”提到年假但实际讲别的”的资料,看它精排后被压到多后面。

第四幕:小林解剖了链的”中间产物”——模型到底看到了什么

小林的好奇心又来了:这条链最后 StrOutputParser() 剥出纯文本,那剥壳之前模型返回的到底是什么? 他拆了一截”只到模型为止”的链:
他打印了 mid 的完整结构,发现:模型在 RAG 里返回的还是一个 AIMessage——content 是最终回答,response_metadata.stop_reasonend_turn(直接回答完毕,没调工具),usage_metadata 记着 token。 他恍然大悟:StrOutputParser 只做一件事——把 AIMessage.content 剥出来变成字符串。 整条链的”最后一环”,就是个拆包装的。
想验证”剥壳前长什么样”吗?把 09_rag.py 里链尾的 StrOutputParser() 去掉,直接 invoke——你拿到的不是一个字符串,而是一个完整的 AIMessage(content 是回答,response_metadata 里躺着 stop_reason 和 usage)。加上 StrOutputParser(),就只剩文本了。

🔧 技术细节:这一章涉及的关键类和签名

BaseRetriever——检索器接口(langchain_core.retrievers)
② 链式语法(LCEL)——| 背后的类型
RunnablePassthrough——透传(RAG 里”原样递问题”)
④ 四个环节的类型全览:
想验证吗?rag_chain.invoke("年假怎么算?") 返回字符串;去掉 StrOutputParser() 直接 invoke,返回 AIMessage——看两者的 type 区别。
⑤ 重排序(第三幕)——CrossEncoder 与两段式检索
关键认知:粗排(向量检索)快但”相似 ≠ 相关”;精排(CrossEncoder)把问题和每块资料拼在一起看,逐块打分,准但贵。所以两段式:粗排负责”别漏”,精排负责”别错”。重排只能重新排序已捞到的块,捞漏了的它救不回来。

第五幕:边界——RAG 不是银弹

小林用了一天,摸清了 RAG 的边界: 它能干的:让模型基于”你的文档”回答问题、自动跟进文档更新(换库内容就行,不用重新训练)、控制模型”不乱编”(资料里没有就说不知道)、给回答标出处(块里的 metadata)。 它不能干的
  • 检索错了,答案就错——RAG 的上限取决于检索(第 8 篇那些边界:否定、数字、专有名词)。检索返回了不相关的块,模型再聪明也答不对。重排序(第三幕)能缓解但治不了本:它只能把”已捞到的块”重新排序,捞漏了的(粗排没召回)它也无能为力。
  • 资料本身有错,模型照样念——RAG 不校验资料对错,只保证”照着念”。
  • 不适合”开放式创意问题”——问”帮我想个宣传语”,RAG 的”翻手册”反而碍事。
  • 多轮对话没接上——这个版本的 RAG 是无状态的,每次提问独立检索。要让它”记得上一轮”,得接上第 4 篇的历史机制(或直接上第 08、09 篇的 Agent / LangGraph)。
想验证边界吗?问 09_rag.py 一个手册里根本没有答案的问题,比如”食堂菜单”——它老实说”手册里没有”。但如果你在手册里写一句错误信息(比如故意把年假天数写错),RAG 会照错念——它不校验内容对错。

结论(小林用一天换来的)

RAG = 检索 + 生成:每次提问先”翻手册”(从向量库找最相关的 3 块),再把资料 + 问题填进模板让模型”照着念”。 LangChain 用 | 把 检索器 → 格式化 → 模板 → 模型 → 解析器 串成一条链,StrOutputParser 负责剥壳取文本。检索分两段:粗排(向量相似度,快、捞 top-N)→ 重排序(CrossEncoder 精排,准、挑最相关)——粗排负责”别漏”,精排负责”别错”。边界:RAG 的上限=检索的上限,检索错就答错(重排序只能缓解不能根治);它不校验资料对错,也不自带记忆。 但这一刻,小林终于把老板要的”基于文档回答问题”跑通了——虽然它还只是个”无状态问答机”。

复现信号:什么时候你会想起这一章

  1. 要让模型回答”你的文档里的问题”——别微调,用 RAG:切块 → 向量化 → 粗排检索 → 重排序 → 生成。
  2. 模型总在编造”手册里没有的信息”——RAG 的 system 提示”没有就说不知道”能压住它。
  3. 文档经常更新——RAG 只要换库内容,模型不用动。
  4. 回答要”有理有据”——检索块的 metadata 存好来源,回答就能标出处。
  5. 检索返回 top-3 但答案还是不对——可能是粗排把”提到关键词但不相关”的块排前面了:加大粗排 k(比如 top-20),再加重排序精排到 top-3。
小林交差了吗?还没有。老板试用了一下说:“我问它’去年这时候问过你什么’,它一脸懵——它不记得我跟它聊过天!” 小林叹了口气,他知道下一步了:给这个问答机加上记忆、工具、还有自主决策——他要让”问答机”进化成”助手”,翻到第 10 篇:会记住、会干活的助手