跳到主要内容

向量放哪儿、怎么比 — 「取前 k 条」的甜头和它的账

这一章讲三件事: 那一堆数存在什么东西里、怎么在两次运行之间保住它、 以及一个从第 1 章坑到第 11 章的默认行为 —— 你要几条,它就给你几条,哪怕一条都不相关。 它在全书链条里的位置: 这是六站里的第四、第五站(存 + 取)。 前面三站的产物到这里第一次被真正使用,而这一站的默认行为决定了后面所有章节的天花板。

1. 顶层全景

一堆块的向量


┌── 向量库 ────────────────────────────────┐
│ ① 存下来 │
│ ② 建一套查得快的索引 │
│ ③ 给一个问题的向量,交出最近的前 k 条 │
└──────────────────────────────────────────┘

├─ 落盘:写到磁盘上,下次不用重新算
└─ 重载:从磁盘读回来,接着用

图说:③ 那一行是这一章的全部戏份。
注意它的形状——**输入是「几条」,不是「多像才算」。**

一句话链条: 向量要存起来 → 存了要能查得快 → 查的接口收的是「取几条」→ 所以它永远凑够那几条 → 于是垃圾也会被交上来 → 解药是另加一道门槛,而书没把药和病放在一起说。

2. 向量库管三件事

它解决什么问题

拿一个列表存所有向量,查询时逐个算距离、排序、取前几个 —— 这样也能跑,而且完全正确。

问题是规模。一百条向量这么干没问题;一百万条,每查一次就要算一百万次距离。 而这一站是「每问一次跑一次」的,用户就在屏幕前等着。

它怎么做

三件事,缺一件就不叫向量库:

它管什么具体是什么
把向量连同原文、标签一起放好
建索引事先排好一套查找结构,让「找最近的几个」不必逐条比
取前 k 条给一个查询向量,交出最近的 k 条

书里用的两个

FAISSChroma
出身Facebook AI Similarity Search,Meta 出品1一个公开了源码的向量数据库(就是本节说的向量库,只是叫法不同)2
它是什么一个纯索引库 —— 只管建索引和查,不管别的一个数据库 —— 自带落盘、集合、标签管理
落盘怎么做你自己调「存到这个目录」和「从这个目录读回来」建库时给一个目录名,它自己管

这个差别在下面第 4 节的走查里会变成两行完全不同的代码。

3. 「取前 k 条」为什么必须做成库里的一个动作

这一节回答一个容易被跳过的问题:为什么不自己写个循环排序?

因为分数不出库

你能问的: 「给我最近的 3 条」
你问不到的: 「所有条目各自的分数是多少」

图说:向量库为了快,建的那套索引本来就不遍历全部——
它跳过了大部分候选。所以「全部的分数」这份数据根本不存在,不是它不肯给。

这就是这个接口只能收「几条」、不能收「多像才算」的根本原因。 你要它按门槛筛,它得先把所有条目都算一遍 —— 那正是索引想避免的事。

于是形状定死了后果

接口的形状: 取前 k 条

└─ 它必须返回 k 条 —— 因为它不知道该在哪儿停

└─ 库里只要有 k 条数据,它就凑得出 k 条

图说:这不是 bug,是接口形状的必然结果。
**认清这一点,第 6 节那些「离谱的第二名」就不再是意外了。**

4. 主走查:建库 → 落盘 → 换个脚本重载 → 查

这一节是本章的主走查。上面三件事、下面那个危险参数,都在这条线上占一步。

书里有一处是全书唯一一条从建库一路走到重载再走到查询的完整线,而且它特意分成了两个脚本 —— 这正是真实系统的样子:灌数据是一个程序,查询是另一个程序3

哪个脚本具体的数 / 字串
① 准备三条文档建库脚本一条讲 LangChain、一条讲 FAISS、一条讲 Hugging Face 上的模型
② 各变成一串数建库脚本all-MiniLM-L6-v2,每条 384 个数(第 04 章那一步)
③ 建索引建库脚本FAISS.from_documents(documents, embedding=embedding_model)
④ 落盘建库脚本save_local(folder_path="chapter5_faiss_store") → 打印 Vector Store saved to: chapter5_faiss_store
⑤ 换一个脚本查询脚本这里是关键:第二个程序完全不知道第一个程序做过什么,它只有那个目录
⑥ 重载查询脚本load_local(folder_path="chapter5_faiss_store", embeddings=embedding_model, allow_dangerous_deserialization=True)
⑦ 查查询脚本问「What is LangChain?」,要 k=2
⑧ 结果查询脚本LangChain is a framework for building LLM-powered apps.和 LangChain 无关的那条(见下)

