跳到主要内容

一套这样的系统长什么样 —— 两条线,一条离线一条在线

这一章讲三件事: 这套系统实际分成哪两条线、每条线上各有哪几步、 以及哪一步出问题该先查哪里

它在全书链条上的位置是地图。 后面十七章没有一章在讲新东西—— 它们全都在改这一章这条走查上的某一步。 所以这一章最好别跳: 跳了之后,第 07 章的「切法」、第 09 章的「索引」你会不知道它们插在哪儿。

1. 这一章讲什么

三句话:

  1. 大多数人以为它是一条线:你问 → 它搜 → 它答。实际上是两条, 而且其中一条在你提问之前几天就跑完了;
  2. 这两条线中间靠一件东西接上:同一个把文字换算成数的模型—— 两条线用的必须是同一个,换一个就得整库重来;
  3. 知道了这两条线,你就知道答得不好时该先查哪一条

2. 顶层全景:两条线,一次接头

┌─ 离线,提问之前就跑完 ────────────────────────────────────┐
│ 你的文档 → 切成小块 → 每块换算成一串数 → 连块带数存进库 │
└───────────────────────────────────────────────┬──────────┘
│ 库
┌─ 在线,你按下回车之后 ──────────────────────────┴──────────┐
│ 你的问题 → 换算成一串数 → 在库里找方向最接近的几串 │
│ → 取出对应的那几块 → 和问题拼成一段话 → 模型写答案 │
└──────────────────────────────────────────────────────────┘

图说:两条线上都有「换算成一串数」这一步,而且必须是同一个换算器。
这是全图唯一的接头处,也是全图最容易搞砸的地方(第 6 节)。

书自己是这么分的:数据入库(data ingestion)和查询处理(query processing)1。 它还列了三个必备零件:一个把文字换算成数的模型、一个存这些数并能搜的库、 一个照材料写答案的模型2

3. 承重词一:块

这一节要讲透本章第一个承重词。一句话先给结论:块就是把长文档切开之后的一小段文字, 它是检索的最小单位。

先看现象:为什么非切不可

你有一份两百万字的资料。第 01 章讲过,模型一次读不完那么多字。 所以你不能把整份资料交给它——你只能挑几段交给它。

「挑几段」这个动作,要求资料事先已经被切成一段一段的。 那一段一段的东西就叫块。

切了之后,一切都跟着变

这里要说清一件后面六章都在处理的后果:

库里存的是块,搜出来的是块,交给模型的也是块。 原文那份完整的文档,从切开的那一刻起,就再没有被整份看过。

于是:

  • 一刀切在句子中间,那半句就永远是半句;
  • 一块里的「它」指的是上一块里的某个东西,那么这一块被单独取出来时,「它」就没有着落;
  • 一份文件的标题写在第 1 块里,第 87 块被取出来时,谁也不知道它属于哪份文件。

这三件事就是第 06、07 两章的全部内容。 这里只要先记住形状: 切块不是一个技术细节,它是后面一连串麻烦的源头。

书里这一步的具体做法

书第 1 章那个例子的切法很朴素3:

目标每块 1 000 字符
├─ 先在这 1 000 字符范围内往回找最后一个空行(段落的分界)
├─ 找不到空行,就往回找最后一个「. 」(句号加空格,句子的分界)
└─ 都找不到,就硬切

切完之后,下一块的起点 = 这一块的终点往回退 200 字符
也就是说,相邻两块共享 200 字符的重复内容。

图说:那个「往回退 200」叫重叠。它是干什么用的、该设多少,第 07 章讲——
书在四个例子里给了 0、20、50、200 四个不同的值,一次都没解释过。

4. 承重词二:嵌入(这一章只讲到现象层)

本章第二个承重词。一句话先给结论:嵌入就是一块文字对应的那一串数。

先看现象:为什么必须有它

你想要「按意思搜」。可计算机没有「意思」这个东西可算,它只会算数。

所以要有一步换算:把一块文字,换算成一串数。 换算的规矩只有一条——意思相近的两块文字,换出来的两串数方向也相近。

