跳到主要内容

01-outline — rag-with-python-cookbook 节级大纲(第二版,已按审稿意见改)

这是第二段的产物:一页纸的结构。不写正文。 上一棒的通读笔记在同目录 reading-notes.md(92k 字符)。 第二版改了什么: 审稿的十四条硬伤逐条落实,改动记在文末「改了什么」。

这本书是什么(一句话): 不是讲原理的书,是 121 个可运行 Python 配方的册子 —— 约七成篇幅是代码和程序输出,散文极薄且多为对下一段代码的复述。 所以拆解不能照散文重写(照写必成目录),要从代码、参数选型和真实输出里把机制读出来。

一条贯穿全书的写作纪律(第二版新增,压过一切便利): 这本书的每个配方都自带一份语料,配方与配方之间几乎从不共用输入。 所以不许把两个配方的输出接成「一次连续运行」。凡是我们把多个配方串成一条走查, 每一步必须标出它出自哪个配方,接缝处照实写「书在这里换了输入/换了切分器/换了模型」。

切分决定:12 章。 依据是新词密度,不是字数;顺序重排了原书章序,理由在文末「为什么这么切」。


全书一条主线(总纲第 3 节的骨架)

判据:只读这一节、不看任何拆解章,也能把这本书的主张复述出来。 海拔口径:只留效果句。 机制细节(向量怎么算出来、池化、距离公式)一律退回章节层, 在主线里只记一句「为什么会这样,第 N 章讲」,并进兑现表。

① 大模型只会照自己参数里的东西作答 —— 事实会过期,你们公司的文档它一个字没见过。
重训一遍不现实。
↓ 可它凭什么会理睬我贴给它的东西?
② 它每次都是照着眼前这段文字往下续写 —— 所以提示里出现的内容,
会和它参数里的知识一样被用上。 【第 01 章】
↓ 那就别改模型,改它眼前看到的东西
③ 提问的时候先把相关文档找出来,原样塞进提示 —— 这就是 RAG。
模型一个参数都没动,答案却变了。 【第 01 章】
↓ 可是「文档」有十几种格式,总不能为每种写一遍下游代码
④ 加载器把 PDF/CSV/Excel/HTML/JSON 统统收敛成同一个东西:
Document = 正文 + 元数据。后面所有步骤只认这一种。 【第 02 章】
↓ 可是整篇文档塞不进提示,整篇算一个向量也没有指向
⑤ 必须切块。切法分两类:靠看得见的标记切,靠意思切。
**而切分是全书埋得最深的雷** —— 它决定了后面几章会不会疼。 【第 03 章】
↓ 切完的块,凭什么能「按意思」被找到
⑥ 把每块变成一串数,于是「像不像」变成「远不远」。
(这串数怎么来的、凭什么意思近的就数也近,第 04 章讲。) 【第 04 章】
↓ 几十万串数放哪儿、怎么比、什么时候该说「我没找到」
⑦ 向量库负责存和取前 k 条。**而取前 k 条永远凑够 k 条,哪怕全是垃圾** ——
解药是相关性阈值,书里有药却从没和病放在一起说。 【第 05 章】
↓ 取回来的前 k 条经常不对,怎么让它更准
⑧ 两条路一起用:按意思找 + 按词面找,融合;
再拿一个更贵的模型把前几条重排一遍。**这是全书最扎实的一段。** 【第 06 章】
↓ 准是准了,库一大就慢
⑨ 拿召回换速度:近似索引、分区、缓存、降维、冷热分层 ——
每一样都在花「可能找不到」买「快」。 【第 07 章】
↓ 「找对了」和「答对了」是两件事
⑩ 检索结果怎么变成一句答案:原样塞进提示、上下文怎么排、
要 JSON、要引用 —— 而提示只能鼓励,保证不了。 【第 08 章】
↓ 它给了一句答案,凭什么信它
⑪ 置信度、用模型核验、摘要长度 —— 书里三个「置信度」只有一个是真的,
而写死摘要长度会当场逼出幻觉(书自带对照组)。 【第 09 章】
↓ 这些步骤每次都手写一遍太蠢,要能拼装、能复用
⑫ 把步骤串成链;多轮对话的难点不在存历史,在**把追问改写成能独立
成立的问题**再去检索。 【第 10 章】
↓ 链是写死的顺序:第一次没找到,它照样往下走
⑬ 让模型自己决定要不要再查一次 —— 代理式 RAG:评估证据够不够 →
不够就改写查询重检索 → 直到满足停止条件。**这是书自称的落点,
而书没落住:十个「代理」全是单趟直线。** 【第 11 章】
↓ 从头到尾,凭什么说这套东西是好的
⑭ 评测。**这是全书最大的洞:** 唯一一次量化是 Accuracy@3 = 0.50,
判据是关键词子串;调参 27 组全拿 1.00,因为只比对期望答案的第一个词。【第 12 章】

主线上必须如实说的三件事(写总纲时不许省):

  1. 落点没落住。 第 11 章十个「代理」没有一个有循环、自我评估或停止条件; 而全书对代理式 RAG 最完整的定义,反而藏在第 7 章结语里 (text/14-fm-introduction.txt:1974,搜「Agentic RAG uses the LLM to control retrieval」)。
  2. 真正的重心是原书第 6 章和第 9 章(我们的第 06、07 章)—— 全书唯一有真机制、真数字、真对照的部分。
  3. 生成模型逐章缩水,而书从不交代: 第 1–6 章 Ollama llama3.2:3b(约 30 亿参数)→ 第 7–10 章 flan-t5-base(约 2.5 亿)→ 第 11 章 t5-small(约 6000 万,且非指令微调)。 后面几章几乎所有「输出很怪」都由此而来。 这件事在第 01 章末尾一次讲清,后面各章只回指。

