跳到主要内容

建库、落盘、取回来 — 以及一个反直觉的坑

这一章讲三件事: 切好的块怎么进库、为什么只进去 20 块、进去的又是谁; 取回来有哪两种取法;以及「取回来最像的」为什么不等于「取回来是答案」。

它在全书链条里的位置: 这是第 04、05 两章的落地。 做完这一章,第 01 章那段模板里 {context} 那个空终于有东西可填了—— 第 07 章会把它填上,并且当场失败一次。

本章主走查:20 块进库,问一句「什么是大语言模型」,取回 3 块; 再用第二种取法问同一句,看哪几块换了。 下面每一块的内容、每一个数,都是原书里真实打印出来的。

1. 建库、落盘、重开:三行代码

这一节是主走查的第 1 步。

切好的块进库,代码只有一句1:

vectordb = Chroma.from_documents(
documents=split_docs[:20], # ← 只取了前 20 块
embedding=embedding, # ← 第 04 章那个向量模型
persist_directory=persist_directory # ← 存到磁盘上的哪个目录
)

这一句里发生了三件事: 把这些块逐块送去算成一串数;把数和原文一起存下来; 把存放位置记在 persist_directory 指的那个目录里。

但存下来还不算完,必须显式落一次盘。 书特意提醒: 要确保运行 vectordb.persist() 来持久化向量数据库,以便我们在未来的课程中使用2。 落完盘打印一下数量,显示 203

下次不用重建,按路径打开就行4:

vectordb = Chroma(
persist_directory=persist_directory, # ← 同一个目录
embedding_function=embedding # ← 必须是同一个向量模型
)

注意第二行。 书在这里写了一句提醒:此处你需要使用和构建时相同的向量模型4—— 这正是第 04 章那条硬约束落成的代码。传错了不报错,只会取回一堆不相干的东西。

2. 为什么只有 20 块:一笔书没算的账

这一节把上一节那个 [:20] 的代价摆出来。它是下一章失败的全部原因。

书在那一行代码后面写了注释,一字不差: 为了速度,只选择前 20 个切分的 doc 进行生成;使用千帆时因 QPS 限制,建议选择前 5 个 doc1

QPS 指的是「每秒最多允许你发几次请求」,是服务方给的限速。 超了就被拒,所以要么放慢、要么少发。

补充(不在书里,这是我们按书给的数推算的): 每一块都要单独调一次向量接口。光南瓜书那一份就切出 720 块(第 05 章那个数), 那就是至少 720 次网络往返——而资料目录里还有别的文件,真实次数只会更多。 按常见的每秒几次到十几次的限速估,光建这一次库就要跑好几分钟; 按第 04 章那张表里 text-embedding-3-small 每美元 62500 页的价钱, 南瓜书那 30 万字符大约合几分钱,钱不是问题,时间是。 这两个量书里都没有给,是我们按它给的 720 和 QPS 提示估的。

这 20 块是从哪儿数出来的,要说清楚,因为下一章的失败全挂在这上面。 建库这一步并不是接着第 05 章那 720 块往下走的:它把资料目录下的所有文件重新读了一遍, 再整体切一次——目录里除了南瓜书那份 PDF,还有一份提示工程教程的 md5[:20] 取的是这一遍切出来的前 20 块,而这 20 块全部落在那份提示工程教程里6

所以库里的实情是:南瓜书的内容一块都没进去。 本章第 4 节马上会看到取回来的三块,你可以自己对一眼——没有一块出自南瓜书。

代价在下一章立刻显形: 第 07 章那次问答问「什么是南瓜书?」, 答的是「抱歉,我不知道南瓜书是什么」。那是库里根本没有这个答案的直接结果, 而根子就在这个 [:20]

3. 框架没内置的那家,自己接上去

这一节是本章唯一的另起走查:书拿智谱演示了怎么补一个框架不认识的向量模型。

书的处境是这样的:框架内置了 OpenAI、百度千帆这些常见的, 但它并没有内置所有向量模型,而是允许用户自定义,以此提供扩展性7

补的做法是照着框架给的一份空壳去填——这份空壳的名字叫基类: 它把「一个向量模型必须能干哪几件事」列成几个空方法,自己不干活, 你照着这几个格子把自己的做法填进去,框架就认得你这一个了。要填的是三个方法8:

方法收什么还什么
_embed一段文本一串数——这是真正去调远程服务的那一个
embed_query一句查询一串数
embed_documents一批文档一批数,一份文档一串