这一串数就叫这块文字的嵌入(英文 embedding,你在任何一家的接口文档里都会撞见这个名字)。

现象层够用了吗?这一章够了

书第 1 章那个例子用的换算模型叫 text-embedding-3-small, 一块文字进去,出来 1 536 个数4。这 1 536 个数长什么样、 为什么用「方向」而不是「长度」来比、这串数是怎么被算出来的——第 08 章讲

这一章你只需要接受一件事:文字进去,一串数出来;意思越近,方向越接近。

判断(我们的,不是书里的):这一步是全书唯一真正「不可替代」的一步。 切块可以粗糙、检索可以只用关键词、模型可以换小的——系统仍然能跑。 但如果没有「把意思变成一个能算的量」这一步,「按意思搜」这四个字就无从谈起。 如果错,会错在: 如果你的场景其实只需要字面匹配(比如全是型号、编号), 那这一步确实可以整个不要——那时候你需要的是第 10 章那半章讲的老办法,不是这一整套。

5. 主走查:一次哈利波特问答,从入库到出答案

这是本章的主走查,也是全书的顶层全景。 后面每一章都在改它的某一步, 所以这条走查会在后面反复被回指。

场景是书第 1 章那个跑例:一份哈利波特知识库的文本文件,问它一个书里的问题5(下面除了「1 000 字符」「200 字符」「1 536 个数」「取最近 3 块」是书里的参数, 其余的数都是为演示编的,不是真实数值。)

离线线:第 1 步 —— 切块

输入:harry_potter_knowledge_base.txt,约 240 000 字符
↓ 每块 1 000 字符、相邻块重叠 200 字符
输出:300 块

第 1 块 开头 "Mr. and Mrs. Dursley, of number four, Privet Drive…"
第 2 块 开头 "…was proud to say that they were perfectly normal…"
↑ 这 200 字符和第 1 块的结尾是重复的

离线线:第 2 步 —— 每块换算成一串数

300 块 → 300 串数,每串 1 536 个

第 17 块的那一串(只画出前 4 个):
[ 0.021, -0.048, 0.113, 0.004, … 共 1 536 个 ]

离线线:第 3 步 —— 连块带数存进库

库里每一条长这样:
id = "chunk-017"
文字 = "…the hut on the rock in the sea…"(那 1 000 字符原文)
那串数 = [0.021, -0.048, …]

图说:注意原文和数是一起存的。搜的时候比的是数,交给模型的是文字——
这个分工在第 05 章和第 15 章还会各出现一次,而且是同一条纪律。

这三步跑完,离线线就结束了。它可能跑了两个小时,而这两个小时发生在你提问之前。

在线线:第 4 步 —— 问题也换算成一串数

问题用书里那句原文:"Why did Uncle Vernon take the family to the hut by the sea?" (弗农姨父为什么把全家带到海边的小屋?)6

问题 → 同一个换算模型 → [ 0.019, -0.051, 0.108, 0.007, … 共 1 536 个 ]

图说:这里必须是同一个换算模型。为什么,下一节讲。

在线线:第 5 步 —— 比出相近程度,取最近的三块

问题那一串 vs 库里 300 串,逐一比方向,得出相近程度:

chunk-017 0.62 ← 最像
chunk-018 0.55
chunk-004 0.49
chunk-211 0.31
…其余 296 块更低

取前 3 块:chunk-017、chunk-018、chunk-004

这三个数(0.62 / 0.55 / 0.49)是为演示编的。 但有一件事现在就得说破:这种分数只在同一次比较里有意义。 0.62 不代表「六成像」,它只代表「在这 300 块里它排第一」。 (为什么会这样,第 08 章第 4 节讲——书里那个真实的例子,最高分只有 0.31。)

在线线:第 6 步 —— 拼成一段话交给模型

Answer the question using only the context below.

Context:
<chunk-017 的 1 000 字符>
---
<chunk-018 的 1 000 字符>
---
<chunk-004 的 1 000 字符>

Question:
Why did Uncle Vernon take the family to the hut by the sea?

Answer:

注意第一行那句 "using only the context below" —— 这就是第 01 章第 6 节说的那条捆绳:只用我给你的材料回答。 书这个例子里,它就是这么一句话7