逐步读

第 ②③ 步:注意查询脚本也要建同一个嵌入模型。 重载时那个 embeddings=embedding_model 参数不是摆设 —— 库里存的是向量, 而你的问题还是一句话,得由查询脚本自己把它变成向量。 这也是第 04 章那条「两头必须同一个模型」在代码上的样子。

第 ④ 步:落盘是把索引整个写成文件。 下次不必重新算 —— 而重新算是这条流水线上最贵的一步 (每条文档都要过一遍模型)。

第 ⑥ 步那个又长又吓人的参数,下一节专讲。

第 ⑧ 步是全章的要害。 我们要的是「What is LangChain?」, 而第二条返回的是库里另一条完全无关的文档 —— 它讲的是一类叫 Transformer 的模型 (今天几乎所有语言模型的内部构造都属于这一类,你在模型名和论文里会反复撞见这个词)。

那条文档里还有一个词是自然语言处理:让机器读、写、理解人类语言这门行当,英文缩写叫 NLP

原文如下:

Transformers from Hugging Face are widely used in NLP.

它和 LangChain 没有任何关系。 它被交出来的唯一原因是:我们说了要 2 条。

库里一共只有 3 条文档,第 2 名是「剩下两条里比较不那么差的那条」。 它不是「相关的第二名」,它是「排序的第二名」——这两件事完全不同,而接口分不出来。

5. 那个被照抄了四次、零解释的危险参数

allow_dangerous_deserialization=True 这个参数在书里出现四次,一次解释都没有4。 它长得像个样板参数,读者会照抄。

它到底在说什么

它的意思是:「我知道加载这个索引文件可能会在我的机器上执行任意代码,我信任这个文件。」

落盘时: 内存里的对象 ──► 写成文件(这个过程叫「序列化」)
重载时: 文件 ──► 还原成内存里的对象(叫「反序列化」)

Python 用的那种通用还原方式,允许文件里带上「还原时该跑哪段代码」的指令。
于是:**一个精心构造的索引文件,可以在你加载它的那一刻运行任何东西。**

图说:这不是理论风险。所以这个库默认拒绝加载,逼你手动写上这个参数,
等于让你签一份字:出了事你知情。

判据一句话

这个索引文件从哪来能不能加
你自己的程序刚刚生成的
从同事那儿拷来的、从网上下载的、用户上传的不能 —— 重新生成一份,别加载

书用了四次,一次都没提这件事。

6. 库会越来越脏:第 01 章那笔账在这里结清

现象:同一条文档返回了两遍

书里有两个配方的输出长这样5:

某个配方的搜索结果:
FAISS performs fast vector searches.
FAISS performs fast vector searches.

另一个配方的搜索结果:
1. ChromaDB is an open-source vector database for AI applications. (source: wiki)
2. ChromaDB is an open-source vector database for AI applications. (source: wiki)

图说:两条一模一样。而这两个程序各自只准备了三条不同的文档。

原因:落盘目录不会自动清空

第一次运行: 建库(目录里 0 条) → 灌进 3 条 → 目录里现在有 3 条
第二次运行: 连上同一个目录(已有 3 条) → 又灌进同样的 3 条 → 目录里现在有 6 条
第三次运行: → 9 条

图说:那个「加文档」的动作只管加,不去重、不覆盖。
作者调试时跑了几遍,库里就有几份重复。

这就解释了第 01 章那个错答案

第 01 章最后那一步:六站全走通,问「这份文档在讲什么」, 答案却是「这份文档似乎是在讲 LangChain」—— 而输入的 PDF 讲的是 RAG。

原因是同一个: 那个配方用的落盘目录,和前面几个配方用的是同一个; 里面还躺着更早那个配方灌进去的四条讲 LangChain 的样例句6

怎么防

做法什么时候用
每次重灌前先删目录开发调试时最省事
给每条文档一个稳定的 id,灌入时按 id 覆盖生产环境的标准做法 —— 同一份文档更新了,替换而不是再加一份
灌完打印一下总条数最便宜的自检:数字对不上就说明脏了

书这三条一条都没提,它甚至没有注意到自己的输出重复了。

7. 那个从头坑到尾的默认行为:它永远凑够 k 条

这一节是全书最要紧的一条,而书从来没有正面说过。

病症