01 一句话进去,一句话出来(原书第 1 章)

主走查: 同一段 RAG 正文 —— 书把它做成了三个文件(RAG.pdf / RAG.txt / RAG.docx), 在第 1 章里反复用。书从没在同一份输入上把六站跑完一遍,所以这条走查是我们接的, 每一步标出配方号,接缝处照实说。

出自具体的数
加载配方 1三种格式 → Total documents loaded: 3,每个 Document 的前 275 字符一模一样
切块配方 2(RAG.docx + 递归字符切分 300/50)Total chunks created: 4,书只印了 3 块(276 / 282 / 166 字符)
↑接缝配方 4/9/11 换成 RAG.txt + 单分隔符切分同样 300/50,切出 3 块 —— 换了文件名和切分器,重跑了一次
嵌入配方 6(换了输入:三句手写的话,不是上面切出来的块)3 条 → 每条 384 维,第一条前 5 维 [-0.0874, -0.0331, -0.0124, 0.0260, -0.0541]
入库 + 检索配方 11(RAG.txt 的 3 块)问「What is RAG?」→ 0.9408 / 1.3505 / 2.0506
生成配方 14(又换了输入:RAG.pdf,问题换成 "What is the document about?")答案却是「The document appears to be about LangChain…」

最后一步是全章最值钱的一处: RAG.pdf 讲的是 RAG(配方 1 印出来的正文可证), 答案却在讲 LangChain —— 因为配方 14 用的落盘目录 chroma_vector_store 正是配方 8 建的那个, 里面还躺着配方 8 那四条讲 LangChain 的样例句。六站全走通了,答案还是错的。 (为什么会这样、怎么防,第 05 章讲。)

承重词(单开一节,从现象讲起): ① RAG(检索增强生成) ② Document(正文 + 元数据)

  • §1 进来时以为模型答错是「不够聪明、换个大的就好」→ 出去时知道它只会照自己参数里的东西作答,事实过期和领域空白是结构性的,再大的模型也没见过你们公司的文档
  • §2 【第二版新增,补的是主线 ②→③ 那一级台阶】 进来时以为「把文档贴进提示」是一种魔法或一种插件 → 出去时知道机制很朴素:它每次都是照着眼前这段文字往下续写,所以提示里出现的内容会和参数里的知识一样被用上 —— 证据是配方 82 的输出把上下文一字不差抄了回来;并照实说书从没做过「不给上下文」的对照组
  • §3 进来时以为 RAG 是一种模型、一个产品或一个框架 → 出去时知道它是两个动作:先把相关文档找出来,再把找到的原文塞进提示;模型一个参数都没改
  • §4 进来时以为 RAG 是一次函数调用 → 出去时知道它是六站流水线(加载 → 切块 → 嵌入 → 入库 → 检索 → 生成),前五站离线做一次、第六站每次提问都跑,而这本书的十一章就是这六站的展开
  • §5 进来时以为流水线里流的是字符串 → 出去时知道流的是 Document:page_content(正文)+ metadata(键值对),元数据必须在第一站就挂上,后面所有的过滤和「这句话哪来的」全靠它
  • §6 进来时只有六个站名 → 出去时能把上面那张表完整走一遍,并且知道每一处接缝是书换了输入;那三个距离数在这里只说一句「越小越近」(是什么量、还有哪两种,第 04 章讲)
  • §7 进来时以为跑 RAG 得先买云端 API → 出去时知道这本书全程本地零 API,并且生成模型逐章缩水到 2.5 亿、再到 6000 万参数而书从不交代 —— 这是读懂后面所有怪输出的钥匙

02 把十几种格式收成一种东西(原书第 2 章)

主走查: 配方 16 的员工 CSV —— 5 列(Name / Department / Role / Location / Joining Date)、 4 行,第一行是 Anil Sharma。CSVLoader4 个 Document,正文渲染成 「Name: Anil Sharma\nDepartment: Engineering\n…」。 同一批数据换 Excel 走一遍(配方 17,3 行 4 列)→ UnstructuredExcelLoader整张表压成 1 个 Document、一行无分隔的字串「ID Name Department Age 1 Jatin Kumar HR 34 2 …」。 换个加载器,粒度从「一行一个」变成「一表一个」,而下游一行代码都没改 —— 这就是加载器的全部意义,也是它最大的坑。

另起一处(失效路径): 配方 22 的洗稿输出 —— 那句 RAG 定义被小写化 + 去停用词洗成 「retrieval augmented generation (rag) architecture combines ability large language models (llms) retrieval system…」, is / an / that / the / of / with a / against / by 全被删,句子已经不成句。