为什么要分成三个而不是一个? 书自己回答了这个问题,答案很实在: 因为对一批文档可以在请求之前做些处理,比如文档特别长时先分个段,防止超过上限9。 而查询那一个只是把单条包成一个列表,转手交给上面那个收一批的方法9

书还坦白了一句:反过来写也行。 先写查询那个、让收一批的那个去循环调它,同样成立—— 它只是给了一个简单的示例,没有说这是唯一写法9

4. 取回来第一种:按余弦排序,取最像的前 k 个

这一节是主走查的第 2 步。

向量检索就是:把你的问题也算成一串数,再在库里找出跟它最接近的那几串。 书写明 Chroma 用的量法正是第 04 章那个余弦10

原书拿一句「什么是大语言模型」去问这个 20 块的库,取 3 块11:

question = "什么是大语言模型"
sim_docs = vectordb.similarity_search(question, k=3)
检索到的内容数:3

拿回来的 3 块是这些(每块只印了前 200 个字符)12:

第几块这一块讲的是
第 0 块「虚假知识:模型偶尔会生成一些看似真实实则编造的知识」——正是第 01 章那个幻觉
第 1 块讲少样本的那一块:先给一个祖孙对话样例,再要模型用同样的隐喻风格回答关于「韧性」的问题
第 2 块「第二章 提示原则」的开头:设计高效提示词的两个关键原则

先核第 2 节那句话:这三块的出处全是那份提示工程教程,没有一块出自南瓜书。 「虚假知识」出自它讲幻觉那一节,「祖孙对话」出自它讲少样本那一节, 「第二章 提示原则」是它的章首。这就是「南瓜书没进库」的直接证据。

第 1 块还要说明一句,免得你以为是自己刚读过的那段。 库里那份教程用的少样本例子是「祖孙对话 / 韧性」, 而本组拆解第 03 章正文里那组是「学生问圣贤 / 文言文 / 孝顺」—— 这是同一技巧的两个不同版本,一个印在书的正文里,一个躺在书的知识库里12

注意这三块的共同点:它们全都和「大语言模型」有关,但没有一块在定义它。 这不是巧合,第 7 节会解释为什么。

5. 取回来第二种:先要相关,再要不一样

这一节是主走查的第 3 步:同一个问题,换一种取法,看哪几块换了。

书先摆出第一种取法的毛病:如果只考虑检索出内容的相关性,会导致内容过于单一,可能丢失重要信息13说白了:最像的三块,很可能是同一段话被切成的三片。

第二种取法叫最大边际相关性(MMR)——它的做法是: 先挑一个最相关的;之后每挑一个,都要在「跟问题相关」和「跟已挑的那些不一样」之间折中13

同一句「什么是大语言模型」,换成 MMR 之后拿回来的是14:

第 0 块:「虚假知识:模型偶尔会生成一些看似真实实则编造的知识」 ← 和上一种一样
第 1 块:「相反,我们应通过提示词指引语言模型进行深入思考……」 ← 换了
第 2 块: 泡茶那段英文原文 Making a cup of tea is easy! … ← 换了

图说:三块里换了两块。换掉的那两块跟问题的相关度更低,
但它们各自讲的是不同的事——这就是「多样性」换来的东西。

这两种取法没有绝对优劣,书也没给选择标准。 记住区别就够了:第一种取「最像的」,第二种取「像但彼此不重样的」。

6. 还有一路没用上:按词找

这一节挂一个名字,它在第 04 章被欠着。

第 04 章那张向量数据库对照表里,Weaviate 那一行写着它「还能把按词找和按意思找两路合起来用」。 这种做法的名字叫混合搜索——就是同一次查询同时跑关键词匹配和按意思找, 再把两份结果合成一份排名15

为什么要合: 按意思找会漏掉专有名词(型号、人名、错误码——这些东西的「意思」很稀薄, 但字面必须对上);按词找会漏掉换了说法的同义句。两条腿走路才都不漏。

这门课没有用它,Chroma 也不提供。书把它留在了那张对照表里,再没回来过。

7. 一个反直觉的坑:向量模型学的是配对,不是因果

这一节把书推迟到第五章才讲的一件事提前挑明,因为不知道它就会误判上面那三块。

书是这么说的:检索起作用有一个核心假设——我们假设匹配到的强相关文本段就是问题对应的答案文本段; 但很多向量模型其实构建的是「配对」的语义相似度而非「因果」的语义相似度16