第 4 节的走查已经给了第一个现场。书里还有一地证据,这里只举分数最刺眼的那次7:

查询: How does RAG reduce hallucinations?
语料: 八条短文档

逐条的相似度(余弦,范围 −1 到 1):
0.6190 RAG reduces hallucinations by grounding answers in retrieved evidence. ← 真正的答案
-0.0185 Dense retrieval uses vector embeddings instead of keyword matching.
0.0139 FAISS is a fast vector similarity library from Meta.
-0.0189 BM25 is a sparse retrieval method based on term frequency statistics.
-0.0296 Sentence-Transformers provides easy embedding models for sentences.
-0.0428 Vector databases store embeddings to enable semantic search.
-0.0015 Cooking recipes often require precise measurements and timing.
0.0383 Traveling to new countries helps you learn about culture and history.

图说:八条里只有第一条真的回答了问题(0.6190)。其余七条全部挤在 0 附近,
其中五条还是**负的**——方向已经相反了。

现在假设你按常规写法取前 3 条:

第 1 名 0.6190 RAG reduces hallucinations by grounding answers… ← 对
第 2 名 0.0383 Traveling to new countries helps you learn about culture and history. ← **出国旅行**
第 3 名 0.0139 FAISS is a fast vector similarity library from Meta.

图说:第二名是「出国旅行长见识」。它和「RAG 怎么减少胡编」毫无关系,
分数只有第一名的十六分之一——可它照样被交给了模型。

0.0383 是什么概念? 上一章说过,真实语料上 0.6 就算很像了; 而 0.04 基本等于「两句话之间没有任何关系」。

为什么这件事比它看起来严重

因为下一站会把这三条原样塞进提示。

模型看到的是三段并列的、看起来同等重要的资料。它没有办法知道第二、三条是凑数的 —— 提示里没有分数,只有文字。

于是两种坏结果:轻则答案被无关内容带偏,重则模型拿「出国旅行」当依据编出一段话。

8. 解药就在同一本书的另一章:先设一道门槛

书里有这味药,而且用得很对。它只是从没和上面那个病放在一起说过 —— 这个连接是我们做的。

做法

在取前 k 条之后、交给模型之前,加一道判断:低于某个分数的一律丢掉。

用上一节那八条跑一遍,门槛设 0.5,书印出来的结果是8:

Filtered Results (similarity > 0.5):
similarity=0.6190 | RAG reduces hallucinations by grounding answers in retrieved evidence.

图说:八条候选,只有一条过线。**其余七条全部被丢掉,包括「出国旅行」那条。**
注意这时候交给模型的是一条,不是三条——**它敢交白卷。**

为什么这么做有效

因为它换了一个判据。

取前 k 条加一道门槛
判据是什么名次 —— 你比别人强就行绝对分数 —— 你得真的够像
库里全是垃圾时交出 k 条垃圾交出 0 条
谁来决定交几条你(写死的 k)数据(过线几条就是几条)

「敢交出 0 条」是这一步全部的价值。 系统说「我没找到」,比编一段话有用得多。

但别把这道门槛和后面那道搞混:这一道拦的是候选资料 —— 分数低的块压根不进模型,模型看都看不到它。 后面还有一道拦的是答案本身 —— 资料已经进去了、话也已经生成出来了, 再判断这段话有几分底气。两道拦的是两样东西,第 09 章第 5 节专门划这条界。

边界:0.5 这个数不是普适的

门槛必须自己调,而且没有一个通用值。 第 04 章最后那个例子已经说明了原因: 块越小,块与块之间、块与查询之间的相似度整体越低。 同一个 0.5, 在 500 字符的块上可能筛掉一半,在 80 字符的块上可能一条都不剩。

定门槛的实用办法: 拿几十条你知道正确答案的问题跑一遍, 把「正确答案的分数」和「明显无关文档的分数」两堆数列出来,门槛画在两堆中间。 这一步属于评测,第 12 章讲。

判断(我们的,不是书里的): 我们认为「永远凑够 k 条」是这本书里危害最大的一处沉默 —— 不是因为它难,而是因为它的表现是「一切正常」:程序不报错、结果有三条、格式很漂亮。 而书在十一章里印了至少五次这个病的现场(负分、并列零分、出国旅行、不相干的 Transformers), 一次都没有把它们指认出来。 如果错,会错在: 如果作者认为「筛掉不相关的」属于下一层(生成层)的职责, 那么把这件事留到后面讲是合理的分工。但书在后面也没有讲 —— 那个用门槛的配方 出现在讲检索的第 9 章,而且是当成一种「语义过滤技巧」介绍的,不是当成解药。