在线线:第 7 步 —— 出答案

模型读完这 3 000 字符 + 一句问题,写出一段答案。
答案里的每一句,都能在那 3 块原文里找到依据。

整条走查到此结束。它一共七步,前三步离线、后四步在线。 把这七步记住,你就有了读后面十七章的坐标系。

6. 一条铁律:两条线必须用同一个换算模型

这是这条走查上最容易被搞砸的一步,而它的代价最大。

书在第 5 章开篇把这件事写进了流程本身:建库时给每块算一串数, 然后「用同一个模型」给每个进来的查询算一串数8

为什么不能换

回到第 4 节那条规矩:「意思相近 ⟹ 方向相近」这句话,只在同一个换算模型内部成立。

换一个模型,它会用另一套自己的规矩去排布这些数。 两套规矩之间没有任何对应关系——就像两个人各自画了一张地图, 一张以正北为上、一张以河流走向为上,你不能拿一张图上的坐标去另一张图上找地方。

库里的 300 串数 ← 模型 A 算的
查询的那一串数 ← 模型 B 算的

比出来的「相近程度」是一堆没有意义的数。
更糟的是:它不会报错。它会照常返回三块,只不过那三块是随机的。

图说:这是这套系统最阴险的一种坏法——它不崩,它只是开始胡说。

换掉的代价具体是多少

整库重算。 第 5 节那条走查的第 2 步要从头再跑一遍: 300 块要重算,三千万块也要重算。

而这正是「换库容易、换换算模型难」这个不对称的来源: 书说换一个存放的库时,你要重建那份造好的册子(索引), 但不必重新生成这些数——而生成这些数才是贵的那部分9换换算模型则恰恰相反:贵的那部分躲不掉。

(这条不对称在第 09 章会作为「先简后繁」的依据再出现一次。)

7. 模型不只在最后出现一次 —— 它在入库时用得更多

这是全书一条贯穿始终、书自己没有命名的暗线。 书在第 2 章开篇明说了这件事10:

用在哪一步它干什么
生成步读检索到的材料 + 用户的问题,写出答案 —— 就是第 5 节走查的第 7 步
准备步(书也叫它入库阶段)从图片里抽文字、把录音转写成文稿、给长文档写摘要、给每一块补上标注——也就是额外记上「它出自哪份文件、第几页、谁写的、什么时候写的」这类说明(第 06 章讲透)

准备步这一栏,才是第 04、05、06 三章的全部内容。

为什么这件事值得单独说破: 大多数人对这套系统的印象停在「最后那一次调用」, 于是把成本和时间也都算在那一次上。实际上,离线那条线上的模型调用次数, 常常是在线那条线的成百上千倍——因为它要把你每一份文档都过一遍,而且是过好几遍。

一份 1 000 页的扫描件进库:
每页调一次模型抽文字 → 1 000 次
每张图调一次模型写描述 → 120 次
每张表调一次模型写摘要 → 45 次
─────────
1 165 次
而一次提问,只调 1 次。

图说:这些数是为演示编的,不是真实数值。要看清的是形状——
离线那一头的调用量,和在线那一头不在一个量级上。

8. 排障法则:答得不好时,先查哪一条线

书给了一条可以直接照做的规矩,它是这一章最实用的一句话11:

「先单独测检索,再去调生成。 如果答案听着很泛、而且和你的文档无关,问题通常出在检索质量,不在模型。」

怎么照做

照第 5 节那条走查,把第 5 步的输出直接打印出来看。

你问: "Why did Uncle Vernon take the family to the hut by the sea?"
打印: chunk-017 / chunk-018 / chunk-004 的原文

├─ 这三块里根本没提到那间小屋
│ → 问题在检索。别去改交给模型的那段话,改也没用。

└─ 这三块里明明白白写着答案,而模型答偏了
→ 这时候才轮到去改那段话(第 03 章讲怎么改)。

这条法则为什么重要: 因为答得不好时,最顺手的动作是去改那段交给模型的话—— 它改起来最快、反馈最直接。而书说,那多半是在改错的地方。 (第 11 章开头那句「改进检索这一步是最有效的办法」,就是这条法则的放大版。)