书给的例子一看就懂16:

提问:今天天气怎么样

向量模型认为更像的是:「我想知道今天天气」 ← 这是同一个问题的另一种说法
向量模型认为不太像的是:「天气不错」 ← 这才是这个问题的答案

图说:两句话经常在同一个语境里一起出现,模型就认为它们「像」。
而「问」和「答」在文字上往往差得很远。

回头看第 4 节那三块:它们全都在讨论大语言模型的某个侧面,但没有一块在定义它。 这不是库太小造成的,是这条机制本身的性质。

书给的两条治法是:换一个效果更好的向量模型、或者拿自己的数据微调一个; 以及建倒排索引——针对每一块另外建一个更能代表它内容的索引项,检索时匹配索引项而不是全文17这门课两条都没做,它们被推给了那个至今没有正文的第二部分。

8. 边界:这个库现在还答不了问题

这一节把账记下来,交给下一章。

现在的状态是:库建好了、能取回块了,但还没有任何东西把取回来的块和模型连起来。 你手上有的是三块文字,离「一句人话的回答」还差整整一步。

而且这个库里装的根本不是你以为的东西。 下一章会拿「什么是南瓜书?」去问它, 而它会答不上来——那不是链坏了,是这一章第 2 节那个 [:20] 把南瓜书整个挡在了库外。

判断(我们的,不是书里的): 这一章那个 [:20] 是整本书里最贵的一处教学妥协。 它让读者跑得快,却把这门课的主角资料(南瓜书)整份挡在库外, 于是紧接着的第一次问答实验直接失败;而书没有把这两件事联系起来说18—— 第 07 章那次「抱歉,我不知道」在原文里读起来像是模型的问题。 如果错,会错在: 这条判断能不能被推翻,全看「库里到底有没有南瓜书」。 你可以自己验:把 [:20] 去掉、让全部块都进库,再问同一句。 答上来了,说明失败确实只是「库里没有」;照样答不上来,那问题在检索本身(取几块、或者第 7 节那条性质), 这条判断就要改。我们照原文核到的是前一种。

9. 可带走的

  1. 建库只有一句代码:把块、向量模型、存放目录交给它;
  2. 必须显式落一次盘(persist),否则下次得重建;
  3. 重开时要把当初那个向量模型再传进去——传错不报错,只是取回一堆无关的东西;
  4. 书只放了前 20 块进库,理由是省时间和限速(QPS);
  5. 而这 20 块全部来自另一份资料(提示工程教程),南瓜书一块都没进去——下一章那次失败的全部原因;
  6. 全库至少要跑 720 次网络往返,钱不多、时间不短(这两个量是我们估的,书没给);
  7. 框架没内置的向量服务可以自己接:照着一份写好的空壳(基类)填三个方法,真正干活的只有第一个;
  8. 取回来第一种是按余弦排序取前 k 个;这门课的 k 是 3;
  9. 取回来第二种是 MMR:先要相关,再要彼此不一样;同一个问题,三块里换了两块;
  10. 还有一路叫混合搜索(按词找 + 按意思找),这门课没用,Chroma 也不提供;
  11. 最要紧的一条:向量模型学的是「配对」,不是「因果」——「我想知道今天天气」比「天气不错」更像那个问题;
  12. 所以「取回来最像的」不等于「取回来是答案」;书给的治法(换模型、微调、倒排索引)这门课一条都没做。

10. 原文地图