9. 元数据过滤有两种相反的顺序,而书两种都用过、从不对比

这是本章第二处另起的走查,因为它落在另一条线上。

第 02 章说过,标签是在加载那一站挂上的,用途之一是「只在符合条件的文档里搜」。 做这件事有两种顺序,后果完全相反。

顺序一:先检索,再过滤

书在第 1 章用的是这种9:

三条文档,其中两条的标签是 category=LangChain

similarity_search(query="What is RAG?", k=5, filter={"category": "LangChain"})

├─ 先按相似度取回一批
└─ 再把标签不符的扔掉

输出:只返回了 2 条(要的是 5 条)

图说:**它会漏。** 假如库里有 100 条 LangChain 文档,
而相似度前 5 名里只有 2 条带这个标签,那另外 98 条就再也没机会了。

顺序二:先过滤,再检索

书在第 9 章用的是这种10:

先按标签筛出子集 → 再在子集里逐条算相似度 → 取前 k

查询:How does RAG reduce hallucinations? 过滤条件:category=AI 且 author=Alice
输出:
similarity=0.6190 | RAG reduces hallucinations by grounding answers in retrieved evidence.
similarity=-0.0428 | Vector databases store embeddings to enable semantic search.

图说:**它会凑数。** 子集里有几条就在几条里排名次,
于是相似度 **−0.0428**(方向相反)的那条照样被交了出来。

怎么选

先检索后过滤先过滤后检索
风险 —— 符合条件的好文档排不进前 k凑数 —— 子集太小就把不相关的也交出来
快不快快 —— 用得上索引慢 —— 子集通常要逐条算
什么时候用标签只是锦上添花(比如「优先今年的」)标签是硬约束(比如「只能看这个部门有权限的」)

权限这类场景只能用第二种 —— 第一种会让不该看的东西先被检索到再被扔掉, 而那意味着它曾经出现在内存里。

书两种都用过,从没有对比过一句。

10. 作者的判断与证据

书里给了证据的:

说法证据
向量库能把索引落盘、再从别的程序读回来配方 53 那两个脚本与 Vector Store saved to: 那一行
相似度检索能按意思排名次配方 53 的第 1 名确实是关于 LangChain 的那条
门槛能筛掉不相关的配方 95 的八条分数与「只剩一条」的输出
元数据能限定检索范围配方 13 的输出:k=5 而只返回两条带标签的
Chroma 自带落盘配方 56 建库时只给一个目录名,不用再手动存

作者只是断言、没有给证据的:

说法缺什么
「持久化向量库在生产环境里尤其有价值」没有一处量过「重建索引要多久」,所以「省下的」是多少无从判断
那个配方标题叫「混合搜索 + 元数据过滤」书用「混合」指的是「语义 + 标签过滤」;而业界说混合检索指的是另一件事(按意思找 + 按词面找,第 06 章讲)。同一本书里这个词有两个意思
有一个配方说要「把块和它的摘要都嵌进去、都参与检索」代码里嵌的只有原块,摘要只是被塞进标签里,从未被嵌入、从未被检索
有一个配方说「多个嵌入可以同时算」代码里同时做的是读文件那一半,真正耗时的嵌入计算仍是一次性排队跑完

11. 边界与局限

  • 书从没说过「取前 k 条永远凑够 k 条」,尽管它印了至少五次现场。
  • 书从没把门槛和这个病联系起来。 那个用门槛的配方在第 9 章,当成一种「语义过滤技巧」介绍。
  • allow_dangerous_deserialization=True 用了四次,零解释。
  • 两处输出里同一条文档出现两遍,书没有注意到,更没有讲「重复灌库」这个生产事故。
  • 两个配方的代码跑不起来: 用了一个类名,而这两段代码的导入区里根本没有它,直接就是找不到名字的错误。
  • 「混合搜索」这个词在同一章里被用成了两个意思。 出门撞见时按业界口径理解 (按意思 + 按词面),不是书里那个「语义 + 标签」。
  • 没有一处讨论索引怎么更新: 文档改了怎么办、删了怎么办、增量灌入怎么做。
  • 没有一处讨论多大规模会出问题。 全书最大的库是 1000 条随机向量(第 07 章讲)。

12. 可带走的