9. 这套基础形态什么时候会失效

书自己给了两个明确的失效点12:

失效点长什么样去哪一章
问题需要多步推理「比较 X 和 Y,然后推荐 Z」——一次检索答不了,因为它得先分别查 X 和 Y第 11 章(把问题拆成子问题)
不同类型的资料需要不同的搜法一半答案在结构化的表里、一半在文档里,而这条走查只有一个库、一种搜法第 10 章(先选源、再缩范围)

这两条是后面那半本书的入口。 记住它们的形状: 不是「这套做法不对」,而是「这条走查只有一条路,而有些问题需要拐弯」。

10. 作者的判断与证据

说法它是什么
两条线(入库 / 问答)的划分是事实描述,而且是这类系统的通用结构,不是作者的观点
三个必备零件同上,书列的是最简清单
「两条线必须用同一个换算模型」是事实,由「这些数只在同一个模型内部可比」直接推出
「换库要重建册子但不必重算这些数」是事实陈述,书没给测量,但这条在原理上成立
「答案听着泛且和你的文档无关,问题通常在检索」作者的经验判断。 书没有给任何统计;「通常」这个词是它自己用的
「第 1 章故意不用框架,是为了让概念清楚」作者的写作选择,他明说了理由
那两个失效点作者的经验归纳。 它们确实成立,但书没有说这是不是全部的失效点

判断(我们的,不是书里的):第 8 节那条排障法则,是这一章唯一一条 「今天就能用上、而且立刻省时间」的东西。 它的价值不在它有多深,而在它指出了一个人人都会犯的顺序错误: 答得不好 → 去改那段交给模型的话。这个动作反馈最快,所以最容易被反复做, 而它多半改不动真正的病灶。 如果错,会错在: 如果你的检索本来就很稳(比如资料很小、块很少、问题很集中), 那瓶颈确实在生成那一头,这条法则会把你引向一次无用的排查。 判据是:先打印一次检索结果,三十秒就能分辨。

11. 边界与局限

这一章的走查里,有三件事书没有交代:

  • 切块的参数从哪来。 1 000 字符、重叠 200——书没有解释这两个数是怎么定的, 而且它在别处的例子里用的是完全不同的值(第 07 章会把这个缺口摊开讲);
  • 取回几块。 例子里取 3 块。为什么是 3 不是 5 或 10?书没给起点值, 只在第 17 章说过「调这个数的时候要盯着取回来的块有几块真有用」;
  • 文档改了怎么办。 离线那条线跑完之后,如果原文档被修改、被删除, 库里那些块和数怎么更新?全书没讲。 这是这本书最实际的一处空白。

还有一处是我们要提醒的:

  • 这条走查假设所有资料都在一个库里、都用一种搜法。 第 9 节那两个失效点都是从这个假设上崩的。读后面的章节时, 每一次都可以回来问一句:这一章在改这条走查的哪一步?

12. 可带走的

  1. 它是两条线,不是一条:离线的入库线(切块 → 换算成一串数 → 存库)+ 在线的问答线(问题换算 → 比出最像的几块 → 拼进去 → 生成);
  2. 块 = 长文档切开之后的一小段,是检索的最小单位。 从切开那一刻起,原文档就再没有被整份看过——后面六章的麻烦都从这儿来;
  3. 嵌入 = 一块文字对应的那一串数。 规矩只有一条:意思相近,方向也相近。 (书那个例子里是 1 536 个数;它是怎么算出来的,第 08 章讲);
  4. 铁律:两条线必须用同一个换算模型。 换了不会报错,它只会开始返回随机的块;
  5. 换库便宜、换换算模型贵——因为换库只要重建册子,换换算模型要把所有文字重算一遍;
  6. 模型在入库时用得比回答时多得多(抽文字、转写、写摘要、补标注), 离线那头的调用量常常是在线那头的成百上千倍;
  7. 排障法则:先单独测检索,再调生成。 答案听着泛且和你的文档无关,病灶几乎总在检索这一头;
  8. 两个明确的失效点:需要多步推理的问题、不同资料需要不同搜法—— 它们是第 10、11 章的入口;
  9. 这条七步走查是全书的坐标系。 读后面每一章时都问一句:它在改哪一步?