承重词: ① 加载器(loader)与它的合同 ② 停用词(stop words)—— 因为书把有害操作当成了最佳实践

  • §1 进来时以为「把文件读进来」是最没技术含量的一步 → 出去时看见同一批数据在 CSV / Excel / JSON / 网页里各长什么样(这一节只摆现象,不给结论)
  • §2 进来时以为加载器就是「把文字抠出来」→ 出去时知道它的合同是「读文件 → 解析 → 返回 List[Document]」,换格式只换加载器一行、下游全部不动 —— 这就是加载器存在的全部理由
  • §3 【第二版合并原 §3 + §6】 进来时以为一个文件出一个文档 → 出去时知道粒度由加载器定(CSV 一行、Excel 一表、JSON 默认写法整个数组一个),而粒度太粗会丢掉行列关系和记录边界,问「Rajeev 多少岁」在 Excel 那条路上几乎必然答错
  • §4 进来时以为元数据是可有可无的附加信息 → 出去时知道加载这一步是唯一能挂上「谁说的、哪来的、什么时候的」的时机,漏挂了后面补不回来
  • §5 进来时以为「小写化 + 去停用词」是文本处理的标准动作(书就是这么教的)→ 出去时知道那是词计数时代的习惯、对句向量模型有害,证据是配方 22 自己印出来的那句洗坏了的正文,而书里没有一句警告
  • §6 【第二版新增,收配方 24/25】 进来时以为「整个目录灌进去」就是多写个 for 循环 → 出去时知道要处理三件事:哪些扩展名认、单个文件失败了怎么办(配方 24 的 Loaded 3 documents. 背后有个吞异常的 except)、以及为什么要先按标记数打包再切(配方 25:3 个小文件 → 1 个逻辑包 → 6 个块)
  • §7 【第二版新增,收配方 26】 进来时以为格式不在清单里就没辙 → 出去时知道这份合同你自己也能实现:继承 BaseLoader、实现 load() 返回 List[Document] 就行,元数据里还能塞列表(tags: ['custom','demo','loader'])

03 切成多大、在哪儿切(原书第 3 章 + 第 6 章配方 63/70 + 第 9 章配方 97)

主走查: 配方 63 —— 同一份 RAG.txt(就是第 01 章那段正文), chunk_size200 / 500 / 1000 字符,同一个查询「What is RAG?」: 切出 6 / 3 / 1 块;200 那次把定义句拦腰切断,第 2 名的正文从「the factual accuracy, contextual relevance…」开始; 1000 那次整篇只剩 1 块,top-3 只回得出 1 条。这一段书里数字自洽、语料不换,是全书最干净的一条走查。

另起两处(失效路径,红线二允许的上限): ① 配方 27 的 n-gram 切法:42 个词、每块 8 词、滑动 5 词 → 7 块,句尾「to a RAG system.」整个被扔掉; ② 配方 28 的朴素语义切法:9 句进去,三块里只出现 4 句,相似度没过 0.5 的 5 句直接消失。

承重词: ① 标记(token) ② 切块(chunking)与它的粒度

  • §1 进来时以为整篇文档直接嵌入最省事 → 出去时知道三条硬约束把这条路堵死了:模型有输入上限、整篇算一个向量等于把所有主题平均掉、上下文越长越贵
  • §2 进来时以为「长度」就是字符数 → 出去时知道模型数的单位是标记(词被切成的小块),300 字符和 300 标记不是一回事,而书里的 chunk_size 两种单位混着用、从不说明
  • §3 进来时以为 RecursiveCharacterTextSplitter 只是「按 300 字符硬切」→ 出去时知道「递归」指的是退让顺序:先试段落 \n\n,块还太大就退到换行,再退到句号,最后才逐字符 —— 书用了它四次,一次没解释
  • §4 进来时以为块大小是随手填的数 → 出去时看到 200/500/1000 → 6/3/1,并知道书给的经验值是 300–800 标记,而同库另一本 RAG 书给的是 1000–2000,差一倍多
  • §5 进来时以为重叠是浪费存储 → 出去时知道它防的是「一句话被切进两块,两块都答不了」,以及重叠也有兜不住的时候:配方 97 重叠 5 个词,块 1 结尾停在「… BM25 is」,另一块从半句「for semantic similarity.」开始
  • §6 进来时以为切分只有「按长度切」一种 → 出去时知道两大类:结构式(靠标题、正则、页码、说话人这些看得见的标记)和语义式(靠意思),以及书里十四个配方只有一个是语义式,和它自己开篇的二分承诺严重不对称
  • §7 进来时以为切分再差也不至于丢东西 → 出去时知道有两个配方当场丢了(见上面「另起两处」),书一个字没提
  • §8 【第二版改:许诺必须指到具体一处】 进来时以为切坏了顶多是「块不太好看」→ 出去时知道它会在后面结账,而且账单是具体的:第 10 章配方 110 里,唯一真正回答问题的那句「It may help with weight loss by reducing calorie intake.」因为 It 的指代对象不在块里,被交叉编码器打到 −6.5435、五条垫底(17-fm-introduction.txt:1716)。病根一句话:块里留下了失去指代对象的代词,这块就永远检索不准。 这里要给它起名叫块内指代断裂,与第 10 章的「查询侧指代」分名

04 一句话怎么变成一串数(原书第 4 章 + 第 9 章配方 91)

主走查: 这一章书里没有一条连贯的线,四个配方各用各的语料 —— 我们照实把它们接起来,每步标配方号。

出自具体的数
三句话进去配方 42"RAG is a powerful technique…" 等三句
切成标记 → 每标记一个向量 → 按掩码求平均配方 42(手写,不经 LangChain)3 个向量,第一句前 5 维 tensor([-0.2326, 0.1575, 0.2014, -0.0072, -0.3706])
同一个模型走完整流水线(多一步归一化)配方 41(换了输入:一句话)384 维,前 10 维 [-0.0455, -0.0481, -0.0086, 0.0733, …] —— 分量比上一行小一个数量级
归一化之后长度是多少配方 50(又换了输入:三条噪声句)三条全是 norm=1.00
有了向量就能比配方 43(又换了输入:四句话)裸 FAISS IndexFlatL2 → 距离 1.0279 / 1.3194

另起一处: 配方 51 —— 7 个 80 字符的小块两两算余弦,阈值 0.7,一条边都没连出来, 而书照样把它当成功案例收尾。

