本页目录
Lesson 03 — 向量检索:从相似到 Top-K
本课目标:把第 1 课里 Chroma 帮你做的"检索"这一步拆开。你会先手写一个最朴素的检索,再对比 Chroma 是怎么做的,搞懂 ANN、Top-K、metadata 过滤、距离度量这些核心概念。
上一课你学会了"两个向量算相似度"。这一课的问题是:有 1 万个文档向量,怎么快速找出和问题最相似的那几个?
1. 检索的本质:算距离 + 排序
不管用什么工具,检索的内核都极其朴素:
1. 把问题也变成向量 query_vec
2. 遍历所有文档向量,算 query_vec 和每个的相似度
3. 按相似度从高到低排序
4. 取最前面的 K 个(Top-K)
这就是暴力检索(brute force)。代码不到 10 行,你可以自己写一个。
那为什么还要向量库?
暴力检索的问题在规模:
| 文档数 | 暴力检索每次查询 |
|---|---|
| 100 条 | 几毫秒,毫无压力 |
| 1 万条 | 几十毫秒,还能接受 |
| 100 万条 | 几秒,用户等不及 |
| 1 亿条 | 直接卡死 |
每次查询都要遍历全部算一遍,数据量一大就扛不住。于是有了 ANN(Approximate Nearest Neighbor,近似最近邻)算法——它不追求 100% 找到最精确的答案,而是用很小的精度损失换取巨大的速度提升。
💡 类比:在一本没有目录的字典里查字,你要一页页翻(暴力);如果字典有拼音索引,你直接翻到对应位置(ANN)——可能差几页,但快了几百倍。
ANN 怎么做到的(了解即可)
主流思路有几类: - HNSW(Chroma 默认):把向量组织成多层"图",查询时从顶层快速跳跃逼近,层层细化。 - IVF:先把向量空间聚类成若干"桶",查询时只搜索问题所在的几个桶。 - PQ(乘积量化):把长向量压缩成短编码,用更少内存算近似距离。
这些算法内部很复杂,但你不需要自己实现——向量库(Chroma、FAISS、Milvus 等)已经帮你封装好了。你只要知道:向量库 = 把"算距离+排序"这件事做得又快又能持久化存储。
2. Top-K:取几个?大有大的问题
检索出来一堆按相似度排好序的文档,取前 K 个喂给大模型。这个 K 怎么选?
| K 太小 | K 太大 |
|---|---|
| 可能漏掉相关文档 | 混入大量无关内容(噪声) |
| 答案信息不全 | 干扰大模型,可能答错 |
| 消耗更多 token(更贵、更慢) |
经验值:通常 K 在 3 ~ 5 之间起步,再根据效果调整。关键不是"越多越好",而是"找到的都得相关"——这正是下一课(chunking)和第 6 课(rerank)要解决的。
🎯 认知:检索质量 = 召回率(该找的有没有找到)× 精确率(找到的有没有掺水)。Top-K 大能提升召回但拉低精确。
3. metadata 过滤:先筛后检
光靠向量相似度有时不够。比如你只想查"2024 年的报销制度",但向量检索可能捞出 2022 年的旧制度。
向量库支持给每条文档打元数据(metadata)——就是一组键值对标签:
collection.add(
documents=["餐饮报销上限80元"],
embeddings=[...],
metadatas=[{"category": "报销", "year": 2024}], # ← 标签
ids=["doc_0"],
)
查询时可以加过滤条件:
collection.query(
query_embeddings=[...],
n_results=3,
where={"category": "报销"}, # ← 只在报销类里检索
)
这就是 "先按标签筛选,再在子集里做向量检索"。生产环境几乎必用——能大幅提升精确度、还能控制权限(比如只查自己部门的文档)。
4. 距离度量:余弦 / 欧氏 / 点积
向量库算"相似度"时,可以选不同的度量方式。Chroma 支持三种:
| 度量 | 算什么 | Chroma 里叫 | 适用 |
|---|---|---|---|
| 余弦相似度 | 方向夹角 | cosine |
RAG 最常用,忽略向量长度只看语义方向 |
| 欧氏距离 | 两点直线距离 | l2 |
关心绝对位置时,数值越小越相似 |
| 点积 | 不归一化的余弦 | ip (inner product) |
向量已归一化时等价于余弦 |
为什么 RAG 默认用余弦? 因为不同文本 embedding 出来的向量长度可能不同(长句向量"模长"可能更大),但我们只关心"语义方向是否一致",不关心长度。余弦正好把长度归一化掉。
⚠️ 注意一个反直觉点:欧氏距离和点积是"越小越相似"或"越大越相似"方向相反。Chroma 里
l2返回的 distances 越小越相似,而cosine也是返回距离(1 - 余弦),越小越相似。看结果时别搞反。
5. 本课代码会做什么
code.py 会做四件事,让你把检索彻底搞懂:
① 暴力检索 vs Chroma 检索
用同一批文档和同一个问题,分别用"手写遍历"和"Chroma"检索,对比结果和速度。你会发现小数据量下结果几乎一样,但 Chroma 更省事。
② Top-K 实验
调整 K 值(1/3/5),看检索回来的内容怎么变化,体会"K 大了会混入噪声"。
③ metadata 过滤
给文档打上 category 标签,演示"只在指定类别里检索"。
④ 距离度量对比
分别用 cosine 和 l2 检索同一个问题,看排序结果有没有差异。
6. 跑起来
python lessons/03_retrieval/code.py
终端会打印每一步的对比结果。重点观察: - 暴力检索和 Chroma 的结果是否一致(验证它们做的是同一件事) - K 变大时,排后面的文档是不是明显没那么相关 - metadata 过滤后,结果是不是全都是指定类别
下一课 Lesson 04 — 文档切块 会讲:文档怎么切才合理,切得好检索才准。