跳到主要内容

阅读笔记 — RAG with Python Cookbook(Deepak Dhyani / BPB Online 2026)

别串书: 库里另有一本同名不同书的 rag-python-cookbook(Dominik Polzer / O'Reilly 2025)。 那一本只讲入库流水线(加载 + 切分),概念解释厚、配方少; 这一本(Dhyani / BPB)是 11 章 200+ 个配方的全流程代码书,概念解释极薄、代码极厚。

文件对照(chapters.json 把正文全标成了 fm-introduction,别被骗)

原书章文件行数
前言 Prefacetext/05-fm-preface.txt
第 1 章 Foundation of Retrieval-augmented Generationtext/08-fm-introduction.txt1736
第 2 章 Document Loaders for RAG Pipelinestext/09-fm-introduction.txt1553
第 3 章 Document Splitting Techniquestext/10-fm-introduction.txt1631
第 4 章 Embedding Strategies for Vector Retrievaltext/11-fm-introduction.txt1537
第 5 章 Vector Stores for Semantic Retrievaltext/12-fm-introduction.txt1698
第 6 章 Efficient Retrieval from Vector Storetext/13-fm-introduction.txt1957
第 7 章 Response Generation with LLM in RAG Systemstext/14-fm-introduction.txt1981
第 8 章 Prompt Engineering for RAG Systemstext/15-fm-introduction.txt1538
第 9 章 Effective Search for RAG Systemstext/16-fm-introduction.txt1818
第 10 章 Implementing RAG with Chainstext/17-fm-introduction.txt1917
第 11 章 Agentic RAG with Dynamic Retrievaltext/18-fm-introduction.txt1927

导航章(01 版权、06 书名页、07 目录、19 索引)跳过不读不引。 02/03/04 是作者简介、审稿人、致谢;05 是前言,读过。

前言(05-fm-preface.txt)

  • 定位:engineers / architects / data scientists / technical practitioners,要「production-grade RAG applications」。(:5)
  • 全书顺序即流水线顺序:架构 → 加载 → 切分 → 嵌入 → 向量库 → 检索效率 → 生成 → 提示 → 搜索 → 链 → 代理。(:6)
  • 最后一章(agentic retrieval)是自称的落点:「autonomous agents can adaptively decide what and how to retrieve」。(:6)
  • 每章一句话摘要在 :8–:18,是全书最可靠的章节意图来源。

全书体例(通读到第 1 章就已定型,后面章章如此)

每一章 = IntroductionStructure(小节清单)→ Objectives → 若干 Recipe(配方)Conclusion。 每个配方 = 三到八行「步骤散文」+ pip install 一行 + <文件名>.py + 整段带注释的 Python + Output: 一段。

这本书的真内容在代码和配方选型里,不在散文里。 散文几乎全是对代码的复述 (「This will find the most relevant text chunks for the given query」这种)。 写拆解时不能照散文重写,那样只会写成目录;要从代码里把机制读出来。

第 1 章 Foundation of RAG(08-fm-introduction.txt)

环境口径(全书统一,只在这一章交代一次)

  • 内存 ≥16 GB · 操作系统 Windows · Python ≥3.13.3 · LangChain 1.0.5 · LLM 用 Ollama 的 llama3.2:3b(本地跑,不调云 API)。(:45–:53)
  • 输入文件在本书 Git 仓库里(书里没给网址)。(:55)
  • 关键:全书没有一处用 OpenAI/Anthropic 云端模型,嵌入一律 sentence-transformers/all-MiniLM-L6-v2,生成一律本地 llama3.2:3b。这决定了书里所有数字的量级。

十四个配方,就是 RAG 六步的一次完整走通

配方干什么关键 API
1一次加载 PDF/TXT/DOCX 三种格式,汇进一个 all_docs_listPyPDFLoader / TextLoader / UnstructuredWordDocumentLoader(:105)
2递归字符切分,300 字符 / 重叠 50RecursiveCharacterTextSplitter(separators=["\n\n","\n",".", " ",""])(:289–:297)
3按标记数切,30 标记 / 重叠 10TokenTextSplitter + tiktoken(:367)
4按单一分隔符切CharacterTextSplitter(separator="\n")(:459)
5按句子边界切NLTKTextSplitter + nltk.download("punkt")(:531–:561)
6三句话 → 三个向量,384 维HuggingFaceEmbeddings("sentence-transformers/all-MiniLM-L6-v2")(:639, :689)
7建 FAISS 索引 + 相似度搜索 top-3FAISS.from_texts(:763)
8建 Chroma 库 + 落盘 + 搜 top-1Chroma.from_texts(persist_directory=...)(:859)
9加载 → 切 → 嵌 → FAISS → 搜,第一条完整入库线FAISS.from_documents(:967)
10已落盘的 Chroma 里检索Chroma(persist_directory=..., embedding_function=...)(:1057)
11带分数的相似度搜索similarity_search_with_score(:1175)
12检索 + 提示模板 + 本地 LLM,第一条完整问答线prompt | llm | StrOutputParser()(:1356)
13带元数据过滤的检索similarity_search(..., filter={"category":"LangChain"})(:1496–:1502)
14PDF 起头的最小完整 RAG 流水线六步合一(:1612–:1721)

可以拿来当主走查的真实数字(书里给的,不是编的)

  • 一段 275 字符的 RAG 定义句,RecursiveCharacterTextSplitter(300/50) 切成 4 块,前三块分别 276 / 282 / 166 字符。(:319–:331)
  • 同一段话交给 TokenTextSplitter(30/10) 切成 3 块,能明显看出重叠:第 2 块以「language models (LLMs) with a retrieval system」开头,正是第 1 块的结尾。(:399–:411)
  • 嵌入结果:3 条文本 → 3 个向量,每个 384 维;第一个向量前 5 维是 [-0.0874, -0.0331, -0.0124, 0.0260, -0.0541]。(:687–:693)
  • 带分数检索(FAISS):0.9408 / 1.3505 / 2.0506,分数越小越相关(FAISS 返回的是 L2 距离)。(:1201–:1217)
  • 元数据过滤:三条文档里两条 category=LangChain,k=5 但只返回 2 条 —— 过滤发生在检索之后。(:1532–:1548)

书里没讲透的地方(第 1 章)

  1. similarity_search_with_score 的分数是什么、越大越好还是越小越好,书里一个字没说。 它只印出来。读者看到 0.94 / 1.35 / 2.05 会本能地以为 2.05 最好。这是必须补的一处(FAISS 默认 L2 距离,越小越近)。
  2. 384 维是从哪来的、为什么是 384、换个模型会变成多少,没说。
  3. 重叠(chunk_overlap)为什么要有,只在代码注释里写了 "to preserve context",没讲清楚它防的是什么(一句话被切断)。
  4. 配方 8 把 vector_store.add_texts(texts) 这一步注释成「Persist the vector store」(:869–:871)—— 这是书里的错:from_texts 已经写过一遍了,add_texts 是又写了一遍重复数据,不是落盘。这也解释了配方 10 为什么问「What is RAG?」却返回「RAG is a polpular framework to make Agentic AI applications」(:1095)—— 它命中的是配方 8 里那条带拼写错误的样例句。
  5. 配方 9 用 HuggingFaceEmbeddings() 不传模型名(:959),默认换成了另一个模型(768 维),和全书其他地方的 384 维模型不一致。入库和查询用不同嵌入模型 = 检索必然失效,这一点书里从来没警告过。
  6. 配方 10 用的是 from langchain.vectorstores import Chroma / from langchain.embeddings import HuggingFaceEmbeddings(:1039–:1041),这两个导入路径在 LangChain 0.2 就已废弃,和同书其他配方用的 langchain_chroma / langchain_huggingface 打架。
  7. 配方 9 的输出(:997)是一段字符交错的乱码,像是排版事故,读者会以为 FAISS 出错了。
  8. 代码里多处出现全角引号 print(f”\n Query: {query}\n”)(:775, :887),照抄会直接语法错误

伏笔(第 1 章埋、后面揭)

  • 「splitting technique ensures that each piece of content stays within the context」(:239)—— 为什么必须切、切多大,第 3 章和第 6 章才给方法。
  • 元数据过滤在这里只是一个 filter= 参数(:1502),到第 5、第 9、第 10 章会长成完整策略(hybrid search / self-query chain)。
  • 「retrieval 和 generation 的区别」在 Objectives 里被强调(:37),但第 1 章的配方 12/14 就把两者接上了 —— 全书真正的分野是第 1–6 章讲怎么把对的东西找出来,第 7–11 章讲找出来之后怎么用

第 2 章 Document Loaders(09-fm-introduction.txt)· 配方 15–26

三个「核心部件」(全书唯一一次给数据结构定义,:37–:41)

  • Document:流水线里流动的基本单位,一个 Document = page_content(正文字串)+ metadata(键值对)。
  • Metadata:挂在文档上的上下文键值对,作用是「过滤 + 可追溯」。
  • Loader interface:实现统一接口,读文件/网页 → 解析成文本 → 返回 List[Document]这就是加载器的全部意义:把十几种格式收敛成同一种东西。

配方清单

配方格式用什么一个 Document 对应什么
15MarkdownUnstructuredMarkdownLoader整篇合成 1 个(:187)
16CSVCSVLoader一行 = 一个文档,而且渲染成「Name: Anil Sharma\nDepartment: …」的键值文本(:277–:287)
17ExcelUnstructuredExcelLoader一个工作表 = 一个文档,整张表压成一行字串「ID Name Department Age 1 Jatin Kumar HR 34 …」(:393)
18HTMLBSHTMLLoader整页 1 个,标签被剥掉只留文字(:532–:552)
19JSONJSONLoader(jq_schema=".")整个数组挤成 1 个文档(:659–:661)
20网页WebBaseLoader1 个,metadata 带 source/title/language(:731)
21TXT + 自定义元数据TextLoader 后手工往 doc.metadata 里塞键元数据变成 {'source':'local_file','category':'tutorial','author':'Deepak'}(:827)
22预处理小写化 + 去停用词 + 空白归一见下,这一条有问题
23语义分组加载langchain_unstructured.UnstructuredLoader按段落切成 Section,metadata 里带 category: NarrativeTextelement_id 哈希、last_modified(:1052)
24批量并行加载ThreadPoolExecutor(max_workers=4) + 按扩展名分发 loader输出 Loaded 3 documents.(:1250)
25先按标记数打包再切tiktoken.get_encoding("cl100k_base")3 个小文件 → 1 个逻辑包 → 6 个块(:1416–:1420)
26自定义加载器继承 BaseLoader,实现 load()元数据可带列表 tags: ['custom','demo','loader'](:1542)

第 2 章的缺口(要补的)

  1. 配方 22 是一条有害的建议,书里当最佳实践讲。 它对文本做小写化 + 删停用词(:894–:914), 理由写成「filter out common words that do not contribute to the meaning」。 这是词袋 / TF-IDF 时代的习惯;而这本书用的是 sentence-transformers 这类基于 Transformer 的嵌入模型, 它们是在完整的自然语句上训练的,删掉 not / without / between 这类停用词会直接翻转句意 (「RAG does not require fine-tuning」删完变成「rag require fine-tuning」)。 书里从头到尾没有一句警告。这是全书最需要外部来源纠正的一处。
  2. 配方 19 的 jq_schema="." 把整个 JSON 数组变成一个文档(:661),而散文写的是 「Each document will have page content and metadata attributes」—— 读者会以为是每条记录一个文档。 书里没有提 .[](逐条)这个更常用的写法,也没解释 jq_schema 是什么语法。
  3. 配方 17 把整张 Excel 表压成一行无分隔的字串(:393)—— 行列关系全丢。 书里不提这有什么后果(检索时问「Rajeev 多少岁」几乎必错),也不指向后面哪一章解决。
  4. 配方 24 用 ThreadPoolExecutor 做「并行加载」,但没说清楚为什么线程池对这件事有效 (读文件和解析 PDF 大部分时间在等 I/O),也没提 CPU 密集解析在 Python 里线程并不真并行。 代码还对每个文件调用了两次 get_loader(fp)(:1170–:1172)。
  5. from langchain.document_loaders import CSVLoader(:229)、 from langchain.document_loaders import TextLoader, PyPDFLoader, ...(:1118) —— 这两处用的是已废弃的导入路径,和同书其他配方用的 langchain_community.document_loaders 打架。
  6. 配方 22 的 Input 声明用 RAG_uncleaned.txt(:952),代码里 load 的却是 RAG.txt(:880); Input 只有一段,Output 却出现了两段的内容(:966)。输入输出对不上。
  7. 全书代码里散布着全角引号(print(f”…”)doc.metadata[“source”]),照抄即语法错误

第 3 章 Document Splitting Techniques(10-fm-introduction.txt)· 配方 27–40

这一章唯一真正的概念骨架(:39–:43)

切分策略分两大类:

  • 结构式(structural):Markdown / 正则 / 按页 —— 靠文档看得见的标记(标题、章节、页边界)来切,保留原有组织;
  • 语义式(semantic):按主题 / 按嵌入 —— 靠意思来切,重排或分段依据概念相似度、话题连贯性、语义转折,不看排版。

选法:排版规整的文档用结构式;无结构、概念密集的文本用语义式。

这一段(:39–:43)是全书为数不多的「真解释」,写拆解时应该把它提到很靠前的位置。

为什么必须切(:101–:115,书里给的四条)

  • LLM 有标记上限,多用标记直接等于多花钱;
  • 切块才能高效存储、检索、语义匹配;
  • 切得好直接提升检索质量;
  • 三个坑:块太小丢上下文 / 块太大超标记上限 / 重叠与句子边界处理错会丢语义。

配方清单(十四条,按结构/语义分两类)

配方切法类别关键机制与真实输出
27n-gram(自己写一个继承 TextSplitter 的类)结构n=8 词一块、overlap=3 → step=5;一句 39 词切出 7 块,每块 8 词(:250–:276)
28主题切分语义SentenceTransformer('all-MiniLM-L6-v2') 编码 9 句 → 余弦相似度矩阵 → 硬编码三个中心 [0,3,6] + 阈值 0.5(:428–:442)
29正则结构r"^##\s+(.*)$" + re.split(flags=re.MULTILINE),再把「标题 + 内容」两两配对(:568–:586)
30Markdown 标题结构r'(?=^#{2,3} .*)' 零宽前瞻,所以标题本身留在块里不被吃掉(:694)
31按元数据分组结构setdefault(source, []).append(doc) 分组,再逐组切(:834–:856)
32按时间结构时间戳 0/60/130/190/250/310 秒,窗口 120 秒 → 3 块,起点标 0:00:00/0:02:00/0:04:00(:994–:1010)
33HTML 标签结构BeautifulSoup.find_all(["h1","p"]) → 每个标签一个 Document,再过一遍字符切分(:1082–:1084)
34表格按行结构pandas itertuples(),每行拼成 "A: Book A"(:1170)
35按页结构PyPDFLoader 本来就是一页一个 Document,所以「按页切」= 什么都不做(:1229–:1239)
36自定义分隔符结构START ... END 成对提取(:1315–:1325)
37JSON Lines结构一行一个 JSON 对象 → 一个 Document,id 进 metadata(:1385–:1389)
38幻灯片结构--- 切,slide 号进 metadata(:1446–:1448)
39按说话人结构line.split(':', 1)speaker 进 metadata(:1512–:1518)
40DOCX 按段落结构python-docxdoc.paragraphs,paragraph_num 进 metadata(:1582–:1596)

一眼可见的分布:十四条里只有配方 28 是语义式,其余全是结构式。 这与本章开头「结构式 vs 语义式」的二分承诺严重不对称 —— 写拆解时要点明。

第 3 章的缺口与错误(要补的)

  1. 配方 27 会静默丢数据。 循环写作 range(0, len(words) - self.n + 1, step)(:221), 词数不是 step 的整数倍时尾巴直接被扔掉。书里的输出就是证据:最后一块停在 「generated response against the query raised by user」,原句结尾「to a RAG system.」消失了(:276)。 书里一个字没提。
  2. 配方 27 的 n-gram 举例本身是错的。(:75–:81)原文写「for example, for n=3, the sentence will look like: RAG with Python」, 紧接着「If we break it into grams of size 2, it will become: RAG with, with Python」—— n=3 那一句根本没给出 3-gram。 而且这里的「n-gram」和 NLP 里标准的 n-gram(用于语言模型的连续词序列统计)不是一回事, 它其实只是「每块 n 个词、滑动 step 个词」。必须点明是作者的用法。
  3. 配方 28 会静默丢句子,而且丢得很厉害。 输入 9 句,输出三块里只出现 4 句 (:460–:470):相似度 ≤0.5 的 5 句既不属于任何簇,也不会被兜底收进任何块 —— 直接消失。 书里承认阈值和中心是「for simplicity」(:308),但没有说未分配的句子会丢。 这是「朴素语义切分」最典型的坑,值得单独讲透。
  4. 配方 31 的标题与代码对不上。 名为「按元数据分组切分」,但代码分完组之后仍然是 逐个 Document 单独 split_documents([doc])(:854)—— 分组这一步对结果没有任何影响, 输出三块和不分组时完全一样(:870–:880)。
  5. 配方 32 的时间窗有边界缺陷: current_start_time += chunk_duration 每次只加一个窗口(:970), 若两条记录之间的间隔超过一个窗口,后面所有块的起始时间标签都会偏小。书里的样例数据恰好避开了这个坑。
  6. 本章不讲怎么选块大小。 三十九个配方里 chunk_size 取过 300、200、100、60、50,没有一次说明为什么。 选块大小的讨论被推到第 6 章(optimizing chunk sizes)和第 9 章(optimal chunking strategies)。
  7. 本章不讲 RecursiveCharacterTextSplitter 到底怎么递归(第 1 章配方 2 用过它,给了 separators 列表, 但从没解释「递归」指的是「先试 \n\n,切不够小再试 \n,再试 .,最后逐字符」)。这是必须补的一处。
  8. 本章也不提 LangChain 自带的 SemanticChunkerMarkdownHeaderTextSplitter —— 配方 28、30 都是手写替代品。

第 4 章 Embedding Strategies(11-fm-introduction.txt)· 配方 41–51

这一章里最值钱的一段:配方 42 把「句子怎么变成一个向量」拆开了

配方 42(:233–:251)不用 LangChain,直接用 transformers 手写了一遍:

tokenizer(docs) → model(**encoded) → last_hidden_state(每个标记一个向量)
→ mean_pooling(按 attention_mask 加权求和 ÷ 有效标记数)→ 每句一个向量

这是全书唯一一次露出「嵌入模型内部在干什么」。 一句话先被切成标记,每个标记出来一个向量, 再按「哪些位置是真内容(attention_mask)」求平均,才压成一个句向量。 但书里把这一步的小标题写成「3. Tokenize the documents」(:227),完全没解释 mean pooling 是什么、为什么要除以 mask 之和。 写拆解时这里应该单开一节 —— 它能把「384 维是怎么来的」「为什么两句话能比距离」一次讲透。

配方清单

配方干什么真实数字 / 关键 API
41一句话 → 一个向量embed_query,384 维,前 10 维 [-0.0455, -0.0481, -0.0086, 0.0733, …](:150–:154)
42手写 mean pooling 嵌一批文档AutoTokenizer + AutoModel + mean_pooling,输出 torch tensor(:273–:277)
43裸 FAISS(不经 LangChain)faiss.IndexFlatL2(dim)index.addindex.search,距离 1.0279 / 1.3194(:369–:411)
44离线嵌入SentenceTransformer("all-MiniLM-L6-v2"),emb.shape = (384,)(:497)
45自定义嵌入类继承 Embeddings,实现 embed_documents + embed_query 两个方法(:570–:582)
46批量嵌入batch_size=32,tqdm 进度条;把 50 行重复文本切成 200 字符块(:690–:742)
47只嵌一次、落盘复用vectorstore.save_local(path) / FAISS.load_local(path, model, allow_dangerous_deserialization=True)(:862–:910)
48嵌入 + 元数据见下,标题说过滤,代码不过滤
49只嵌摘要pipeline("summarization", model="sshleifer/distilbart-cnn-12-6"),max_length=50, min_length=20(:1145–:1157)
50预归一化vecs / np.linalg.norm(vecs, axis=1, keepdims=True),三条噪声句归一后 norm 全 = 1.00(:1278–:1324)
51块的相似度图networkx 建图,余弦阈值 0.7 —— 输出零条边(:1462)

表 4.1「怎么选嵌入策略」(:1482–:1526)

书里唯一一张选型表,六行:通用文本→预训练通用嵌入 / 高度专业(法律医疗金融)→领域微调 / 预算或隐私受限→本地模型(all-MiniLM-L6-v2、bge-base)/ 要极致准确→混合或重排(dense+sparse、bge-reranker)/ 多模态→CLIP、BLIP / 大规模生产→高效模型 + FAISS 或 Chroma + 缓存。 但第二行的「为什么管用」一栏串行了,写成了「Tight budget or have privacy constraints」—— 那是第三行的理由。表格错位。

第 4 章的缺口与错误(要补的)

  1. 配方 50 的说法是错的,而且错得会误导人。 小节名叫「Noise-resistant embeddings with pre-normalization」, 正文说真实数据有错字、大小写不一致、OCR 失真,「预归一化是一种稳住嵌入的技术」(:1195–:1197)。 把向量除以自己的长度(L2 归一化)和抗错字没有任何关系。 它真正的作用是: 归一化之后,欧氏距离的排序和余弦相似度的排序变成同一个 —— 所以用 IndexFlatL2 也能当余弦用。 而且书自己的输出恰好证伪了它:三条噪声句在归一化之前向量就已经几乎一样了(前 5 维只差在小数点后第三位), 那是模型本身稳,不是归一化的功劳。这一处必须纠正,并给出归一化真正的用途。
  2. 配方 51 的输出是空的。 「Graph edges based on similarity >= 0.7:」下面一条边都没有(:1462–:1464), 八个块两两之间没有一对超过 0.7。书照样把它当成功案例收尾。 —— 顺带暴露一个真问题:块切得越碎(这里 chunk_size=80),块与块之间的相似度就越低, 阈值法在小块上根本不成立。这是很好的教学素材,但要我们自己讲。
  3. 配方 51 的正文里混进了另一个配方的介绍。(:1332)那一段讲的是「两段式检索:先用轻量本地模型召回, 再用 API 模型重排」,和图结构毫无关系,后面也没有对应的代码。像是删掉某个配方后残留的文字。
  4. 配方 48 名为「用元数据做更好的过滤」,代码里一次都没过滤。(:1016–:1018)只是做了普通的 similarity_search(query, k=2) 然后把 metadata 打印出来。
  5. allow_dangerous_deserialization=True 这个参数,全书出现四次,一次都没解释。(:910 等) 它的含义是「我知道这个索引文件会被 pickle 反序列化、因而可以执行任意代码,我信任它」。 这是一个真实的安全开关,读者照抄去加载别人给的索引文件会中招。必须补。
  6. 配方 49 把摘要嵌进库里,却把回原文的指针丢了。(:1173)metadatas=[doc.metadata for doc in documents], 而那三个 Document 根本没有 metadata —— 结果检索到摘要之后无法回到全文。 小节正文明明说「retrieves the relevant summaries, which can then be expanded by referencing the original full text」(:1052), 代码没有实现这句话。
  7. 配方 46 的输出(:754–:768)是同一句话重复八遍 —— 因为演示语料是 "LangChain is powerful.\n" * 50。 书没有说明「一个 200 字符的块装得下 8 行」,读者会以为程序出错。
  8. FAISS 的全称与出身:Facebook AI Similarity Search,Meta AI 出品(:281)。这是书里少有的直接交代来历的地方。

第 5 章 Vector Stores(12-fm-introduction.txt)· 配方 52–61

这一章的立论(:7–:11)

关键词搜索靠字面匹配,查询换个说法就找不到;于是有了语义检索: 文档和查询都表示成高维空间里的向量,相似度按意思算而不是按字面算。 向量库(vector store)就是专门存这些向量、给它们建索引、快速查询的数据库。

配方清单

配方干什么关键点
52FAISS 建库 + 落盘 + 查FAISS.from_documentssave_local("chapter5_faiss_index")(:154–:162)
53分成两个脚本:一个建、一个查save_local / load_local(..., allow_dangerous_deserialization=True)(:271, :309)
54「混合搜索 + 元数据过滤」similarity_search(query, k=2, filter={"topic":"Vector DB"})(:435–:443)
55大批量嵌入batch_size=64,20 条语料 → 切块 → 建库 → 落盘 → 加载 → 查(:533–:606)
56ChromaDB 建库Chroma(collection_name=..., embedding_function=..., persist_directory=...) + add_documents(:694–:726)
57切分器 + 向量库串起来PDF → RecursiveCharacterTextSplitter(500/50) → Chroma(:866–:900)
58「块 + 自动摘要」一起检索pipeline("summarization", model="facebook/bart-large-cnn"),摘要塞进 metadata(:1042–:1076)
59异步建索引asyncio.gather + loop.run_in_executor 并发读文件(:1270–:1305)
60评测检索质量见下,全书唯一一次给出量化指标
61稠密 + 稀疏双路检索BM25Retriever.from_texts + Chroma retriever + RunnableParallel + 去重合并(:1535–:1663)

配方 60 是全书唯一一次量化评测(:1397–:1499),必须写进拆解

两条评测题 → 每条查 top-3 → 检查期望关键词有没有出现在检索结果的拼接文本里
结果:Accuracy@3 = 0.50 第一条 0.137 秒 · 第二条 0.017 秒 · 平均 0.077 秒

三个可讲的点:

  • 准确率只有 0.50,而书对此不置一词;
  • 第一条查询比第二条慢将近八倍(0.137 vs 0.017 秒)—— 第一次要把模型加载进内存,之后是热的;
  • 它的「对不对」判据是关键词子串匹配(expected.lower() in retrieved_texts.lower(),:1441), 期望答案写的是 "RAG""vector stores" 这种词。这是极弱的代理指标,书里没说它弱。

第 5 章的缺口与错误(要补的)

  1. 「混合搜索(hybrid search)」这个词在同一章里被用成了两个意思。 配方 54 把「语义相似 + 元数据过滤」叫 hybrid(代码注释直接写 # Hybrid filtering,:441); 而配方 61 讲的 dense + sparse 才是业界通称的 hybrid search(书里管它叫 multi-vector retrieval,:1501–:1509)。 同一个承重词一词两义,读者出门必撞车。 拆解里必须把两者拆开命名并说明业界叫法。
  2. 配方 56 和 57 的代码跑不起来: 用了 SentenceTransformerEmbeddings(...)(:684, :882), 但两段代码的 import 块里根本没有这个名字(只 import 了 HuggingFaceEmbeddings)。直接 NameError。 (配方 58 的 import 块里倒是有 from langchain_community.embeddings import SentenceTransformerEmbeddings,:992 —— 显然是复制粘贴时漏了。)
  3. 配方 56 的输出里同一条文档出现了两遍(:758–:760),配方 55 也一样(:618–:620)。 原因是 persist_directory 里的数据在反复运行时累积重复,而 add_documents 不去重。 书完全没有注意到,更没有讲「重复入库」这个生产环境的常见事故。
  4. 书从来没有说过:相似度检索永远返回 k 条,哪怕全都不相关。 证据满地都是:配方 53 问「What is LangChain?」,第二条返回「Transformers from Hugging Face are widely used in NLP.」(:339); 配方 61 问「What are neural networks?」,第二条返回「Reinforcement learning is based on rewards and punishments.」(:1685)。 没有任何一处讨论分数阈值 / 相关性截断。 这是全书最大的概念缺口之一。
  5. 配方 58 的代码没有实现它自己写的设计。 正文说要「块和摘要都嵌入、都参与检索」(:944–:950), 代码里 Chroma.from_documents(documents=summarized_docs, ...) 嵌的是 page_content(原块), 摘要只是被塞进 metadata,从未被嵌入、从未被检索。
  6. 配方 59 的异步只并行了便宜的那一半。 asyncio.gather 并发的是读文件(:1305), 而真正耗时的嵌入计算 db.add_texts(all_chunks)(:1321)仍然是一次性同步调用。 小节正文吹的是「Multiple embeddings can be generated in parallel」(:1168),代码没做到
  7. 配方 61 的「加权融合」是假的。 两路权重都写死成 0.5(:1611), 于是最后那次 sorted(..., key=score) 什么都没排。它实际做的只是「稠密结果 + 稀疏结果拼起来、按内容去重」。 业界做这件事的标准办法叫 Reciprocal Rank Fusion(RRF),LangChain 也自带 EnsembleRetriever, 书里两者都没提。这是要补外部来源的一处。
  8. 配方 61 里 BM25 只被介绍成「traditional keyword-based techniques」(:1503), 没有解释 BM25 到底怎么打分(词频 + 逆文档频率 + 文档长度归一)。第 9 章会再用一次,同样不解释。

第 6 章 Efficient Retrieval(13-fm-introduction.txt)· 配方 62–71

这一章是全书信息密度最高的一章,也是错误最密集的一章。 它管的是「找得对」之后的「找得快、找得准」。

配方 65(交叉编码器重排)是全书最好的一段,应该当主走查

书里难得给了真解释(:566–:570):

双编码器(bi-encoder) 把查询和文档各自单独变成向量,再比距离 —— 快,可以预先把全库算好; 交叉编码器(cross-encoder) 把查询和候选文档拼在一起喂进模型,直接吐一个相关性分数 —— 因为它建模了两者之间的完整交互,排序更准;但它无法预先计算,所以只用在前 k 条候选上,不用在全库。

真实数据(:756–:790),而且重排真的改了顺序:

名次FAISS 初排交叉编码器重排分数
1Applications of ML and AI are diverseApplications of ML and AI are diverse9.0629
2Machine Learning (ML) is a subset of AIMachine Learning (ML) is a subset of AI7.0553
3Artificial Intelligence (AI) is the simulation…Deep Learning is a specialized branch6.0113
4Deep Learning is a specialized branchArtificial Intelligence (AI) is the simulation…1.7071

查询是「What are the applications of machine learning?」。那条泛泛讲 AI 定义的块, 向量检索把它排第 3,交叉编码器把它踢到最后,分数只有 1.7,和前面差了一个数量级。 模型是 cross-encoder/ms-marco-MiniLM-L-6-v2(:674)。 (书没说这些分数是无界的 logit,不是 0–1 概率。)

配方清单

配方干什么真实数字
62「近似最近邻索引」d=64、库 1000 条、查 5 条、k=5;距离 5.68–7.98(:155–:181)
63块大小调优同一段文字:200 字符 → 6 块 · 500 → 3 块 · 1000 → 1 块(:308–:338)
64查询缓存5 次查询里 2 次命中,query_cache 就是一个 dict(:434–:458)
65交叉编码器重排见上表
66「分布式索引」三个文件各建一个 FAISS,再 merge_from 合并(:876–:880)
67网格搜参3 块大小 × 3 重叠 × 3 个 top_k = 27 组,全部 Accuracy=1.00(:1150–:1206)
68降维TruncatedSVD,384 维 → 5 维(:1366–:1368)
69按主题分区KMeans 分 3 簇 → 3/1/1 块;查询按最近质心路由到 Topic 0(:1532–:1542)
70自适应切分按内容类型分派:代码整块留、列表整块留、叙述文按 500 切(:1598–:1646)
71冷热分层180 天内访问过的进 FAISS(热),其余落 JSON(冷)(:1844–:1876)

书里给的两个可引用的经验值

  • 块大小 300–800 个标记,视用途和模型上下文窗口而定(:187)。 (对照:另一本 RAG 书 Polzer 给的是 1000–2000 个标记 —— 两本书差一倍多,值得在拆解里点出来。)
  • 热数据的界线:180 天(6 个月)内被访问过(:1854)。

第 6 章的缺口与错误(要补的,这一章最多)

  1. 配方 62 名为「近似最近邻」,演示的却是精确暴力搜索。 小节开头点名 HNSW 图、FAISS 索引、Annoy 树是常见 ANN 手法(:67), 代码里用的却是 faiss.IndexFlatL2(:119)—— Flat 就是逐条算距离,一条不漏,零近似。 print("Is trained:", index.is_trained) 输出 True(:155),更是暴露了这一点: Flat 索引根本不需要训练,所以天生 is_trained=True;真正的 ANN 索引(IVF、PQ)才需要先 train()「用什么换什么」这个 ANN 的核心(牺牲一点召回换几十上百倍速度)全书没讲。必须补。
  2. 配方 66 的代码和它自己的输出对不上。 vs = FAISS.from_documents(chunks, embedding_model)vectorstores.append(vs) 这两行缩进在 for 循环之外(:868–:870), 照这个写法只有最后一个文件进了索引、vectorstores 里只有一个元素。 可输出却同时命中了三个文件的内容(:952–:962)。印出来的代码跑不出印出来的结果。
  3. 配方 66 叫「分布式索引」,其实是单进程里的三个索引合并(merge_from)。 没有分片路由、没有节点、没有跨机通信。小节正文吹的是横向扩展和容错(:794–:796),代码一样都不沾。
  4. 配方 67 的 27 组结果全是 1.00,因为它的评分标准形同虚设。 判对的条件是 expected.lower().split()[0] in retrieved_texts.lower()(:1061)—— 只取期望答案的第一个词,而那三个词是 AI / Deep / AIAI 这两个字母在几乎任何一块里都能命中(连 "explAIn" 都算)。 于是「最优参数」输出的其实只是第一个被试到的组合(chunk=200, overlap=20, k=2), 因为并列时不会替换。书把它当作调参结论写了出来(:1204–:1206)。
  5. 配方 67 的输出第一行是 Documents stored in ChromaDB.(:1148),而这个程序从头到尾没碰过 Chroma。 —— 复制粘贴污染,和第 5 章配方 57 的输出串了。
  6. 配方 68 的降维结果是 384 → 5,不是 384 → 128。 因为 TruncatedSVD 的成分数 不能超过样本数,而样本只有 5 块(:1282–:1284)。书照原样印出 Reduced dim: 5 却不解释一个字。 这恰恰是降维最该讲的约束。
  7. 配方 68 的代码有一行被注释吃掉了: # 3. Initialize the embedding modelembeddings = HuggingFaceEmbeddings(:1270), 赋值语句整个跑到注释里去了,下一行 original_embeddings = embeddings.embed_documents(...) 必然 NameError
  8. 配方 69 有一行两条语句挤在一起: kmeans = KMeans(...)labels = kmeans.fit_predict(...)(:1466),语法错误。 更要紧的是:按主题分区之后查询只进一个分区(:1512), 要是答案落在别的分区里就永远取不到 —— 这是拿召回换速度的典型取舍,书一个字没提。
  9. 配方 70 当场违反了它自己立的规矩。 规则写的是「代码块整块保留」(:1612–:1616), 可它先按 \n\n 分块,于是 class Student 里的空行把这个类劈成了两块: Chunk 4 只有 __init__,Chunk 5 只有 promote(:1748–:1766)。输出就是证据。 elif "-" in block(:1618)判「这是列表」也极其脆弱 —— 任何含连字符的句子都会被误判。
  10. 配方 71 的冷热分层是反的。 热数据进 FAISS 一次建好索引; 而冷数据每次查询都要 embedding_model.embed_documents(cold_texts) 重新嵌入一遍全部冷数据(:1906), 再手算余弦。这比把它们留在索引里贵得多,而不是更省。小节正文说冷存储是「slower, cheaper」(:1780), 代码实现的却是「每查一次就把冷数据全算一遍」。
  11. 配方 64 的缓存是按查询字串精确匹配的(if query in query_cache,:442)。 「What is machine learning?」和「what is ML?」不会命中同一条。 书把缓存的好处吹了三条(:346–:350),没提这个致命限制,也没提业界的做法(语义缓存: 先把查询嵌入,和缓存里的查询向量比相似度,超过阈值才算命中)。这是要补的一处。
  12. 配方 62 用的是 np.random.random 生成的随机向量(:109–:111), 随机向量之间没有任何语义,所以那 5 个「最近邻」不代表任何检索质量,只演示了机制。书没有说明。

第 7 章 Response Generation(14-fm-introduction.txt)· 配方 72–81

全书的转折点:第 1–6 章讲「怎么把对的东西找出来」,从这一章起讲「找出来之后怎么用」。

一个必须先说的事实:这一章换了模型,而且换成了一个小得多的模型

  • 第 1–6 章的环境清单里写着「LLM model: Ollama's llama3.2:3b」; 第 7 章的环境清单把这一行删掉了,也删掉了操作系统那一行(:45–:53)。
  • 十个配方里有八个用的是 google/flan-t5-base(:135 等),一个用 google/flan-t5-large(:1815), 一个纯 Python 不用模型。
  • flan-t5-base 是一个约 2.5 亿参数的指令微调 seq2seq 模型 —— 比 llama3.2:3b 小一个数量级以上这直接解释了本章几乎所有输出的怪相:它们全是把上下文原句抄回来。 书里从头到尾没有交代换模型这件事,更没解释后果。

十个「生成花样」

配方名字实际做了什么
72直接作答RetrievalQA.from_chain_type(chain_type="stuff") + qa.run(query)(:153–:167)
73结构化输出手拼 JSON:question / answer / sources / confidence(:333–:343)
74「思维链引导」见下 —— 其实是采样 5 个候选选一个
75带引用作答把上下文编号成 [1] (来源): 内容,提示模型「cite them like [1], [2]」(:734–:738)
76混合生成稠密 + BM25 双路取文档,再用三个不同提示各生成一遍,拼起来(:934–:954)
77批判性核验facebook/bart-large-mnli 逐条核验答案里的每个断言(:1067–:1085)
78置信度打分拿 FAISS 距离换算成 0–1 的「置信度」(:1268–:1284)
79查询拆解纯 Python 零依赖:按连接词拆问题 + Jaccard 词重叠检索(:1446–:1479)
80「上下文增强」见下 —— 代码里没有任何增强
81渐进式披露生成完整答案后按句分批 time.sleep(2) 吐出来(:1899–:1923)

书里讲得好的两处(要保留进拆解)

  1. 「stuff」链的边界说清楚了(:77):把检索到的文档原样塞进提示, 文档短的时候管用,否则会撑爆模型的标记上限
  2. 对「让模型吐 JSON」的诚实警告(:187):

    提示可以鼓励模型输出 JSON,但不能保证语法合法。没有 JSON 模式、函数调用或解析器校验, 模型会生成「看起来像 JSON、实际解析不了」的文本。生产环境要用受约束的结构化输出接口,不能只靠提示。 这是全书最有价值的一条工程忠告。

埋在第 7 章末尾的一段:全书最好的「代理式 RAG」定义(:1974)

代理式 RAG 用大模型来控制检索。生成初步答案后,模型评估检索回来的文档 证据是否充分、是否相关;不够就改写查询或发出更有针对性的子查询,重新检索 (可以换检索器、换过滤条件、换时间窗口),循环直到满足停止条件 —— 置信度阈值、最大轮数、标记或预算上限

这段话比第 11 章(专讲代理式 RAG 的那一章)给出的定义更完整、更可操作。 而它却出现在第 7 章的结语里,下一章讲的是提示工程,完全接不上。 写拆解时应该把这段提到讲代理式 RAG 的地方去,并注明它出自第 7 章结语。

第 7 章的缺口与错误(要补的,这一章的「代码不兑现承诺」最严重)

  1. 配方 74 叫「思维链」,做的却是「best-of-N 采样 + 挑一个」。 代码开 do_sample=True, top_k=50, top_p=0.95, num_return_sequences=5(:461–:467)生成五个候选, 然后按「候选和(查询+上下文)的余弦相似度」挑最高的那个(:510–:530)。 两个问题:
    • 这不是思维链。 思维链是让模型把中间推理步骤写出来;这里只是多抽几次样再选。 提示里那句「Think step by step」对 flan-t5-base 不起作用。
    • 选拔标准本身是错的:和原文越像分数越高。 五个候选的分数是 0.906 / 0.811 / 0.776 / 0.865 / 0.773,冠军(0.906)恰恰是把原句一字不差抄回来的那个(:590)。 这是在奖励复述,不是在奖励正确。
  2. 配方 73 的「置信度」就是答案的长度。 confidence = round(min(1.0, len(answer_text) / 300), 2)(:331)。 输出里的 0.79 意思是「答案有 237 个字符」。它和答案对不对毫无关系。
  3. 配方 78 的置信度同样是假的,而且理由写错了。 注释写着 「FAISS similarity is cosine distance → lower = closer」,做法是 sims = [1 - s for s in scores](:1276–:1282)。 但这个库是用 FAISS.from_documents 默认建的,返回的是 L2 距离,不是余弦距离,取值没有上界。 L2 距离大于 1 时 1 - s 变负数,被 np.clip(sims, 0, 1) 强行压成 0。 输出的 0.68 只是一个凑出来的数。一章之内出现两个假置信度,而小节正文还提醒读者 「若打分机制本身不可靠,过度依赖置信度会误导人」(:1194)—— 它说中了自己。
  4. 配方 77 的核验一次都没生效,而且是两个错叠在一起。
    • 它把 NLI 模型(判断「前提是否蕴含假设」的模型)当普通文本分类器用, 写成 nli_model(f"{claim} </s> {text}")(:1079)—— 正确用法是把前提和假设成对传入 (或者用 zero-shot-classification 管线)。
    • 判定时找的是大写标签 "ENTAILMENT" / "CONTRADICTION"(:1113–:1117), 而模型返回的是小写 "neutral" / "entailment"两个计数器永远不可能加到 1。 输出如实地暴露了这一点:两条断言全是 neutral,support_ratio: 0.0contradiction_ratio: 0.0(:1184–:1186)。 书照样把它当成功案例收尾。
  5. 配方 75 要求模型带引用,模型没带,书没发现。 提示明写「cite them like [1], [2]」(:738), 输出的 answer 里一个方括号都没有(:794)。
  6. 配方 76 的正文承诺了交叉编码器重排,代码里没有。 小节说「生成最终答案前通常会用交叉编码器重新打分排序,这是高质量 RAG 系统的标准做法」(:818), 而配方从头到尾没有交叉编码器。三个提示生成的三段答案几乎一模一样(:991)。
  7. 配方 80 名为「上下文增强」,代码里没有任何增强。 正文说要把 来源、作者、时间戳、领域标注、表格、要点这些上下文信号嵌进回答(:1659), 代码只是取回最相似的两句话用空格拼起来(:1733)。 输出更糟:两句顺序反了、还从句子中间断开(:1771–:1775)—— 因为它用 text.split(".") 切句,而样例文本里到处是换行。
  8. 配方 81 的「检索」是拿查询的第一个词去原文里找位置。(:1852–:1862) 于是问「How does RAG reduce hallucinations?」时,它拿 how 去定位,截出的窗口不对, 最终答案是四个字:「natural language generation」(:1966)。书原样印了出来。 另外它的「渐进披露」是先把答案整个生成完,再按句 time.sleep(2) 分批打印 —— 不是流式输出。 小节自己也承认真正的生产系统该用 LangChain 的 CallbackHandler 做流式(:1779)。
  9. 配方 79 的输出和它自己的代码对不上。 代码打印的表头是 Subquery:Subquery Answers:(:1545, :1553), 输出里写的是 Decomposed steps:Answers:(:1603, :1613)。 另外它暴露了一个真问题:子答案 1 和 2 都混进了一句 It is useful when a question asks for multiple facts or steps —— 这句话里的 It 已经和它指代的东西分开了。「切碎之后代词失去指代对象」正是切分那几章埋下的坑, 在这里第一次结出恶果。 书没有把这两件事联系起来,但拆解应该联。
  10. 配方 72 用的 RetrievalQA.run() 都是 LangChain 已废弃的接口(:153, :167), 而这本书自称基于 LangChain 1.0.5。现在的写法是 create_retrieval_chain / LCEL 管道(书自己在第 1 章配方 12 用过 prompt | llm | parser)。
  11. 第 7 章开头提了一句「安全生成需要一个独立的架构层,和检索、排序、事实核查分开,用来落实隐私、安全与品牌一致性」(:9), 然后全书再没出现过。 十个配方没有一个做这件事。
  12. 配方 73 的步骤说明里写着「好的 RAG 系统必须在生成时用上对话历史,这样模型才能理解追问」(:201), 而这个配方里没有任何对话历史。 承诺与代码不符;全书也没有任何一处做多轮对话历史管理 (第 11 章有一个「上下文感知会话代理」,但那是另一回事)。

第 8 章 Prompt Engineering for RAG(15-fm-introduction.txt)· 配方 82–90

这一章的环境清单又把「LLM model: Ollama's llama3.2:3b」写了回去(:51), 而九个配方里没有一个用 Ollama —— 全是 google/flan-t5-basefacebook/bart-large-cnn

九种提示法

配方提示法提示里到底写了什么
82上下文接地「Use the following context… Do not just repeat words — provide a clear explanation.」(:180–:191)
83思维链「Think step by step: 写 2–3 条推理要点,然后在 'Final Answer:' 后写最终答案」(:336–:360)
84查询改写不是提示,是 query.replace("RAG", "Retrieval-Augmented Generation (RAG)")(:509)
85多提示集成三个提示(直接答 / 分步答 / 一句话答)+ 第四次调用「把它们合成一个最好的答案」(:693–:735)
86置信度感知用检索的余弦分数算 High/Medium/Low(:906–:918)
87带引用作答答案是硬编码的字符串(见下)
88结构化输出提示里画了一个 JSON 模板要求「strictly in the following JSON format」(:1185–:1209)
89摘要式提示facebook/bart-large-cnn,max_length=84, min_length=80(:1335–:1345)
90证据高亮关键词过滤(必含 RAG+hallucinations,或含 reduces/grounding/factual accuracy)+ 再摘要(:1450–:1490)

全书最好的一次对照:三个「置信度」,只有一个是真的

出处怎么算的是不是真信号
第 7 章配方 73min(1.0, len(answer)/300) —— 答案有多长❌ 与对错无关
第 7 章配方 781 - FAISS_L2距离,再裁到 [0,1]❌ L2 距离无上界,1-s 常为负,被裁成 0
第 8 章配方 86util.semantic_search 返回的余弦相似度取平均,>0.7 高 / >0.5 中 / 其余低✅ 是真信号

配方 86 的输出:两条证据的分数是 0.391 和 0.169,平均 0.280 → 判为「Low」(:973–:981)。 这是全书唯一一次系统诚实地说「我不太确定」。 拆解里应该把这三个放在一起对照讲。

第 8 章的缺口与错误(要补的)

  1. 配方 87 的答案是作者手打的字符串,根本没有调用任何模型。 函数里把提示拼好之后(:1087–:1097),下一行直接 answer = "RAG reduces hallucinations by ... [1]. Additionally, ... [2]."(:1099–:1102), 然后 return answerprompt 这个变量从头到尾没被用过,pip install 里也只有 sentence-transformers, 连生成模型都没装。输出(:1116)就是那个字符串。 这是全书最严重的一处「演示造假」。
  2. 配方 83 输出里的「Final Answer」不是模型产生的,是 Python 贴上去的。 代码写着:若模型输出里找不到 "Final Answer:",就 answer_text += f"\nFinal Answer: {retrieved[0]}"(:372–:374)。 而输出里模型部分只有一句照抄的定义、没有任何推理要点,「Final Answer:」那一行恰好等于 Top 1 检索结果(:427–:429)。 书把这个拼接结果当作「思维链提示成功」展示。
  3. 配方 82 的提示明写「不要只是重复词句,要给出清晰的解释」,而输出正是逐字重复上下文(:242)。 —— 顺带暴露一件正经事:flan-t5-base 这种小模型基本不服从复杂指令。书没有说。
  4. 配方 82 还暴露了一个书里从不讨论的问题:检索回来的句子是按相似度排的,不是按原文顺序排的。 输出的 Retrieved Context 里,「It reduces hallucinations…」排在定义句前面(:240), 于是「It」指的是什么,读者(和模型)都看不出来。上下文该怎么排序,全书一字未提。
  5. 配方 84 名为「查询改写提示」,里面没有提示、也没有模型。 只是一次字符串替换(:509)。 小节正文承诺的是「让模型生成不同措辞、同义词或拆解出的子查询」(:433)。
  6. 配方 85 的集成把最好的答案扔了。 三个提示的输出里,提示 2 给出了最完整的一段(:789), 提示 1 和 3 都只有一个短语。第四次调用「合成一个最好的」之后, 最终答案退化成最短的那个短语「grounding answers in retrieved documents」(:795)。 小节正文说集成应该靠「排序、多数投票或置信度打分」(:611),代码只是把三段话塞进第四个提示。
  7. 配方 88 的整段代码用的是全角引号(Import jsonmodel_name = “google/flan-t5-base”,:1153–:1213) —— 照抄必然语法错误,而且 Import 还大写了。 更要紧的是:输出里那个漂亮的 JSON 是 except 分支手工拼出来的(:1229–:1237), "answer" 字段就是模型吐的那句原始短语。模型并没有产出 JSON。 这恰好印证了第 7 章那条正确的忠告(提示保证不了合法 JSON),但书没有把两件事联系起来。
  8. 配方 89 的输出里有一句现成的幻觉,而这一章的主题正是「减少幻觉」。 摘要结尾写着:「For more information, visit the RAG website or read the book RAG: A Guide to Augmented Reality.」(:1367) —— 这本书不存在,这个网站也不存在。 原因也看得出来: min_length=80, max_length=84(:1339–:1341)逼着模型必须凑够 80 个标记, 素材不够就只能编。书原样印了出来,一个字没评。这是极好的教学素材。
  9. 配方 90 的三条「证据高亮」里有两条是编的。(:1529–:1533) 第二条「This reduces hallucination by grounding questions in evidence, rather than relying on a single source of information」 把 answers 改成了 questions;第三条「It can also reduce hallucinations by making it easier for people to remember what they have been told」 纯属胡说。 根本原因是:「高亮证据」应该是原样摘出原文句子,而这个配方是先过滤再摘要 —— 摘要就是改写,改写就把证据毁了。手段和目的相反。
  10. 配方 90 里还有一行 clean_text = full_text.replace("â€TM", "'").replace("–", "-")(:1442) —— 作者在代码里给自己的输入文件打乱码补丁。这说明连示例语料都没清干净。

第 9 章 Effective Search(16-fm-introduction.txt)· 配方 91–100

这一章是全书技术含量最高、也最能当拆解主干的一章。 它把「怎么找」的四条路摆齐了: 稠密向量、BM25 关键词、两者混合、混合再加查询扩展;然后是四种收窄手段: 语义过滤、粗排精排两段式、元数据过滤、时间过滤。

全书唯一一次真正解释算法内部:BM25(:204)

BM25 是一个基于概率打分函数的稀疏检索法。它比早期的词袋法多了两件事:

  • 词频饱和:同一个词在一篇文档里重复很多次,分数不会无限涨;
  • 逆文档频率:越罕见、越有区分力的词,权重越高。 另外它还按文档长度做归一,免得长文档天然占便宜。 在 RAG 里它常被当作基线或互补检索器 —— 简单、高效、可解释, 但只看表面的关键词重合,抓不住深层语义

这段是全书唯一一次把一个算法的内部机制讲清楚的地方,拆解里必须留住并展开。

配方清单与真实分数

配方方法关键实现输出的分数
91稠密检索normalize_embeddings=True + faiss.IndexFlatIP(:146–:156)0.3682 / 0.0452 / -0.0133
92BM25BM25Okapi(tokenized_docs)(:265)1.2374 / 0.0000 / 0.0000
93混合检索两路分数各做 min-max 归一,再 alpha*dense + (1-alpha)*bm25(:444–:452)1.0000 / 0.1570 / 0.0949
94混合 + 查询扩展写死的 15 个候选词里按余弦相似度挑 3 个拼进查询(:595–:636)扩展词:document matching / query understanding / retrieval
95语义过滤算余弦,低于 0.5 的一律丢掉(:839–:876)8 条里只有 1 条过线(0.6190)
96「层级检索」BM25 粗排 top-5 → 交叉编码器精排(:1004–:1031)+8.4985 对 -11.34 / -11.36 / -11.40 / -11.42
97切分策略按词切 30 词一块、重叠 5 词,再检索 + 重排(:1165–:1187)0.2636 / 0.0191 / 0.0029
98批量检索三个查询一次算成一个相似度矩阵(:1386)见下
99元数据检索先按元数据过滤,再在剩下的里面算相似度(:1554–:1580)0.6190 / -0.0428
100时间检索先按日期范围过滤,再算相似度(:1737–:1765)0.6190 / 0.0383

这一章把前面几章的两个大坑同时照亮了

坑一:相似度检索永远返回 k 条,哪怕全是垃圾。 这一章给出了全书最刺眼的证据:

  • 配方 91 的第 3 名分数是 -0.0133(负数),照样进了 top-3(:200);
  • 配方 92 的第 2、3 名分数是 0.0000 和 0.0000 —— 查询词一个都没出现在那两篇里,照样返回(:308–:310);
  • 配方 98 问「RAG 怎么减少幻觉」,第 2 名是「Traveling to new countries helps you learn about culture and history」,相似度 0.0383(:1426);
  • 配方 99 加了元数据过滤之后,第 2 名的相似度是 -0.0428(:1626)。

而解药就在同一章的配方 95:设一条相关性阈值,低于 0.5 的一律丢掉。 八条文档里只有一条过线。书从来没有把这两件事联系起来说 —— 这是拆解必须做的连接。

坑二:重排到底值多少钱,这一章给出了全书最有说服力的数字。 配方 96 里 BM25 粗排的 top-5 混进了「烹饪食谱」和「出国旅行」两条完全无关的文档 (因为它们的 BM25 分数都是 0,排序其实是随机的); 交叉编码器一打分:相关的那条 +8.4985,其余四条全在 -11.3 到 -11.4 之间(:1075–:1083)。 差了将近 20 分,而且四条无关文档挤在一起,界限极其干净。 这是全书最适合当主走查的一段。

前后章的两处「同一件事,做对了 vs 做错了」

  1. 归一化。 第 4 章配方 50 声称 L2 归一化能「抗噪声」(错); 第 9 章配方 91 才用对了 —— normalize_embeddings=TrueIndexFlatIP(内积索引), 归一之后内积就等于余弦相似度(:146–:156)。这才是归一化真正的用途。
  2. 混合检索的融合。 第 5 章配方 61 的「加权」两路权重都是 0.5、排序等于没排(假); 第 9 章配方 93 才是真的:两路分数各自 min-max 归一到 [0,1],再按 alpha 加权求和(:444–:452), alpha=0 纯 BM25、alpha=1 纯稠密。这是一个可讲透的机制。 (副作用也要讲:min-max 归一会强行把最高分变成 1.0、最低分变成 0.0, 所以混合结果的第一名恰好是 1.0000 —— 这个 1.0 不代表「完全匹配」,只代表「这一批里最高」。)

第 9 章的缺口与错误(要补的)

  1. 配方 93 印出来的代码跑不出印出来的结果。 演示块里 Dense 和 Hybrid 那两行 for 循环 被注释掉了(:482、:490),而循环体里的 print 还留着并且缩进错位。 照这段代码运行,Dense 和 Hybrid 两节各只会重复打印 BM25 循环的最后一条。
  2. 配方 94 的 BM25 输出和配方 93 一字不差(1.2374 / 0.0000 / 0.0000), 可它喂进去的是加了 retrieval 这个词的扩展查询 —— 而 retrieval 在多篇文档里都出现。 BM25 分数不可能一点不变。 这一节的输出是抄过来的。
  3. 配方 94 暴露了一个真问题,但书没有点破:扩展之后分数普遍升高,不等于检索变好了。 稠密检索的 top-3 分数从 0.3682 / 0.0452 / -0.0133 变成 0.5879 / 0.4707 / 0.3435(:755–:759)。 原因是扩展词把查询拉向了「检索」这个泛化话题,于是每一篇讲检索的文档都变得更像分数涨了,区分度反而降了。
  4. 配方 96 叫「层级检索」,做的却是「粗排 + 精排」。 小节自己的定义写得很清楚:层级检索是按粒度分层(文档 → 章节 → 段落),先定位大单元再往下钻(:927)。 代码里只有一层语料、一次 BM25、一次重排 —— 和第 6 章配方 65 是同一件事。
  5. 配方 95 的注释把归一化的作用说错了,和第 4 章配方 50 是同一个误解: docstring 写「normalize embeddings to avoid negative/weird values」(:845), 但归一化并不能消除负的余弦值 —— 它自己的输出里就有 -0.0185、-0.0428(:903, :913)。 另外它做了两次归一(:855、:859),而 util.cos_sim 本身就会归一,这两行是多余的。
  6. 配方 95 开始悄悄换了嵌入模型。 配方 91–94 用 all-MiniLM-L6-v2(384 维), 配方 95–100 换成 all-mpnet-base-v2(768 维),书没有一个字交代,也没解释换的理由。
  7. 配方 97 的输出恰好证明了固定长度切分会切坏句子,书却不评一句。 第 1 块结尾是「… BM25 is」(截断),第 3 块开头是「for semantic similarity. BM25 is …」(从半句开始)(:1270–:1274)。 重叠 5 个词根本兜不住。 这正是第 3 章该讲而没讲透的东西,在这里现了原形。
  8. 配方 99 和第 1 章配方 13 是两种相反的过滤顺序,书从不对比。
    • 第 1 章配方 13:similarity_search(query, k=5, filter={...}) —— 先检索,再过滤, 所以 k=5 但只返回 2 条;
    • 第 9 章配方 99:先按元数据筛出子集,再在子集里算相似度(:1554–:1580), 所以一定能凑够 k 条(只要子集够大)。 两种顺序的取舍(会不会漏、会不会凑数)是元数据过滤最该讲的一点,书一句没说。
  9. 配方 100 的正文承诺「优先较新的文档」,代码只做了硬性日期区间过滤。 小节说要「按时间相关性给结果排优先级」「偏好较新的文档,同时仍允许取用旧材料作历史背景」(:1630), 而 time_aware_retrieve 里没有任何按时间加权的成分(:1720–:1779)—— 落在区间内的文档一视同仁,落在区间外的直接丢弃。

第 10 章 Implementing RAG with Chains(17-fm-introduction.txt)· 配方 101–111

「链(chain)」在这一章的意思是:把检索、推理、生成拆成可拼装的步骤,按顺序串起来。 书没有正面定义过这个词,只在开篇说链「提供了一种把检索、推理、生成连起来的结构化方式」(:7)。

配方链名它到底做了什么
101知识检索问答链检索 → 拼上下文 → 套模板 → 生成(:156–:172)
102会话式 RAG 链langchain_classic.chains.ConversationalRetrievalChain + 手工维护 chat_history(:226, :280–:298)
103摘要链facebook/bart-large-cnn,长度按输入动态算(:432–:436)
104带引用作答链加了相似度阈值:sim = 1/(1+L2距离),低于 0.5 丢掉(:567–:583)
105塞文档链把元数据拼进正文再嵌入(:732–:738)
106工具增强链查询里有 +-*/() 就走计算器,否则走检索(:963–:971)
107来源可追溯链LCEL 写法 (answer_prompt | llm).invoke({...}),顺手带出 metadata["source"](:1163–:1195)
108稠密 + 稀疏混合链见下 —— 两路都是关键词匹配,没有向量
109元数据自查询链见下 —— 没有自查询
110重排链FAISS top-5 → 交叉编码器重排(:1662–:1682)
111退一步链先把问题改写得更宽泛,拿宽泛问题去检索,再用原问题作答(:1798–:1866)

这一章里三段真正值得学的东西

  1. 配方 105:把元数据拼进正文再嵌入(:732–:738)

    page_content = f"author: Alice category: health year: 2022. Intermittent fasting improves insulin sensitivity."

    这样「What did Alice write about health?」就能靠语义命中作者名, 不必事先知道要按 author=Alice 过滤。这是「过滤」之外的另一条路,书没有讲透但代码演示了。

  2. 配方 103 与第 8 章配方 89 是同一件事的正反面。 两个配方都用 facebook/bart-large-cnn 做摘要:

    • 第 8 章配方 89 写死 min_length=80, max_length=84模型被逼着凑字数,编出了一本不存在的书;
    • 第 10 章配方 103 按输入长度动态算:max_len = min(100, max(30, 词数*0.6))min_len = max_len*0.4, 51 个词的输入算出 min=12, max=30,加上 num_beams=4length_penalty=1.8do_sample=False → 摘要忠实,就是原文第一句(:478–:486)。 「摘要长度设死会逼出幻觉」—— 这是全书自带对照组的一个结论,拆解里必须讲。
  3. 配方 110 的重排结果里藏着全书最重要的一条暗线。 查询是「What are the benefits of intermittent fasting?」,五条候选的重排分数:

    分数文本
    8.5342Intermittent fasting improves insulin sensitivity.
    6.0997Exercise combined with intermittent fasting can boost fat loss.
    3.8877Fasting can reduce inflammation and support cellular repair.
    1.2349Drinking water during fasting helps with hydration.
    -6.5435It may help with weight loss by reducing calorie intake.

    减重明明是间歇性禁食最有名的好处,却排在最后一名、还是负分。 原因就一个字: 这句话以「It」开头,而「It」指的是什么不在这个块里 —— 交叉编码器无从判断它在说什么。 同一个病在书里出现了三次:第 7 章配方 79(Jaccard 检索把带 It 的句子乱配)、 第 9 章配方 97(固定长度切分把句子拦腰截断)、这里。 「第 3 章怎么切,第 7、9、10 章就怎么疼」—— 这是全书最好的一条贯穿线索,但书自己从没连起来。

第 10 章的缺口与错误(要补的)

  1. 配方 108 的「稠密检索」是一部字典。 DenseRetriever 里没有嵌入、没有向量、没有模型,只有一张写死的关键词表 topic_keywords = {"health": ["health","medical","wellness","intermittent fasting"], …}(:1298–:1330), 判断方式是 if any(k.lower() in query_lower for k in keywords) —— 纯字符串包含。 于是所谓「稠密 + 稀疏混合」实际上是关键词匹配 + 关键词匹配。 而且演示是做局的:history 的关键词表里赫然写着 "before 1900", 而测试查询正是「History events before 1900」(:1304, :1428)。 pip install 里列的 faiss-cputorch 一个都没被 import。
  2. 配方 109 里没有自查询。 「自查询(self-query)」的定义是 让大模型把自然语言问题翻译成结构化过滤条件;而这里的类别是手写在测试用例旁边的 (("Tell me about health", "health"),:1545)。 更要命的是 filter_by_category 调用的是 similarity_search(query="", filter=..., k=3) —— 查询字符串是空的,语义那一半根本没参与。 代码还有三处坏了:SentenceTransformerEmbeddings 用了但没 import(:1521); ChatOpenAIRetrievalQA import 了但没用(:1494–:1496)。
  3. 配方 111 的「退一步」没有退成。 提示里给了两个示例,示范的是把问题放宽 (「谁发现了青霉素?」→「医学史上有哪些重要发现?」); 而模型对「Where is the Eiffel Tower located?」给出的「退一步问题」是 「What is the location of the Eiffel Tower?」(:1906)—— 这是同义改写,不是放宽。 书照样当成功案例展示。 另外「退一步提问(step-back prompting)」出自 Google DeepMind 2023 年的论文 (Zheng 等,「Take a Step Back」),书里一个字没交代来历。这是要补外部来源的一处。
  4. 配方 104 的输出又是 Python 拼的,不是模型生成的。 if "(" not in out or len(out.splitlines()) < len(retrieved): 之后 out = "\n".join([f"- {d['content']} ({d['source']})" …])(:624–:626)。 —— 和第 8 章配方 83、第 8 章配方 88 是同一套把戏,全书第三次
  5. 配方 106 的「工具增强」里没有大模型参与决策。 llm 建出来了(:900)但从头到尾没被调用;选哪个工具靠的是一句 if any(op in query for op in ["+","-","*","/","**","(",")"])(:965)。 真正的工具调用是模型自己决定调哪个工具,这里是 if 语句。 而且计算器用的是 eval()(:918)—— 虽然清空了 __builtins__,书里没有一句安全提示; 它的表达式提取是 re.findall(r"[0-9\.\+\-\*\/\(\)]+", query) 再全部拼接, 遇到「What is 2 + 2 in 2024?」会拼出 2+22024能跑通只是因为例句挑得巧。
  6. 配方 101 和 105 用的是私有接口 retriever._get_relevant_documents(query, run_manager=None)(:156, :792), 还带着 # type: ignore 注释。公开接口是 retriever.invoke(query) —— 同一章的配方 107 和 110 用的正是 .invoke()(:1167, :1688)。同一本书里两套写法打架。 配方 101 的注释还写着「In LangChain 1.0.5, use .generate()」(:168),这条指引是错的。
  7. 配方 102 是全书唯一一次做多轮对话,但最关键的机制没讲。 ConversationalRetrievalChain 之所以能答对追问「And what method does it use for retrieval?」, 靠的是它内部先把追问 + 对话历史合成一个能独立成立的问题,再拿这个问题去检索。 书只说了一句「chain 内部自己管对话历史,所以不需要自定义提示」(:276–:278), 没有解释这个改写步骤 —— 而那正是会话式 RAG 的全部难点。
  8. 配方 103 名叫「摘要链」,里面既没有链也没有检索 —— 纯 transformers,连 LangChain 都没 import。
  9. 配方 110 用了裸 except:(:1639),会吞掉一切异常。 (顺带:FAISS.load_local 在新版里不传 allow_dangerous_deserialization=True 一定抛错, 所以这段代码实际上每次都走 except 分支重建索引 —— 「已有索引就加载」这个卖点从未生效。)

第 11 章 Agentic RAG with Dynamic Retrieval(18-fm-introduction.txt)· 配方 112–121

这是全书自称的落点,也是全书最弱的一章。 十个「代理」里,没有一个有循环

先说清楚一件事:这一章又换了模型,而且换到了最小的一个

  • 第 1–6 章:Ollama llama3.2:3b(约 30 亿参数)
  • 第 7–10 章:google/flan-t5-base(约 2.5 亿参数)
  • 第 11 章:t5-small(约 6000 万参数),配方 112–117 全用它;配方 120 用 flan-t5-small

t5-small 不是指令微调模型。 原版 T5 只认它预训练时用过的任务前缀 (translate English to German:summarize: 之类),给它一段自由格式的指令,它只会照抄输入。 本章几乎所有输出的怪相都源于此。环境清单里写的仍然是「Ollama's llama3.2:3b」(:53)。

十个「代理」

配方名字实际实现
112自查询代理让 t5-small 把问题转成 JSON(query + filters)——失败,走了兜底分支
113任务导向工具代理if 查询里有数字 → 计算器 else 检索(:343–:366)
114上下文感知会话代理维护 conversation_history 列表 —— 但从不读它
115动态重排代理重排 = 数查询和文档的共同词个数(:771)
116自适应摘要代理文档库里只有一篇文档(:967–:972)
117思维链检索代理提示里写「Think step by step」,模型把上下文原样吐回来
118混合检索代理稠密 FAISS + TF-IDF(不是 BM25),结果去重后取前 k
119时间感知检索代理k=1 取一条,然后对这一条按日期排序(:1523–:1529)
120流式检索代理先整段生成完,再 for word in output.split(): print(...)(:1679–:1683)
121查询扩展代理WordNet 同义词扩展 + 多次检索取最优分(:1763–:1843)

配方 121 的输出是全书最好的一份反面教材,而书一个字没评

WordNet 同义词扩展把「Tell me about health」扩成了(:1886):

'Tell me about wellness' ← 有用
'Tell me almost health' ← about → almost,意思变了
'Tell me approximately health' ← about → approximately
'Tell me astir health' ← about → astir(苏格兰方言「活动着的」)
'Tell Pine Tree State about health' ← me → Pine Tree State
'Tell Maine about health' ← me → Maine

「me」变成了「Maine」和「Pine Tree State」,因为 WordNet 里 ME 是缅因州的邮政缩写、 而「Pine Tree State」是缅因州的别称,三者在同一个同义词集里。 「History before 1900」被扩成「History ahead 1900」「History in front 1900」(:1910)—— before 变成了「在前面」,时间方向整个反了。

这就是「朴素同义词扩展为什么在生产里不能用」的现成证据。 (这个配方之所以还能给出正确结果,是因为它把原查询也留在扩展列表里, 并且跨所有扩展查询取每篇文档的最优分(:1835)—— 这个兜底设计值得单独讲。)

第 11 章的缺口与错误(要补的)

  1. 这一章没有一个「代理」。 按第 7 章结语给出的定义(:1974 of ch7), 代理式 RAG 的核心是循环:评估证据是否够 → 不够就改写查询重检索 → 直到满足停止条件。 本章十个配方全部是单趟直线,分支最多的一个是 if 有数字 else没有一次自我评估,没有一次重试,没有一个停止条件。
  2. 配方 112 的自查询失败了,书当成功展示。 输出里 Parsed Query: {'query': <原问题原文>, 'filters': {}}(:204) —— 这正是 except 兜底分支写死的返回值(:167–:168)。 t5-small 根本没吐出 JSON,过滤条件是空的。 检索结果里因此混进了「The capital of France is Paris.」(:206), 而问题问的是健康与胰岛素敏感性。
  3. 配方 114 名为「上下文感知」,而 conversation_history 只写不读。 代码把每轮问答 append 进列表(:601–:603),但这个列表从未进入任何提示。 后果就在输出里:问「What is the capital of France?」,答案是「France」(:669)—— 而同样的文档、同样的问题,配方 115 答对了「The capital of France is Paris.」(:891)。 差别在于配方 114 调了两次模型(先让模型把文档「答」一遍,再把这个答案当 Context 喂进第二次), 两次 t5-small 叠加,信息被磨没了。
  4. 配方 115 的「动态重排」是数共同词个数,连停用词都算。 score = len(set(文档词) & set(查询词))(:771), 「What is the capital of France?」得 4 分,靠的是 is / the / of / France。 小节正文承诺的是「语义对齐、查询意图、回答质量」(:689)。
  5. 配方 116 的「自适应」永远不会触发。 文档库里只有一篇文档, 而分支条件是 if len(retrieved_docs) == 1 … return top_doc.page_content(:1025–:1027), 摘要那条路是死代码。
  6. 配方 117 的输出里没有任何推理,只有原文回声。 第一个查询的「答案」开头就是「Context: RAG combines retrieval and generation…」(:1264)—— 模型把提示里的 Context: 标签一起抄了出来。三个查询无一例外。 这一章讲的思维链,一次都没发生。
  7. 配方 118 的「稀疏检索」是 TF-IDF,不是 BM25。 代码用 sklearnTfidfVectorizer(:1353), 而小节正文明写 BM25(:1284),代码注释写「BM25 / TF-IDF」(:1351)。 这两者不是一回事 —— BM25 多了词频饱和与文档长度归一(第 9 章自己讲过)。 合并方式是 dict.fromkeys(dense + sparse)[:k](:1377–:1379),没有打分、没有融合,稠密结果永远占前排。
  8. 配方 119 的时间感知是空操作。 检索器写死 k=top_k 而调用时 top_k=1(:1549), 拿到一条结果之后 results.sort(key=日期, reverse=True)(:1529)——对一个元素排序等于什么都没做。
  9. 配方 120 不是流式。pipe(prompt) 把整段答案生成完,再逐词打印, 代码注释自己写着 # simulate streaming(:1679)。真正的流式要用 TextIteratorStreamer 或 LangChain 的 .stream()和第 7 章配方 81 是同一个假动作。
  10. 本章有六个配方的 import 行被粘连在一起,直接语法错误: import reimport warnings(:251、:460、:953)、 from transformers.pipelines import pipelineimport re(:721)、 from langchain_huggingface import HuggingFaceEmbeddingsimport warnings(:1318)、 from langchain_huggingface import HuggingFaceEmbeddingsfrom langchain_community.vectorstores import FAISSimport warnings(:1488)。 另有四处正则里用了全角引号 re.sub(r”…”, “”, query)(:306、:507、:793、:992)。
  11. is_math_task 只要查询里有任何一个数字就判定为数学题(:347、:519)。 「What happened in 2008?」会被送进计算器。而计算器用的是 eval(), 配方 118 里的那一版__builtins__ 都没清(:1391)。
  12. 配方 114 的回答清洗里出现了 re.sub(r"(Answer concisely.*|User:.*|**Benutzer:**)", ...)(:597) —— Benutzer 是德语的「用户」。这段清洗逻辑显然是从别处抄来的。

通读之后的总账(写大纲的人从这里开始看)

一、这本书到底是什么

它不是一本讲 RAG 原理的书,是一本 121 个可运行代码片段的配方册。

  • 11 章,原文约 50.5 万字符,其中大约七成是 Python 代码和程序输出;
  • 讲解性散文极薄,而且绝大部分只是对下一段代码的复述;
  • 全程本地模型、零云端 API:嵌入用 all-MiniLM-L6-v2(384 维)或 all-mpnet-base-v2(768 维), 生成用 Ollama llama3.2:3b → flan-t5-baset5-small(逐章变小,书从不交代);
  • 环境:Windows · Python ≥3.13.3 · LangChain 1.0.5(这是它最有价值的时效性: 书里出现了 langchain_classiclangchain_chromalangchain_huggingfacelangchain_unstructured 这些 1.0 之后的新包名,也仍混着 langchain.vectorstores 这类废弃路径)。

二、全书主线(一条,不是章节摘要的拼接)

① 大模型只会照自己参数里的东西作答 → 事实会过期、领域知识没有
↓ 于是
② 把外部文档「找出来,塞进提示里」 —— 这就是 RAG(第 1 章一次走通六步)
↓ 可是文档格式五花八门
③ 加载器把十几种格式收敛成同一个 Document(正文 + 元数据)(第 2 章)
↓ 可是整篇文档太大,塞不进提示、也嵌不准
④ 必须切块;结构式切法看排版标记,语义式切法看意思(第 3 章)
↓ 切完的块怎么才能「按意思」被找到
⑤ 把每块变成一串数(嵌入),距离近就意思近(第 4 章)
↓ 几百万个向量放哪儿、怎么快速比
⑥ 向量库 + 索引;顺便带上元数据用来过滤(第 5 章)
↓ 数据一大,精确比对就慢;而且找回来的东西常常不对
⑦ 近似索引、缓存、分区、降维换速度;交叉编码器重排换精度(第 6 章)
↓ 「找得对」之后,「用得对」是另一件事
⑧ 检索结果怎么变成答案:直接答 / 结构化 / 带引用 / 带置信度 / 先拆解再答(第 7 章)
↓ 而这一切都由提示词决定
⑨ 提示法:上下文接地、思维链、查询改写、多提示集成、要求带引用(第 8 章)
↓ 但再好的提示救不了错的检索
⑩ 回头把「怎么找」做扎实:稠密 / BM25 / 混合 / 扩展 / 阈值过滤 / 粗排精排(第 9 章)
↓ 这些步骤要能拼装、能复用
⑪ 用「链」把步骤串起来(第 10 章)
↓ 链是写死的顺序,遇到复杂问题不会变通
⑫ 让模型自己决定「要不要再查一次、查什么、怎么查」—— 代理式 RAG(第 11 章,自称的落点)

但要如实说清楚两件事:

  • 落点没落住。 第 11 章的十个「代理」全是单趟直线,没有循环、没有自我评估、没有停止条件。 全书对代理式 RAG 最完整的定义,反而藏在第 7 章的结语里 (text/14-fm-introduction.txt:1974:评估证据是否充分 → 不够就改写查询或发子查询重新检索 → 直到满足置信度阈值 / 最大轮数 / 预算上限)。
  • 真正的重心在第 6 章和第 9 章。 这两章是全书唯一有真机制、真数字、真对照的部分。

三、伏笔与暗线(不通读看不出来的)

  1. 「切分」这件事在第 3 章埋雷,第 7、9、10 章连炸三次。

    • 第 3 章:配方 27 的 n-gram 切分静默丢掉句尾;配方 28 的语义切分静默丢掉 9 句里的 5 句;
    • 第 7 章配方 79:检索出的句子是「It is useful when a question asks for multiple facts or steps」—— It 指的是什么,已经不在这个块里了;
    • 第 9 章配方 97:固定 30 词切分把句子拦腰截断,块 1 结尾是「… BM25 is」,块 3 开头是「for semantic similarity.」;
    • 第 10 章配方 110:「It may help with weight loss」在交叉编码器那里拿到 -6.5435 分,五条里垫底, 而减重明明是问题问的那个好处。 「块里出现了没有指代对象的代词」= 这一块永远检索不准。书从没把这四处连起来。
  2. 「相似度检索永远返回 k 条」这个坑贯穿第 1 章到第 11 章,而解药在第 9 章配方 95。 证据:第 5 章配方 53 返回不相干的 Transformers 文档;第 9 章配方 91 第 3 名分数是负的; 配方 92 第 2、3 名分数是 0.0000;配方 98 给「RAG 怎么减少幻觉」返回「出国旅行」。 解药是相关性阈值(配方 95:低于 0.5 一律丢掉;第 10 章配方 104 也用了一次)。 书从来没有把「病」和「药」放在一起说过。

  3. 「置信度」在书里出现三次,只有一次是真的。 第 7 章配方 73(答案长度 ÷ 300)假、配方 78(1 − L2 距离,会被裁成 0)假、 第 8 章配方 86(余弦相似度取平均,判为 Low)真。

  4. 「摘要长度设死会逼出幻觉」是全书自带对照组的一个结论。 第 8 章配方 89 写死 min_length=80 → 编出了不存在的书名和网站; 第 10 章配方 103 按输入长度动态算 → 摘要忠实。

  5. 「归一化」被讲错一次、用对一次。 第 4 章配方 50 说它能「抗噪声」(错);第 9 章配方 91 用 normalize_embeddings=True + IndexFlatIP, 归一之后内积等于余弦相似度(这才是它的用途)。

  6. 「混合检索」这个词在书里有两个意思。 第 5 章配方 54 把「语义 + 元数据过滤」叫 hybrid(代码注释就写着 # Hybrid filtering); 而业界通称的 hybrid search 是「稠密 + 稀疏」,书在第 5 章配方 61、第 9 章配方 93、第 11 章配方 118 讲的是后者。

  7. 一个反复出现的造假手法:输出是 Python 拼的,不是模型生成的。 第 8 章配方 83(Final Answer: 是把 top-1 检索结果贴上去的)、 第 8 章配方 87(答案是硬编码字符串,提示变量从未被使用)、 第 8 章配方 88(漂亮的 JSON 出自 except 分支)、 第 10 章配方 104(带引用的列表是 if 兜底拼的)。四处。

四、建议怎么切章(按新词密度切,不按原书章序)

原书 11 章的顺序对讲解不利:第 7、8 章(生成 + 提示)插在两段检索之间,把第 6 章和第 9 章劈开了; 而第 9 章才是「怎么找」最扎实的一章,却排在第 8 章之后。建议九章,顺序重排:

拟定章讲什么对应原书主走查建议
01 一句话进去,一句话出来大模型为什么需要外挂知识;RAG 六步一次走通;Document = 正文 + 元数据 这个贯穿全书的数据结构第 1 章、第 2 章「核心部件」一段 275 字符的 RAG 定义句,RecursiveCharacterTextSplitter(300/50) 切成 4 块(276/282/166 字符)
02 把十几种格式收成一种东西加载器接口;一个 Document 对应什么(CSV 一行 / Excel 一表 / JSON 一整个数组);元数据在这一步就要挂上;去停用词是有害的(必须纠正)第 2 章CSV 四行员工数据 → 四个 Document,每个渲染成「Name: Anil Sharma\nDepartment: …」
03 切成多大、在哪儿切结构式 vs 语义式的二分;RecursiveCharacterTextSplitter 的递归到底递归什么(书没讲,必须补);重叠防的是什么;切分会静默丢数据(配方 27、28 的证据);块大小 300–800 标记第 3 章 + 第 6 章配方 63、70 + 第 9 章配方 97同一段文字 200/500/1000 字符 → 6/3/1 块;再拿配方 27 的输出证明尾巴被扔了
04 一句话怎么变成一串数标记 → 每个标记一个向量 → mean pooling 压成句向量(第 4 章配方 42 的代码把这一步露出来了);384 维从哪来;归一化真正的用途(内积 = 余弦);嵌入模型必须首尾一致第 4 章 + 第 9 章配方 91三句话 → 三个 384 维向量,首向量前 5 维 [-0.0874, -0.0331, …]
05 向量放哪儿、怎么比FAISS / Chroma;落盘与重载;allow_dangerous_deserialization 是什么(必须补);距离越小越近;top-k 永远返回 k 条(证据 + 阈值这一味药)第 5 章 + 第 1 章配方 11 + 第 9 章配方 95FAISS 带分数检索 0.9408 / 1.3505 / 2.0506,再摆出配方 91 的 -0.0133 和配方 92 的 0.0000
06 找得准:两条路 + 一次重排稠密 vs BM25(BM25 的三个机制:词频饱和 / 逆文档频率 / 长度归一);混合怎么融合(min-max + alpha,以及业界的 RRF);双编码器 vs 交叉编码器;粗排精排两段式第 9 章 + 第 6 章配方 65第 9 章配方 96:BM25 粗排把「烹饪食谱」「出国旅行」混进 top-5,交叉编码器一打分 +8.4985 对 -11.34 / -11.36 / -11.40 / -11.42
07 找得快:拿什么换什么近似最近邻到底近似在哪(书讲错了,必须补 IVF / HNSW / PQ);缓存(精确匹配的局限 + 语义缓存);按主题分区的召回代价;降维的样本数约束;冷热分层第 6 章384 维 → SVD 目标 128 维 → 实际只得到 5 维(因为只有 5 个样本)
08 找回来之后怎么用「stuff」链的边界;结构化输出为什么不能只靠提示;引用怎么带;三个置信度里只有一个是真的;摘要长度设死会逼出幻觉;上下文的排序问题第 7 章 + 第 8 章 + 第 10 章配方 103三个置信度的对照表 + 第 8 章配方 89 那句编出来的「RAG: A Guide to Augmented Reality」
09 从写死的链到会变通的代理链 = 可拼装的步骤(LCEL);会话式 RAG 的追问改写(书没讲,必须补);把元数据拼进正文再嵌入;代理式 RAG 的真定义(取自第 7 章结语);并说清楚第 11 章没做到;WordNet 扩展的反面教材第 10 章 + 第 11 章 + 第 7 章结语配方 121 的输出:「Tell me about health」→「Tell Maine about health」

为什么这么切(三条理由):

  • 原书第 6 章和第 9 章讲的是同一件事的两半(快 / 准),中间隔着两章生成,读者会断线 → 合并为拟定 06、07;
  • 原书第 7、8 章高度重叠(十个生成花样 + 九个提示法,提示法基本是生成花样的重述)→ 合并为拟定 08;
  • 原书第 3 章的切分坑要到第 9、10 章才结果,拟定 03 一次讲完并前置证据。

五、必须补的外部缺口(书里没有、或书里讲错的)

先翻书架,再上网。下面已经把书架上的位置标出来了。

#缺口为什么必须补去哪儿找
1ANN 索引到底近似在哪(IVF 倒排、HNSW 图、PQ 乘积量化)书第 6 章配方 62 挂着 ANN 的名字,演示的是精确暴力搜索,「拿召回换速度」这个核心一字没讲../ai-agent-reference/docs/leann/03-backends-registry.md02-search-recompute.md;../ai-agent-reference/docs/agentmemory/02-retrieval.md
2Reciprocal Rank Fusion(RRF)书第 5 章配方 61 的「加权融合」是假的、第 9 章配方 93 是简单加权;业界标准做法是 RRF,书从未提及../ai-agent-reference/docs/txtai/04-search-and-fusion.md;../ai-agent-reference/docs/agentset/02-engine-retrieval.md;../ai-agent-reference/docs/simplemem/02-hybrid-retrieval.md
3双编码器 / 交叉编码器 / 稀疏与多向量的完整分工书讲对了交叉编码器(第 6 章配方 65),但没讲它的训练方式、分数是无界 logit、以及 ColBERT 这类多向量路线../ai-frontier-reference/docs/sentence-transformers/04-cross-encoder.md05-sparse-and-multi-vector.md
4mean pooling 与句向量是怎么训出来的书第 4 章配方 42 把 mean pooling 的代码摆出来却不解释,而这是「384 维从哪来」的唯一答案../ai-frontier-reference/docs/sentence-transformers/01-module-pipeline.md02-contrastive-training.md
5RAG 怎么评测全书只有第 5 章配方 60 一次评测(Accuracy@3 = 0.50),判据是关键词子串匹配;第 6 章配方 67 的网格搜参 27 组全 1.00,因为只比对期望答案的第一个词。「怎么知道自己的 RAG 好不好」这个最要紧的问题,书等于没答../ai-frontier-reference/docs/ragas/01-metrics-engine.md(忠实度 / 答案相关性 / 上下文精度召回);同库 docs/hands-on-rag-for-production-design/11-evaluation.md;docs/essential-graphrag/11-evaluation.md
6allow_dangerous_deserialization=True 是什么书出现四次、零解释;它的含义是「我知道加载这个索引会 pickle 反序列化、可能执行任意代码」。读者照抄去加载别人给的索引会中招通用知识 + LangChain 官方文档(FAISS.load_local);必要时 ../ai-frontier-reference/aiRef/repos/langchain/
7去停用词 + 小写化会毁掉 Transformer 嵌入书第 2 章配方 22 把它当最佳实践;这是词袋时代的习惯,对句向量模型是有害的../ai-frontier-reference/docs/sentence-transformers/;或本库 docs/hands-on-rag-for-production-design/04-embeddings.md
8step-back prompting 的来历第 10 章配方 111 用了这个技术却不提出处。它出自 Google DeepMind 2023 年论文(Zheng 等,「Take a Step Back: Evoking Reasoning via Abstraction in Large Language Models」,arXiv 2310.06117)—— 写进正文前必须打开原文核对作者与编号WebFetch arxiv(一次只抓一个,失败两次换源);或 grep -ril "step.back" ../ai-*-reference/docs/
9会话式 RAG 的追问改写(condense question)第 10 章配方 102 靠 ConversationalRetrievalChain 答对了追问,而这一步(把追问 + 历史合成一个独立问题再检索)正是难点,书只字未提同库 docs/learning-langchain/04-retrieval-generation-rag.md;../ai-frontier-reference/docs/langchain/03-runnable-lcel.md
10真正的 self-query 检索器第 10 章配方 109、第 11 章配方 112 都叫「自查询」,都没实现;LangChain 有现成的 SelfQueryRetriever,用 LLM 把自然语言译成结构化过滤条件同库 docs/learning-langchain/;../ai-frontier-reference/aiRef/repos/langchain/
11真正的流式输出第 7 章配方 81、第 11 章配方 120 都是「先生成完再逐词打印」,代码注释自己写着 simulate streaming../ai-frontier-reference/docs/langchain/03-runnable-lcel.md(.stream());transformers 的 TextIteratorStreamer
12语义缓存第 6 章配方 64 的缓存按查询字串精确匹配,「what is ML?」不会命中「What is machine learning?」grep -ril "semantic cache|GPTCache" ../ai-*-reference/docs/
13书出版时的时代坐标2026 年 BPB 出版,基于 LangChain 1.0.5;需要交代 LangChain 1.0 带来的包名重组(langchain_classic 等)以及书里仍在用的废弃路径../ai-frontier-reference/docs/langchain/index.md

另外要在总纲里明确的定位对比(同库已有的四本 RAG 书):

  • docs/hands-on-rag-for-production-design/(14 章,已拆)—— 讲生产架构与取舍,概念厚;
  • docs/essential-graphrag/(12 章,已拆)—— 讲知识图谱路线;
  • docs/rag-ready-patterns-for-data-platforms/(8 章,已拆)—— 讲数据平台侧;
  • docs/rag-python-cookbook/(Polzer / O'Reilly,只拆了索引)—— 只讲入库流水线,概念解释厚、配方少。
  • 本书的独特位置:唯一一本「121 个可运行片段、贴着 LangChain 1.0.5 的 API」的书, 也是唯一一本能拿自己的失败输出当教材的书。

六、写作时的红线提醒(这本书特别容易踩)

  1. 不能照散文重写。 这本书的散文是对代码的复述,照写必然写成目录(红线二)。 要从代码和输出里把机制读出来,并且给每章配主走查 —— 好在这本书的真实输出极多,素材充足。
  2. 不能把书里的错误照单全收。 上面列的每一处「书里讲错的」,拆解里都要先讲对的,再说明书里是怎么写的, 并按「区分谁在说话」的规矩标清楚哪句是作者的、哪句是我们的判断。
  3. 代码里的全角引号、粘连的 import、错位的缩进,不要原样搬进拆解; 要提的话,当成「这本书的可用性缺陷」在边界与局限那一节集中说一次,不要每章都提。
  4. 不要引导航章(01 版权 / 06 书名页 / 07 目录 / 19 索引)。
  5. 引用格式:「CHAPTER N <章名>」第 X 段(text/NN-fm-introduction.txt:X,搜「原文短语」)注意 chapters.json 把正文章名全标成了 introduction,写章名时要用正文第 3 行的真章名。