承重词: ① 嵌入(embedding) ② mean pooling(按掩码求平均)

  • §1 进来时以为搜索就是找关键词 → 出去时知道字面匹配有个结构性缺陷:同一件事换个说法就搜不到
  • §2 进来时以为「把词变成数」是给每个词编个号 → 出去时知道给的是一串数,并且让意思相近的东西这串数也相近,于是「像不像」变成了「远不远」
  • §3 【第二版新增,补的是「这个性质从哪来」那一级台阶】 进来时把「意思近 ⇒ 数也近」当成给定的魔法 → 出去时知道它是训练目标逼出来的:拿大量「该近的一对」去拉近、把同一批里其余的推远;并落到边界上 —— 嵌入模型只在它训练过的那类文本上守信(来源:我们的 frontier 书架)
  • §4 进来时以为一句话直接就有一个向量 → 出去时知道中间还有两步:句子先切成标记、每个标记各出一个向量,再只对真内容求平均才压成句向量 —— 书把这段代码原样摆出来了,却把小标题写成「Tokenize the documents」,一个字不解释
  • §5 进来时以为 384 这个维度是可以调的参数 → 出去时知道它是模型定死的(all-MiniLM-L6-v2 是 384、all-mpnet-base-v2 是 768),并且入库和查询必须用同一个模型,而书里有两处正好犯了这个错、从没警告过
  • §6 【第二版:三种量的承重讲解落在这里,01 和 05 都只回指】 进来时以为相似度就是一个数 → 出去时知道有三种量:L2 距离(越小越近)、内积、余弦相似度,它们分别在量什么、方向朝哪、彼此什么关系;书里 FAISS 默认返回 L2,而正文从没交代过方向
  • §7 进来时以为向量归一化是「抗噪声、抗错字」(书就是这么写的)→ 出去时知道它真正的用途是把长度统一成 1,此后内积就等于余弦、L2 的排序也和余弦一致 —— 而第 9 章配方 91 才把它用对
  • §8 进来时以为块切得越细越精确 → 出去时知道切得越碎,块与块之间的相似度就越低(配方 51 的零条边)

05 向量放哪儿、怎么比、什么时候该说「没找到」(原书第 5 章 + 第 1 章配方 13 + 第 9 章配方 95/99)

主走查: 配方 53 —— 这是书里唯一一条从建库走到重载再走到查询的连贯线,而且是两个脚本。 三条文档(LangChain / FAISS / Transformers 各一句)→ 建 FAISS 库 → save_local("chapter5_faiss_store") → 打印 Vector Store saved to: chapter5_faiss_store换一个脚本load_local(..., allow_dangerous_deserialization=True) → 问「What is LangChain?」、k=2 → 第 1 条答对,第 2 条是「Transformers from Hugging Face are widely used in NLP.」—— 和 LangChain 没有一点关系,可它照样被交了出来。

另起两处: ① 阈值这一味药(配方 95,换了语料照实说):八条文档对同一个查询的余弦分数 0.6190 / -0.0185 / 0.0139 / -0.0189 / -0.0296 / -0.0428 / -0.0015 / 0.0383, 0.5 一刀切下去只剩 1 条;而如果按 top-3 取,第 2 名会是「出国旅行」那条(0.0383); ② 元数据过滤的两种顺序:配方 13(先检索后过滤,k=5 只回 2 条)对 配方 99(先过滤后检索,子集里凑数,第 2 名 −0.0428)。

承重词: ① 向量库与索引 ② top-k(为什么它永远凑够 k 条) ③ allow_dangerous_deserialization

  • §1 进来时以为存向量拿个列表就行 → 出去时知道向量库管三件事:存下来、建索引、按相似度取前 k;FAISS 是纯索引库,Chroma 多管一件持久化
  • §2 【第二版:原 §2「怎么读那三个距离数」删掉,落点已在 04§6;这里换成「取前 k 条为什么要做成库的原语」】 进来时以为「取最像的几条」自己写个循环也行 → 出去时知道它必须做在库里:分数不出库、只出名次和条数,而这个接口的形状本身就决定了它永远凑够 k 条
  • §3 进来时以为索引建好就在那儿了 → 出去时知道落盘和重载各是哪一步(主走查那两个脚本),以及重复运行会把同一批数据反复灌进去 —— 第 01 章那个「答案在讲 LangChain」的账单,在这里结清
  • §4 进来时以为 allow_dangerous_deserialization=True 是个照抄就行的样板参数 → 出去时知道它的意思是「我知道加载这个索引文件会执行任意代码,我信任它」,拿别人给的索引照抄会中招(书用了四次,零解释)
  • §5 进来时以为返回来的都是相关的 → 出去时知道 top-k 永远凑够 k 条,证据就在主走查的第 2 条,以及配方 95 那八个分数里有五个是负的
  • §6 进来时以为这病没法治 → 出去时知道解药就在同一本书的另一章:设一条相关性下限,低于 0.5 一律丢掉,八条候选只剩一条过线 —— 而书从没把病和药放在一起说过;并当场和第 09 章的置信度划清界限(一个丢候选,一个拦答案)
  • §7 进来时以为元数据过滤就是多传一个 filter 参数 → 出去时知道有两种相反的顺序:先检索后过滤会,先过滤后检索会凑数,书两种都用过、从不对比

06 找得准:两条路、一次融合、一次重排(原书第 9 章 + 第 6 章配方 65)