主题原书章原文位置
建库一句代码、只取前 20 块搭建并使用向量数据库text/05-p81-100.txt:266(搜「Chroma.from_documents」) · text/05-p81-100.txt:267(搜「为了速度,只选择前 20 个切分的 doc」)
建库前重新读全目录、重新切一次搭建并使用向量数据库text/05-p81-100.txt:136(搜「folder_path = 」) · text/05-p81-100.txt:143(搜「prompt_engineering」) · text/05-p81-100.txt:234(搜「split_documents(texts)」)
那 20 块全是提示工程教程(下一章重开同一个库的旁证)构建检索问答链text/06-p101-120.txt:53(搜「什么是prompt engineering」) · text/06-p101-120.txt:66(搜「相反,我们应通过 Prompt 指引」)
落盘与数量 20搭建并使用向量数据库text/05-p81-100.txt:272(搜「持久化向量数据库」) · text/05-p81-100.txt:280(搜「向量库中存储的数量:20」)
重开要传同一个向量模型构建检索问答链text/06-p101-120.txt:11(搜「和构建时相同的」) · text/06-p101-120.txt:41(搜「embedding_function=embedding」)
自定义向量接口:三个方法Embedding 封装讲解text/05-p81-100.txt:375(搜「Embeddings 基类」) · text/05-p81-100.txt:462(搜「token 限制」)
余弦距离搭建并使用向量数据库text/05-p81-100.txt:286(搜「Chroma的相似度搜索使」)
相似度检索的三块搭建并使用向量数据库text/05-p81-100.txt:293(搜「similarity_search(question,k=3)」) · text/05-p81-100.txt:303(搜「虚假知识」) · text/05-p81-100.txt:308(搜「祖孙对话样例」) · text/05-p81-100.txt:323(搜「如何去使」)
MMR 与它的理由搭建并使用向量数据库text/05-p81-100.txt:330(搜「丢失重要信息」) · text/05-p81-100.txt:332(搜「Maximum marginal relevance」) · text/05-p81-100.txt:350(搜「相反,我们应通过 Prompt 指引」) · text/05-p81-100.txt:357(搜「Making a cup of tea」)
混合搜索词向量及向量知识库text/04-p61-80.txt:534(搜「结合多种搜索算法」)
配对不是因果评估并优化检索部分text/08-p141-160.txt:157(搜「强相关文本段并没有包含答案文本」) · text/08-p141-160.txt:160(搜「今天天」)
两条治法评估并优化检索部分text/08-p141-160.txt:163(搜「优化向量模型或是构建倒排索引」)

