检索与摘要 — RAG,给模型补课的两条路
这一章讲三件事: 模型不知道的事,怎么临时补给它(检索); 东西太多装不下,怎么压小(摘要);以及塞料时最容易踩的一个坑—— 你给它的每条信息,它都觉得「必有深意」。 这是原书第 5 章的下半章,也是今天绝大多数 LLM 应用的主干技术。
1. 这一章讲什么
第 06 章说了动态上下文「从哪来」。最大的一个来源,大到值得单独一章: 从你自己那堆资料里找出来。
主走查是书里那个 60 行的最小 RAG 程序:11 条用户旧书评、 一本候选书《The Beach》、一次检索、一份提示词、一个分数。 我们会把这 60 行从头走到尾,每一步旁边都有真字串。
2. 顶层全景:先检索,后生成
模型两手一摊的时刻:问它训练集里没有的事——公司内网文档、上周的新闻、 你昨晚的购买记录。理想情况它说不知道;不理想,它面不改色地编。
RAG——检索增强(Retrieval-Augmented Generation)生成,2020 年 5 月一篇论文 给它定的名:先从你的资料库里检索出相关片段,再把这些片段塞进提示词, 让模型「开卷」作答1。
离线(准备): 文档 → 切成小块 → 每块算一个「位置」→ 存进库里
在线(请求): 用户问题 → 算同一个坐标系里的位置 → 找出离它最近的几块
→ 塞进提示词 → 模型看着这些料作答
图说:RAG 的两半。检索的学问全在「位置怎么算」——
两派算法,是本章 3.2 与 3.3 的内容。
塞料之前,先立一条警告,这是全章的红线:
契诃夫之枪谬误
剧作家契诃夫有句名言:第一幕墙上挂了把枪,后面就必须打响—— 否则就别挂。LLM 把这条编剧纪律也学进去了:你塞给它的每条上下文, 它都默认「必有深意」,并努力把它用上2。
所以检索错了一条不相关的,不是「多了一句废话」, 而是它会被过度解读、把答案带偏。书作者把这叫契诃夫之枪谬误。 唯一的解药是:要检索,就检索对——这正是后面两派路数要解决的问题。
3. 核心原理
3.1 检索是一个搜索问题
把检索形式化:你手里有一条「搜索串」(比如候选书的简介), 和一堆「文档」(用户历年的书评、帖子)。 目标:找出与搜索串最相似的几段,最好每段还带个分数3。
「相关」定义不清,业界退而求其次:搜「相似」。「相似」怎么算,分两派。
3.2 词法检索:数共同词,老而弥坚
词法检索——按「用了哪些词」来算相似,不管词的意思。 这招比 LLM 老得多,是信息检索学科几十年的家底4。
最简版本是 Jaccard 相似度,书里拿它算了完整一遍,我们跟着走:
搜索串: 「The Beach 讲一个年轻背包客寻找未被旅游染指的海滩」
片段: 用户旧书评一条
第一步,去停用词——the、a、is 这类对意思没贡献的高频词,扔掉
第二步,词干化——walking、walks、walked 全归并成 walk
第三步,算分:两边共有的词 ÷ 两边合起来不重复的词 = 0 到 1 之间一个数
图说:Jaccard 三步。两个「backpacking」对上,比十个「the」对上值钱——
但 Jaccard 分不清这个,它眼里每个词都一样重。
Jaccard 的好处:不用预先建任何东西,零内存开销,小数据集上飞快—— GitHub Copilot 就用它,在 IDE 当前打开的文件里闪电找出相关片段(作者一手经验)5。
它的粗糙也明显:共同的词是 go 还是 backpacking,它一视同仁。
进阶版 TF-IDF——按「这个词在全部文档里稀不稀有」给匹配加权,
越稀有的词对上越算数——以及更精细的 BM25,都补这个洞;
代价是要预先统计全库词频6。
词法检索的死穴:认词不认意。 「I forgot my backpack」和「I'm going backpacking」词干重叠,其实无关(假阳性); 「didn't remember their rucksack」和「忘了背包」意思是近亲,却一个词都不共享(假阴性)。 错别字、同义词、跨语言,全都破防7。
3.3 神经检索:把一段文字变成一个坐标
想按意思匹配,就得先把「意思」变成能算距离的东西。 嵌入(embedding)——用一个小模型,把一段文字变成一个坐标(一长串数字); 意思相近的文字,坐标就近。 这样的文字在空间里各就各位, 找相似就变成了找最近邻——离查询点最近的那几个坐标8。
注意:嵌入模型不是 LLM。 它是另一种模型—— 架构同源(也是 Transformer 家),但产出不是「下一个词」而是「一个坐标」; 它通过对比学习——把意思相关的文字拉到坐标系里相近处、 不相关的推远,如此反复调教——练出这个本领。 它比 LLM 小几个量级、便宜几个量级,所以把整座文档库都「坐标化」也负担得起9。
落地是两步(对照第 2 节那张图):
- 离线建索引——把全部文档切块、逐块算坐标、存进库, 这份「为查找预先备好的数据结构」就叫索引;
- 在线查询:把查询串也算成坐标,让库返回最近的几个坐标连同 原文块。
3.4 切块:一块一个主意
文档怎么切,直接决定检索质量。三条准则10:
- 不超嵌入模型的窗口(2024 年 OpenAI 的嵌入模型上限 8,191 个标记);
- 一块只装一个主意——装了两个话题的块,坐标会落在两个话题中间的无人区;
- 大小适合进提示词——检索出来是要塞给模型的。
切法上:滑窗(256 词一窗、128 词一步,窗口留重叠,防一个想法被腰斩) 简单均匀;按自然边界切(段落、章节)能保证一句不断; 代码场景还有一招:给块补上归属——方法是某个类的成员, 就把类定义和初始化代码也拼进去,坐标才立得稳11。
3.5 向量库:找最近邻是个已解决的问题
嵌入坐标一般上千维,在大库里找最近邻并不平凡—— 但已经被人解决得很好。向量数据库——专门存向量、并能飞快回答 「谁离这个点最近」的存储系统:自己跑,有 FAISS 这类库; 买服务,有 Pinecone 这类托管商12。
3.6 主走查:60 行的最小 RAG,从头走到尾
现在把 3.2–3.5 全部装配起来,走书里的完整例子。 场景:用户在书店网站浏览,系统要根据他过去的书评, 预测他会给眼前这本书打几分13。
第 ① 步,料。 用户写过 11 条书评,其中两条是:
"I hate stories about backpacking. It's boring."
"Another bland romantic utopia. This time on a tropical island."
第 ② 步,建索引。 11 条全部送嵌入模型(text-embedding-3-small), 各得一个坐标,存进 FAISS。
第 ③ 步,查询。 候选书是《The Beach》,系统拿它的简介做 查询串:
"The Beach by Alex Garland critiques backpacker culture by exposing the
selfishness and moral decay behind their pursuit of an untouched paradise."
第 ④ 步,取回最近的两条(k=2)。 返回的正是开头那两条—— 注意匹配上的不是字面:「critiques backpacker culture」和 「I hate stories about backpacking」共享的词其实不多,是意思对上了。
第 ⑤ 步,组装提示词(样板话 + 检索结果):
Here is a book I might want to read:
The Beach by Alex Garland critiques backpacker culture by …
Here are relevant reviews from the past:
I hate stories about backpacking. It's boring.
Another bland romantic utopia. This time on a tropical island.
On a scale of 1 (worst) to 5 (best), how likely am I to enjoy this book?
Reply with no explanation, just a number.
第 ⑥ 步,模型答:2。 一个讨厌背包客故事的人, 大概率不会喜欢一本「背包客天堂幻灭记」——检索给对了料, 模型的常识就能把最后一步走完14。
60 行代码,离线一半在线一半,一个能跑的 RAG 就这么多。 生产级的缺口里,这本书补得上两样:评估(第 13 章整章谈) 与更新(应用一改版,攒下的旧样本就作废——同在第 13 章); 权限与多租户,书里管不着。
3.7 两派怎么选:能 debug 与懂意思
作者(两个搜索出身的工程师)给了一张不带立场的对照15:
| 词法检索 | 神经检索 | |
|---|---|---|