主走查: 同一个查询「How does RAG reduce hallucinations?」在这一章跑了六次。 语料只换过一次,接缝写清楚: 配方 91–94 是同一批六条短文档; 配方 95/96 换成七条(多了「烹饪食谱」和「出国旅行」两条完全无关的,并把第一条换成一句正面回答)。

出自具体的数
按意思找配方 91(六条)0.3682 / 0.0452 / −0.0133
按词面找配方 92(同六条)1.2374 / 0.0000 / 0.0000 ← 后两条一个查询词都没有
两路融合配方 93(同六条,min-max 归一 + alpha=0.5)1.0000 / 0.1570 / 0.0949
把查询写长配方 94(同六条,扩展词 document matching / query understanding / retrieval)0.5879 / 0.4707 / 0.3435 —— 分数普遍涨了,第 2、3 名还换了人
↑接缝配方 95/96 换成七条语料多了「烹饪食谱」「出国旅行」两条
粗排配方 96(七条,BM25 取前 5)「烹饪食谱」和「出国旅行」进了前 5
精排配方 96(交叉编码器)+8.4985−11.3435 / −11.3601 / −11.3980 / −11.4207

差了将近 20 分,而且四条无关文档挤在一起,界限干净得不像话。

承重词: ① BM25(它多做的那三件事) ② 交叉编码器(与双编码器的分工)

  • §1 进来时以为有了向量检索就不再需要关键词检索 → 出去时知道两条路各有死角:型号名、人名、罕见词这类只认字面的东西,按意思找反而找不准
  • §2 进来时以为关键词检索就是数词频 → 出去时知道 BM25 比数词频多做三件事:词频饱和、逆文档频率、按文档长度扣分 —— 这是全书唯一一次把一个算法的内部讲清楚
  • §3 进来时以为按意思找总比按词面找强 → 出去时看到配方 91 的第 3 名是负分、配方 92 的第 2、3 名并列 0.0000(分数并列就意味着名次是随机的),两条路各自都会交出垃圾
  • §4 进来时以为两路结果拼起来就行 → 出去时知道分数量纲不同不能直接加:书里做对的一次是两路各自 min-max 归一到 [0,1] 再按 alpha 加权(副作用是第一名恒等于 1.0000,那不代表「完全匹配」);而业界标准做法是只看名次不看分数的 RRF,书从未提及(补自我们的书架)
  • §5 进来时以为重排就是「再排一次序」→ 出去时知道两种编码器的分工:双编码器把查询和文档各自单独算成向量(可以预先把全库算好,所以快),交叉编码器把两者拼在一起喂进模型(算不了预先,所以只能用在前几条上,但准)
  • §6 进来时以为重排是锦上添花 → 出去时看到 +8.4985 对 −11.34 一线,并知道这些分数是无界的 logit、不是 0–1 概率(书没说);再回指第 6 章配方 65 的同一现象(9.0629 对 1.7071)
  • §7 进来时以为把查询写长一点总能多找到 → 出去时知道扩展后分数普遍升高不等于找得更准:扩展词把查询拉向泛化话题,每篇讲检索的文档都变得更像,分数涨了区分度反而降了
  • §8 进来时以为书里的「层级检索」是按粒度分层 → 出去时知道书用这个名字指的是「粗排 + 精排」,和它自己给的定义打架 —— 出门撞见这个词要按标准含义理解

07 找得快:每一样都在拿召回换速度(原书第 6 章)

主走查: 只用配方 62 那一个库(1000 条 × 64 维随机向量、5 条查询、k=5), 把近似索引、分区、缓存三样落在它上面。默认的 IndexFlatL2 逐条算 1000 次距离, 最近的五条距离 5.6875–7.9800;Is trained: True 这一行恰好暴露它不是近似索引 (Flat 根本不需要训练)。这个库上换索引之后的数字书里没有,凡是编的当场声明。 另起两处: ① 降维(配方 68:384 维想降到 128,只有 5 个块,实际得到 5 维); ② 冷热分层(配方 71:冷数据每查一次就全部重新嵌入一遍)。

承重词: ① 近似最近邻(ANN)—— 书讲错了,必须补 ② 召回(recall)

  • §1 进来时以为向量检索天然就快 → 出去时知道默认的 IndexFlatL2逐条算、一条不漏
  • §2 【第二版拆节:原 §2 一节六七张生面孔,超配额,按标准只许拆不许砍】 进来时以为 ANN 是「一种更快的索引」→ 出去时知道为什么可以不算全部:先把向量聚成一堆一堆、只搜最近的几堆(IVF);或者建一张从粗到细的图、跳着走(HNSW)
  • §3 【拆出来的下半节】 进来时以为省下来的只是「少比几条」→ 出去时知道还能连距离都不真算:把向量压成短码、用查表代替算距离(PQ)—— 并落到共同的账上:这三样都在拿召回换速度,而书挂着 ANN 的名字、演示的却是精确暴力搜索
  • §4 进来时以为按主题分区只是「整理一下」→ 出去时知道查询只进一个分区,答案落在别的分区就永远取不到;并一句话对照配方 66 的「分布式索引」(分片是搜全部再合并、召回不掉;分区是只搜一个、召回会掉,书把两者都叫扩展)
  • §5 进来时以为缓存能挡掉大部分重复查询 → 出去时知道按字串精确匹配的缓存,「what is ML?」不会命中「What is machine learning?」;业界的做法是把查询也嵌入、按相似度命中
  • §6 进来时以为维度想降到多少就降到多少 → 出去时知道成分数不能超过样本数:书里 384 维想降到 128,而只有 5 个块,实际得到 5 维
  • §7 进来时以为把老数据挪出索引总能省钱 → 出去时知道书里的冷热分层是反的:冷数据每查一次就全部重新嵌入一遍再手算余弦,比留在索引里贵得多
  • §8 【第二版新增,收配方 59】 进来时以为「异步建索引」就是全流程并行 → 出去时知道书只并行了读文件那一半,真正耗时的嵌入计算仍是一次性同步调用,而小节正文吹的是「多个嵌入可以并行」