13. 原文地图

主题原书章原文位置
三个必备零件Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:336(搜「three essential components」)
两条线的划分Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:344(搜「data ingestion and query processing」)
入库三步Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:348(搜「Split documents into smaller chunks」)
问答四步Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:356(搜「Convert the user’s question into an embedding vector」)
故意不用框架Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:380(搜「without frameworks」)
切块的具体做法与参数Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:388(搜「def chunk_text」)
那个换算模型的名字Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:413(搜「text-embedding-3-small」)
取最近三块Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:444(搜「top_k=3」)
那句捆绳指令Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:467(搜「using only the context below」)
两个失效点Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:492(搜「multistep reasoning」)
排障法则Chapter 1. Getting Started with RAGtext/03-ch01-chapter-1-getting-started-with-rag.txt:494(搜「Test retrieval independently」)
模型出现在两个地方Chapter 2. Foundation Modelstext/04-ch02-chapter-2-foundation-models.txt:6(搜「transcribe audio」)
入库阶段这个叫法Chapter 2. Foundation Modelstext/04-ch02-chapter-2-foundation-models.txt:8(搜「ingestion phase」)
用同一个模型算查询Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:10(搜「Embed each incoming user query with the same model」)
换库不必重算这些数Chapter 6. Vector Databases and Similarity Searchestext/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:198(搜「which is the costly part」)

Footnotes

  1. 出处:「Chapter 1. Getting Started with RAG」第 344 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:344,搜「data ingestion and query processing」)。入库三步见同章第 348 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:348,搜「Split documents into smaller chunks」);问答四步见第 356 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:356,搜「Convert the user’s question into an embedding vector」)。

  2. 出处:同章第 336 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:336,搜「three essential components」)。原文列的三件是:把文本表示成向量的嵌入模型、搜这些向量的向量库、拿检索到的材料生成答案的模型。

  3. 出处:同章第 388 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:388,搜「def chunk_text」)。函数签名写着 size=1000, overlap=200;它先在窗口内往回找 \n\n,找不到再往回找 ". ",都找不到才硬切。

  4. 出处:同章第 413 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:413,搜「text-embedding-3-small」)。这个模型输出 1 536 个数的说法见「Chapter 5. Embeddings」第 110 段(text/07-ch05-chapter-5-embeddings.txt:110,搜「1,536 values」)。具体型号会变,记住的应该是「一块文字换出一串定长的数」这个形状,不是这个名字。

  5. 出处:同章第 404 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:404,搜「harry_potter_knowledge_base」)。

  6. 出处:同章第 458 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:458,搜「hut by the sea」)。这是书自己用的那句测试问题;取回块数写在同章第 444 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:444,搜「top_k=3」)。

  7. 出处:同章第 467 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:467,搜「using only the context below」)。这一版只有一句;完整的四件套写法在第 03 章。

  8. 出处:「Chapter 5. Embeddings」第 10 段(text/07-ch05-chapter-5-embeddings.txt:10,搜「Embed each incoming user query with the same model」)。书把「用同一个模型」直接写进了检索流程的第 2 步。

  9. 出处:「Chapter 6. Vector Databases and Similarity Searches」第 198 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:198,搜「which is the costly part」)。

  10. 出处:「Chapter 2. Foundation Models」第 6 段(text/04-ch02-chapter-2-foundation-models.txt:6,搜「transcribe audio」)与第 8 段(text/04-ch02-chapter-2-foundation-models.txt:8,搜「ingestion phase」)。后一段给了「准备步」的另一个名字:入库阶段。

  11. 出处:「Chapter 1. Getting Started with RAG」第 494 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:494,搜「Test retrieval independently」)。

  12. 出处:同章第 492 段(text/03-ch01-chapter-1-getting-started-with-rag.txt:492,搜「multistep reasoning」)。原文举的例子就是「比较 X 和 Y,然后推荐 Z」,以及「不同数据类型需要不同的检索策略」。