全章那条走查,一行写完: 三条文档 → 各变成 384 个数 → 建索引 → save_local("chapter5_faiss_store")换一个脚本load_local(..., allow_dangerous_deserialization=True) → 问「What is LangChain?」、要 2 条 → 第 1 条对,第 2 条是「Transformers from Hugging Face are widely used in NLP.」—— 因为我们说了要 2 条。

  1. 向量库管三件事:存、建索引、取前 k 条;FAISS 是纯索引库,Chroma 多管一件落盘;
  2. 「取前 k 条」这个接口只收「几条」,不收「多像才算」 —— 因为索引本来就不遍历全部, 所有条目的分数这份数据不存在;
  3. 所以它永远凑够 k 条 —— 交出来的是「排序的第 k 名」,不是「相关的第 k 名」;
  4. 书里最刺眼的一次: 八条候选里五条是负分,而按前 3 名取, 第二名是「出国旅行长见识」(0.0383,只有第一名的十六分之一);
  5. 解药是加一道门槛:低于某个分数一律丢掉 —— 它的全部价值是敢交出 0 条;
  6. 门槛没有通用值,块越小整体分数越低;定门槛要靠几十道已知答案的题去量(第 12 章);
  7. allow_dangerous_deserialization=True 是一份知情同意书 —— 别人给的索引文件不要加载,重新生成一份;
  8. 落盘目录不会自动清空,重复运行会把同一批数据一遍遍灌进去 —— 第 01 章那个「答案在讲 LangChain」就是这么来的;
  9. 防脏库最便宜的一招:灌完打印一下总条数;生产做法是按稳定 id 覆盖;
  10. 元数据过滤有两种相反的顺序:先检索后过滤会漏,先过滤后检索会凑数 —— 权限这类硬约束只能用后者。

13. 原文地图

主题原书章原文位置
关键词搜索的死角、语义检索的立论CHAPTER 5 Vector Stores for Semantic Retrievaltext/12-fm-introduction.txt:9(搜「similarity to be measured by meaning rather than exact words」)
FAISS 的出身CHAPTER 4 Embedding Strategies for Vector Retrievaltext/11-fm-introduction.txt:281(搜「Facebook AI Similarity Search」)
Chroma 是什么CHAPTER 5 Vector Stores for Semantic Retrievaltext/12-fm-introduction.txt:624(搜「ChromaDB is an open-source vector database designed specifically」)
建库 → 落盘 → 重载 → 查(两个脚本)同上text/12-fm-introduction.txt:271(搜「vectorstore.save_local(folder_path=save_dir)」) · text/12-fm-introduction.txt:309(搜「allow_dangerous_deserialization=True」) · text/12-fm-introduction.txt:337(搜「LangChain is a framework for building LLM-powered apps」) · text/12-fm-introduction.txt:339(搜「Transformers from Hugging Face are widely used in NLP」)
同一条文档返回两遍同上text/12-fm-introduction.txt:618(搜「FAISS performs fast vector searches.」) · text/12-fm-introduction.txt:758(搜「ChromaDB is an open-source vector database for AI applications. (source: wiki)」)
Chroma 的落盘目录与「加文档」同上text/12-fm-introduction.txt:692(搜「persist_directory = "./chroma_db_offline"」) · text/12-fm-introduction.txt:726(搜「vectorstore.add_documents(docs)」)
八条候选的逐条分数CHAPTER 9 Effective Search for RAG Systemstext/16-fm-introduction.txt:898(搜「--- All Docs with Scores ---」) · text/16-fm-introduction.txt:917(搜「Traveling to new countries helps you learn about culture and」)
门槛筛完只剩一条同上text/16-fm-introduction.txt:884(搜「semantic_filter(query, DOCS, threshold=0.5)」) · text/16-fm-introduction.txt:924(搜「similarity=0.6190」)
先过滤后检索,第 2 名是负分同上text/16-fm-introduction.txt:1562(搜「match = all(doc.get(key) == value for key, value in metadata_filters.items())」) · text/16-fm-introduction.txt:1626(搜「similarity=-0.0428」)
先检索后过滤,k=5 只回 2 条CHAPTER 1 Foundation of Retrieval-augmented Generationtext/08-fm-introduction.txt:1502(搜「filter={"category": "LangChain"}」) · text/08-fm-introduction.txt:1540(搜「In RAG LangChain is used to build RAG applications.」)
「混合搜索」被当成「语义 + 标签」用CHAPTER 5 Vector Stores for Semantic Retrievaltext/12-fm-introduction.txt:341(搜「Hybrid search with metadata filtering」)