08 找回来之后怎么答(原书第 7 章 + 第 8 章)

主走查: 配方 82 那一份小语料 + 查询「How does RAG reduce hallucinations?」。 提示明写「不要只是重复词句,要给出清晰的解释」,而输出正是逐字复述上下文。 每一步的具体输出都在书里(15-fm-introduction.txt:240:242)。 另起两处: ① 结构化输出(配方 88:漂亮的 JSON 出自 except 兜底分支); ② 带引用(配方 75:提示写了「cite them like [1], [2]」,输出里一个方括号都没有)。

承重词: ① stuff 链(把上下文原样塞进提示) ② 结构化输出(为什么提示保证不了)

  • §1 进来时以为「把上下文交给模型」没什么讲究 → 出去时知道最简单的那种做法有名字叫 stuff:检索到的文档原样塞进提示,文档短的时候够用,长了直接撑爆模型的输入上限
  • §2 【第二版改落点:不再讲「模型变小了」,那句归 01§7】 进来时以为提示写清楚模型就会照做 → 出去时知道提示是请求不是约束:同一个提示模型可以照做也可以不照做,而不照做的输出照样像个答案(配方 82 逐字复述就是现场);「因为这一章悄悄换成了 2.5 亿参数的小模型」只作一句回指
  • §3 【第二版改:只讲排序这一件事,并照实说这次答案是对的】 进来时以为检索回来的几段话顺序无所谓 → 出去时知道它们是按相似度排的、不是按原文顺序排的,于是「It reduces hallucinations…」排在了定义句前面;这一次两句都被检索到了,所以答案其实是对的 —— 但顺序一旦让代词跑到指代对象前面,读者和模型都看不出 It 指什么。全书对上下文该怎么排一字未提
  • §4 进来时以为提示里画个 JSON 模板模型就会吐合法 JSON → 出去时知道书自己给了全书最有价值的一条忠告(提示只能鼓励、保证不了语法合法),而书自己那个漂亮的 JSON 恰恰是 except 分支手工拼出来的
  • §5 进来时以为让模型标 [1][2] 就能追溯来源 → 出去时知道可靠的做法是把来源留在检索结果的元数据里、由代码贴回去 —— 引用是工程问题,不是提示问题
  • §6 进来时以为提示工程有很多招要学 → 出去时知道书里九种提示法里,「查询改写」其实是一次字符串替换、「思维链」的推理要点是 Python 事后贴上去的、「带引用作答」的答案是硬编码字符串,真正靠提示起作用的只有上下文接地那一种

09 怎么知道它在编(原书第 7 章配方 73/78 + 第 8 章配方 86/89/90 + 第 10 章配方 103)

主走查: 三个「置信度」的对照 —— 配方 73(答案长度 ÷ 300 → 0.79)、 配方 78(1 − L2 距离再裁到 [0,1] → 0.68)、配方 86(检索余弦取平均,0.391 与 0.169 → 0.280 判为 Low)。 三处语料不同,照实标。 另起两处:① 摘要长度(配方 89 写死 min_length=80 → 编出不存在的书; 配方 103 按输入长度动态算 → 忠实);② 证据高亮(配方 90 三条里两条是编的)。

承重词: ① 置信度(三个里只有一个是真的) ② 幻觉(在 RAG 里的具体成因)

  • §1 进来时以为幻觉是模型「不老实」、只能忍着 → 出去时知道在 RAG 里它多半有具体成因:检索没找到、上下文缺指代、或者被格式要求逼出来的
  • §2 进来时以为系统报出来的置信度是可信的 → 出去时能一眼分出真假三式,并知道只有检索相似度那一式是真信号
  • §3 【第二版改落点:原「全书唯一一次诚实」与 §2 重复】 进来时以为系统说「不确定」就没辙了 → 出去时知道不确定的时候有三条路:报出来 / 拒答 / 降级再检索一次;并当场与 05§6 的检索阈值划清界限(阈值丢候选,置信度拦答案)
  • §4 进来时以为可以让另一个模型判断答案有没有被证据支持 → 出去时知道这要用判断「前提是否蕴含假设」的模型、并且必须成对把前提和假设传进去;书里那次两个错叠在一起,支持率和矛盾率永远都是 0.0
  • §5 进来时以为摘要参数只影响长短 → 出去时看到全书自带的对照组:写死长度逼着模型凑字数,当场编出一本不存在的书;按输入长度动态算,摘要就是忠实的
  • §6 进来时以为「把证据高亮出来」和「做个摘要」差不多 → 出去时知道两者方向相反:高亮要原样摘出原句,而书那个配方是先过滤再摘要 —— 改写当场把证据改坏了

10 把步骤拼成链(原书第 10 章)

主走查: 配方 102 那段两轮对话。第一轮问「What is RAG?」→ 答对; 第二轮追问「And what method does it use for retrieval?」→ 这句话里的 it 不在句子里 → 链内部先把「追问 + 历史」合成一个能独立成立的问题,再拿这个问题去检索 → 答对。 书靠现成的链答对了,却没讲这一步,而那正是会话式 RAG 的全部难点。 另起两处: ① 配方 110 的重排账单(−6.5435,兑现 03§8 的许诺); ② 配方 105 把元数据拼进正文再嵌入。