Footnotes

  1. 出处:「搭建并使用向量数据库」第 266 至 270 段(text/05-p81-100.txt:266,搜「Chroma.from_documents」;那句注释见 text/05-p81-100.txt:267,搜「为了速度,只选择前 20 个切分的 doc」)。代码块里那三行注释是我们加的,原文只有中间那一句关于 20 块的注释。 2

  2. 出处:「搭建并使用向量数据库」第 272 至 276 段(text/05-p81-100.txt:272,搜「持久化向量数据库」)。原文还在建库前先执行了一句 rm -rf 删掉旧库,并提醒 Windows 用户手动删。

  3. 出处:「搭建并使用向量数据库」第 278 至 280 段(text/05-p81-100.txt:280,搜「向量库中存储的数量:20」)。

  4. 出处:「构建检索问答链」第 11 段与第 38 至 45 段(text/06-p101-120.txt:11,搜「和构建时相同的」;代码见 text/06-p101-120.txt:41,搜「embedding_function=embedding」)。这段代码出现在原书下一章的开头,本章把它挪过来,是因为「存」和「取」拆到两章讲会让读者以为需要两套东西。 2

  5. 出处:「搭建并使用向量数据库」第 134 至 143 段与第 234 段(遍历目录见 text/05-p81-100.txt:136,搜「folder_path = 」;打印出来的文件清单见 text/05-p81-100.txt:143,搜「prompt_engineering」;重新切一次见 text/05-p81-100.txt:234,搜「split_documents(texts)」)。这里要点明书自己的一处不一致: 第 05 章那个 720只切南瓜书那一份 PDF 得到的数(text/05-p81-100.txt:100,搜「split_documents(pdf_pages)」;结果见 text/05-p81-100.txt:103,搜「数量:720」);而建库这一章重新读了目录下的所有文件、又切了一次,这一次切出来多少块,书从头到尾没有打印过。两次用的是同一个变量名 split_docs,很容易读成同一批块。

  6. 这一条是我们核出来的,书没有说过一个字。 证据是两次检索的实际输出:本章第 4 节那次(问「什么是大语言模型」)取回的三块,依次是「虚假知识」「祖孙对话 / 韧性」「第二章 提示原则」(text/05-p81-100.txt:303,搜「虚假知识」;text/05-p81-100.txt:308,搜「祖孙对话样例」;text/05-p81-100.txt:323,搜「如何去使」);原书下一章重开同一个库(数量仍是 20)问「什么是prompt engineering?」,取回的三块同样是提示工程教程的正文(text/06-p101-120.txt:53,搜「什么是prompt engineering」;text/06-p101-120.txt:66,搜「相反,我们应通过 Prompt 指引」)。六块里没有一块出自南瓜书。 顺带说一处矛盾:书打印的文件清单里南瓜书排在提示工程教程前面,但那份清单的路径前缀(./)和代码里写的(../../)对不上,说明它和真正建库的那一次不是同一次运行——以检索结果为准。

  7. 出处:「Embedding 封装讲解」第 368 至 371 段(text/05-p81-100.txt:369,搜「并没有内置所有」)。原文举的内置例子是 OpenAI 和 LLAMA。

  8. 出处:「Embedding 封装讲解」第 375 至 377 段(text/05-p81-100.txt:375,搜「Embeddings 基类」);三个方法的实现依次见第 425、434、447 段(text/05-p81-100.txt:425,搜「def _embed」)。原书这一段里 embed_query 被转码成了 embedquery(少了下划线),代码块里是对的。

  9. 出处:「Embedding 封装讲解」第 460 至 463 段(text/05-p81-100.txt:462,搜「token 限制」)。原文的原话是:「其实也是可以的,embed_query 单独请求也是可以的」「这里只是给出一个简单的 demo」。书在这里很坦白地承认了它给的不是唯一写法,这类坦白值得留着。 2 3

  10. 出处:「搭建并使用向量数据库」第 286 至 289 段(text/05-p81-100.txt:286,搜「Chroma的相似度搜索使」)。原文的原话是:当你需要数据库返回严谨的按余弦相似度排序的结果时,可以使用 similarity_search

  11. 出处:「搭建并使用向量数据库」第 291 至 296 段(text/05-p81-100.txt:293,搜「similarity_search(question,k=3)」)。

  12. 出处:「搭建并使用向量数据库」第 300 至 326 段(三块依次见 text/05-p81-100.txt:303,搜「虚假知识」;text/05-p81-100.txt:308,搜「祖孙对话样例」;text/05-p81-100.txt:323,搜「如何去使」)。表里那三句是我们从每块前 200 个字符里概括的,原文印的是块的原文。第 1 块那个例子的原文写着「我们先给了一个祖孙对话样例,然后要求模型用同样的隐喻风格回答关于『韧性』的问题」,块里的对话是「<孩子>: 请教我何为耐心」(text/05-p81-100.txt:316,搜「请教我何为耐」);本组拆解第 03 章正文用的是「<学生>: 请教我何为耐心」那一组,出自原书正文(text/04-p61-80.txt:296,搜「请教我何为耐」)。两处是同一技巧的两个版本。 2

  13. 出处:「搭建并使用向量数据库」第 330 至 335 段(text/05-p81-100.txt:330,搜「丢失重要信息」;名字见 text/05-p81-100.txt:332,搜「Maximum marginal relevance」)。原文对做法的说法是:在已经选择了一个相关性高的文档之后,再选择一个与已选文档相关性较低但是信息丰富的文档。 2

  14. 出处:「搭建并使用向量数据库」第 337 至 361 段(第 1 块见 text/05-p81-100.txt:350,搜「相反,我们应通过 Prompt 指引」;第 2 块见 text/05-p81-100.txt:357,搜「Making a cup of tea」)。图说里「相关度更低但各自讲不同的事」是我们的观察,书没有对这三块做过任何点评。

  15. 出处:「词向量及向量知识库」第 534 段(text/04-p61-80.txt:534,搜「结合多种搜索算法」)。原文的措辞是「支持结合多种搜索算法(基于词法搜索、向量搜索)的混合搜索,从而提高结果的相关性和准确性」。「为什么要合」那两条理由是我们补的,书没有展开。

  16. 出处:「评估并优化检索部分」第 155 至 161 段(text/08-p141-160.txt:157,搜「强相关文本段并没有包含答案文本」;例子见 text/08-p141-160.txt:160,搜「今天天」)。这一段在原书里出现在第五章讲检索优化的地方,离这一章很远——本组拆解把它提前到这里,理由是:不知道这条性质,读者会把第 4 节那三块的结果误读成「库太小」。 2

  17. 出处:「评估并优化检索部分」第 163 至 166 段(text/08-p141-160.txt:163,搜「优化向量模型或是构建倒排索引」)。原文对倒排索引的说明是:针对知识库的每一个片段构建一个能够表征该片段内容、但和查询的相对相关性更准确的索引,检索时匹配索引和查询的相关性而不是全文。

  18. 出处:「搭建并使用向量数据库」第 291 至 326 段(见脚注 9 与脚注 10 的地址)。「书没有把这两件事联系起来」是我们逐处核过的结论:原书在打印那次「抱歉,我不知道南瓜书是什么」的前后,没有任何一句提到库里有没有南瓜书。