Footnotes

  1. 出处:「CHAPTER 4 Embedding Strategies for Vector Retrieval」第 281 段(text/11-fm-introduction.txt:281,搜「Facebook AI Similarity Search」)。原文说它是 Meta AI 开发的高性能库,用来做稠密向量的相似检索与聚类。这是书里少有的直接交代来历的地方。

  2. 出处:「CHAPTER 5 Vector Stores for Semantic Retrieval」第 624 段(text/12-fm-introduction.txt:624,搜「ChromaDB is an open-source vector database designed specifically」)。原文强调它是专为机器学习与 RAG 场景做的,不是通用数据库。

  3. 出处:同章第 271 段(text/12-fm-introduction.txt:271,搜「vectorstore.save_local(folder_path=save_dir)」)、第 277 段(text/12-fm-introduction.txt:277,搜「Vector Store saved to: chapter5_faiss_store」)、第 309 段(text/12-fm-introduction.txt:309,搜「allow_dangerous_deserialization=True」)、第 337 段(text/12-fm-introduction.txt:337,搜「LangChain is a framework for building LLM-powered apps」)、第 339 段(text/12-fm-introduction.txt:339,搜「Transformers from Hugging Face are widely used in NLP」)。三条输入文档写在第 247–251 段(text/12-fm-introduction.txt:251,搜「Transformers from Hugging Face are widely used in NLP.」)。

  4. 出处:同章第 309 段(text/12-fm-introduction.txt:309,搜「allow_dangerous_deserialization=True」);另外三处见「CHAPTER 4 Embedding Strategies for Vector Retrieval」第 910 段(text/11-fm-introduction.txt:910,搜「allow_dangerous_deserialization=True」)。补充(不在书里,来自通用知识): 这个开关对应的是 Python 的通用对象还原机制,它允许被还原的数据里带上「还原时执行哪段代码」的指令;所以加载来路不明的索引文件等于运行来路不明的程序。

  5. 出处:同章第 618 段(text/12-fm-introduction.txt:618,搜「FAISS performs fast vector searches.」)与第 758 段(text/12-fm-introduction.txt:758,搜「ChromaDB is an open-source vector database for AI applications. (source: wiki)」)。后一个配方建库时指定了落盘目录(text/12-fm-introduction.txt:692,搜「persist_directory = "./chroma_db_offline"」),然后调「加文档」(text/12-fm-introduction.txt:726,搜「vectorstore.add_documents(docs)」)—— 这个动作只管加,不去重。

  6. 出处:「CHAPTER 1 Foundation of Retrieval-augmented Generation」第 841 段(text/08-fm-introduction.txt:841,搜「RAG is a polpular framework」)—— 那是更早的配方灌进 chroma_vector_store 的四条样例句之一;而第 01 章末尾那个完整流水线用的是同一个目录(text/08-fm-introduction.txt:1662,搜「persist_directory="chroma_vector_store"」)。

  7. 出处:「CHAPTER 9 Effective Search for RAG Systems」第 898 段起(text/16-fm-introduction.txt:898,搜「--- All Docs with Scores ---」);「出国旅行」那条的分数(0.0383)在第 917 段(text/16-fm-introduction.txt:917,搜「Traveling to new countries helps you learn about culture and」)。八条语料写在第 803–823 段(text/16-fm-introduction.txt:820,搜「Cooking recipes often require precise measurements and timing.」)。

  8. 出处:同章第 884 段(text/16-fm-introduction.txt:884,搜「semantic_filter(query, DOCS, threshold=0.5)」)与第 924 段(text/16-fm-introduction.txt:924,搜「similarity=0.6190」)。书把这个配方叫「语义过滤」,放在讲检索技巧的一节里,没有和前面那些「凑数的第二名」联系起来。

  9. 出处:「CHAPTER 1 Foundation of Retrieval-augmented Generation」第 1502 段(text/08-fm-introduction.txt:1502,搜「filter={"category": "LangChain"}」)与第 1540 段(text/08-fm-introduction.txt:1540,搜「In RAG LangChain is used to build RAG applications.」)。要的是 5 条,输出只有两条 —— 因为三条文档里只有两条带这个标签。

  10. 出处:「CHAPTER 9 Effective Search for RAG Systems」第 1562 段(text/16-fm-introduction.txt:1562,搜「match = all(doc.get(key) == value for key, value in metadata_filters.items())」)—— 这一行先按标签筛出子集;结果在第 1624–1626 段(text/16-fm-introduction.txt:1626,搜「similarity=-0.0428」)。