承重词: ① 链与 LCEL(提示 | 模型 | 解析器) ② 追问改写(condense question) ③ 查询侧指代

  • §1 进来时以为把几个函数按顺序调一遍就叫链 → 出去时知道链的价值在可拼装可替换
  • §2 【第二版改名:叫「查询侧指代」,与 03§8 的「块内指代断裂」分名】 进来时以为多轮对话就是把历史一起发过去 → 出去时看到追问里的 it 在句子里找不到指代,检索这一步先崩,和模型强不强没关系
  • §3 进来时以为解决办法是把历史塞进提示 → 出去时知道真正的做法是先把「追问 + 历史」改写成一个独立成立的问题,再拿它去检索 —— 提示里塞历史帮不了检索,因为检索器读的是查询、不是提示
  • §4 【第二版新增,收配方 79 + 111,并把 03§8 的许诺在 §6 兑现】 进来时以为难问题只能整句丢给检索器 → 出去时知道有两条改造路:拆成子问题(配方 79)和先退一步问得更宽(配方 111,出处是 Google DeepMind 2023 年的 step-back 论文,书没交代);并照实说书里这两处都没做成
  • §5 进来时以为「按作者找文档」只能靠元数据过滤条件 → 出去时知道还有一条路:把「author: Alice category: health year: 2022」直接拼进要嵌入的正文
  • §6 【第二版新增:03§8 的账单在这里结清】 进来时以为重排只是排序 → 出去时看到配方 110 的五条候选里,唯一真正回答问题的那句因为以 It 开头、指代对象被切掉了,被打到 −6.5435 垫底 —— 第 03 章埋的雷在这里结账
  • §7 进来时以为链里接上工具就是代理的雏形 → 出去时知道书里的工具选择是一句 if,而「流式」是先整段生成完再逐词打印

11 从写死的链到会变通的代理(原书第 11 章 + 第 7 章结语)

主走查(第二版整条换掉): 拿第 7 章结语那段定义当尺子,把配方 121 从头量到尾 (text/14-fm-introduction.txt:1974,搜「Agentic RAG uses the LLM to control retrieval」): 生成初步答案了吗?评估证据了吗?改写后重新检索了吗?停止条件在哪? 每一步写出书里的实际行为 —— WordNet 同义词扩展(「Tell Maine about health」、 「History ahead 1900」)落在「改写查询」那一步,其余各步照实写「缺」。 这样两个承重词各占一步,而不是一步都不在。

承重词: ① 代理式 RAG(真定义,取自第 7 章结语) ② 停止条件

  • §1 进来时以为链已经够用了 → 出去时知道链是写死的顺序:第一次检索没找到,它照样往下走
  • §2 进来时以为代理式 RAG 就是「用了某个 agent 框架」→ 出去时知道它是一个循环,四步各是什么、停止条件有哪几种;而这段定义藏在第 7 章的结语里
  • §3 【第二版:原 §3§4 压成一节,原来两节都在讲查询扩展,与 06§7 同题】 进来时以为同义词扩展是稳赚不赔的 → 出去时看到「me」被扩成「Maine」、「before」被扩成「ahead」,而它还是给出了正确结果 —— 靠的是两个兜底:原查询留在候选里、跨所有变体取每篇文档的最优分。兜底设计比扩展本身值钱
  • §4 进来时以为这一章讲的是代理 → 出去时知道十个配方全是单趟直线:没有一次自我评估、没有一次重试、没有一个停止条件,逐条对照 §2 那个定义,缺了哪一步一目了然
  • §5 进来时以为这只是作者偷懒 → 出去时知道还有一层技术原因:这一章用的是 6000 万参数、且不是指令微调的 t5-small,给它自由格式的指令它只会把输入抄回来

12 怎么知道自己的 RAG 是好是坏(原书第 5 章配方 60 + 第 6 章配方 67 + 书架补)

主走查: 配方 60 的两条评测题 —— 每条查 top-3 → 判对的条件是「期望关键词是不是作为子串 出现在三条结果的拼接文本里」→ 期望答案写的是「RAG」「vector stores」这种词 → Accuracy@3 = 0.50,书对此不置一词;两条查询耗时 0.137 秒对 0.017 秒另起一处: 配方 67 的网格搜参(判据只取期望答案的第一个词,AI / Deep / AI → 27 组全拿 1.00)。

承重词: ① 忠实度(faithfulness) ② 上下文精度与召回

  • §1 进来时以为前面十一章讲完就能上线 → 出去时知道全书只量化过一次检索质量,结果是 0.50,而书对这个数字不置一词
  • §2 进来时以为 0.50 至少是个真数 → 出去时知道它量的不是「答对没有」,是「有没有出现这几个字母」
  • §3 进来时以为调参结果总归有点参考价值 → 出去时知道 27 组全拿 1.00 是因为判据只取第一个词,判据坏了,整张调参表就是噪声
  • §4 进来时以为评测就是算个准确率 → 出去时知道至少要分两层量:检索层(上下文精度与召回)和生成层(忠实度、答到问题上没有)
  • §5 进来时以为评测只看答得对不对 → 出去时知道延迟也是评测的一部分,而那 0.137 对 0.017 的差距是模型加载造成的:不区分冷热,量出来的延迟全是假的
  • §6 进来时以为没有标注数据就没法评测 → 出去时知道可以先用几十条小集合跑出基线,关键是判据不能松到什么都能通过

舍弃什么、为什么(第二版新增,审稿第 7 条)

原书 11 章章章覆盖,但有整节被我们丢掉。丢什么、为什么、读者去哪儿看,写在这里。

丢掉的出处为什么丢读者去哪儿看
第 2 章「按语义分组加载」(配方 23)09:1052它的输出和普通按段落加载没有区别,元数据里多了几个哈希03 章讲切分时会覆盖同一件事
第 3 章的九个结构式切法(配方 31–40)10:834它们是同一条原理的九次重复(找一个看得见的标记,照它切)03§6 用一张表一次列完
第 6 章「分布式索引」(配方 66)13:792它其实是单进程里三个索引合并,没有分片路由07§4 用一句话作分区的对照面
第 7 章「查询拆解」(配方 79)14:1446它是纯 Python 的词重叠检索,和提示无关10§4 与 step-back 合成一节
第 10 章「退一步链」(配方 111)17:1798书里那次没退成(给出的是同义改写)10§4
第 7 章「渐进式披露」(配方 81)14:1899它不是流式,是先生成完再 sleep 打印10§7 一句话带过

为什么这么切(与原书章序的差异,交稿时要向主人说明)

我们的原书为什么动
06 找得准 · 07 找得快原书第 9 章 · 第 6 章这两章讲的是同一件事的两半(准 / 快),原书中间隔着两章生成,读者会断线;而且原书第 6 章那个「近似索引」配方演示的是精确搜索,必须靠第 9 章的真数据来对照
08 生成 · 09 置信度与幻觉原书第 7 章 + 第 8 章原书两章高度重叠;按「怎么答」和「凭什么信」重新分,两章各自才有一条完整的线
12 评测原书第 5 章配方 60 + 第 6 章配方 67全书只有这两处评测,分散在两章的角落里且两处判据都坏了
03 切分提前把账单摆出来原书第 3 章埋雷,第 10 章配方 110 才结果书自己从没把这两处连起来 —— 这是全书最好的一条暗线

没有动的: 01–05 完全按原书 1–5 章的顺序;10、11 也按原书 10、11 章。

术语纪律(承重词一词一义,写之前先钉死)

只准是这个意思容易撞车的地方
归一化向量除以自己的长度(L2 归一),第 04 章第 06 章的分数归一必须叫「min-max 分数归一」,不许简称归一化
混合检索稠密 + 稀疏两路一起检索,第 06 章书在第 5 章把「语义 + 元数据过滤」也叫 hybrid,要点明这是书的用法
标记(token)模型把词切成的小块,第 03 章立与「字符」严格分开,书里两种单位混用
n-gram第 03 章那个配方里指「每块 n 个词、滑动 step 个词」要标明这是作者的用法
层级检索书用它指「粗排 + 精排」标准含义是按粒度分层,要点明书用错了名字
块内指代断裂块里留下失去指代对象的代词,第 03 章立与第 10 章的「查询侧指代」严格分名,两者是两种病

自查

  • 两头是同一件事的行:已逐行比对。 第二版新查的四处: 01§7(模型逐章缩水)与 08§2(提示是请求不是约束)已分家; 05§6(阈值,把不相关的丢掉)与 09§3(不确定的时候怎么办)不是一件事; 02§3 已把原 §3/§6 合并;11§3 已把原 §3§4 合并。
  • 新词密度: 07§2 原来一节要引入 ANN / IVF / HNSW / PQ / 聚类桶 / 分层图 / 乘积量化 / 召回, 超过一节 5 个的上限,第二版拆成 §2 §3 两节(拆,不是砍)。
  • 每章一条主走查,均已指定具体输入与具体数字; 另起处最多两处,未超上限。
  • 不再声称「全书共用输入」。 这本书的配方各用各的语料,凡是接起来的地方都标了接缝。

第二版改了什么(审稿十四条的落实位置)

#审稿说的改在哪
101/05 主走查张冠李戴01 主走查改成带配方号的表 + 接缝说明;05 主走查换成配方 53 的落盘—重载线
203§8 那颗雷没人收账03§8 明确指到配方 110 的 −6.5435;10§6 新增一节结账;08§3 只讲排序并照实说答案是对的
306 主走查的 BM25 分数与语料串了06 主走查改成配方 91→92→93→94→96 的分步表,语料接缝写清
4L2 距离讲了三遍承重讲解只留 04§6;01§6 一句话,05 原 §2 删掉换成「取前 k 条为什么是库的原语」
511 章承重词一步都不在走查上11 主走查整条换成「拿定义当尺子量配方 121」;§3§4 压成一节
607§2 密度超标 + 主走查语料不一07§2 拆成 §2§3;主走查只用配方 62 那个库,降维与冷热分层另起
7缺「舍弃什么」表文末新增舍弃表;配方 66 并进 07§4、配方 79/111 合成 10§4、配方 59 进 07§8
802 两处重合 + 丢了配方 24–2602§1 改成纯现象、§3 合并、新增 §6§7
904 跳级(「意思近⇒数也近」当给定)04 新增 §3,从对比训练讲起(书架来源)
10主线 ①→② 缺一级台阶主线新增 ②「它照着眼前的文字往下续写」;01 新增 §2
1109§3 落点被 §2 吃掉;08§2 落点撞车09§3 换成「不确定的时候怎么办」;08§2 换成「提示是请求不是约束」
12「4 块(276/282/166)」只数得出三个数01 主走查照实写「书说 4 块、只印了 3 块」,并加一条配方 63 的 6/3/1 作 03 章主走查
1302§5 的证据句是我们编的改用配方 22 的真实输出作主证据
14主线第 ⑤ 步海拔写高了主线只留效果句,机制记账给 04 章