向量检索到底在算什么
用大白话讲余弦相似度、L2 归一化和点积,解释向量检索为什么栽在合同编号上,BM25 怎么补上,RRF 如何合并两份排名,以及阈值为什么不能省。
作者 SecureRAG Team · · 10 分钟阅读
向量检索把每段文本变成一个空间里的点,然后返回那些离你的问题最近的段落。机制就这么简单,但它后面所有的长处和短处都从这里长出来,包括那些和“理解”毫无关系的短处。
一句话变成空间里的一个点
嵌入模型读一段文本,吐出的一串数字:本站较小的模型是 384 个,中文优先那一档是 512 个。把这串数字当成坐标来读。一段讲解除条款的文字,落在 384 维空间里的某个位置;一段讲付款条件的文字,落在另一个位置;同一层意思的两种说法,会落到彼此附近,近到可以被算作邻居。
这个空间里的距离,代替了意思上的相近程度。你提问时,问题用同一个模型编码,检索器把所有段落按“离问题这个点有多近”排序,最近的排前面。全程没有查表,没有逐字匹配,就是一次按距离排序。
让它好用的性质,也正是让它危险的性质。换说法是免费的——“我得提前多久说离职”能找到“通知期为三十天”,两句话没有任何共同实词。精确却是要花钱的:一个从没被训练成把 CN-2024-0871 当作有意义字符串的模型,会把这个编号放进由它所切出的那些通用碎片主导的区域里。
余弦相似度为什么能变成点积
教科书上衡量两个向量接近程度的量是余弦相似度,也就是两个向量夹角的余弦:
cos(a, b) = (a · b) / (|a| · |b|)
分子是点积,分母是两个向量的长度。每次比较都要算两个平方根和一次乘法,而一个问题可能要比较上万次。所以工程上的做法不是去算分母,而是把它消掉:每个向量在生成的那一刻就做 L2 归一化,也就是除以自身长度,让长度变成 1。存进去的是单位向量。
两个向量长度都为 1 之后,分母变成 1 × 1 = 1,余弦相似度就退化成了点积:
当 |a| = |b| = 1 时,cos(a, b) = a · b
这就是存储层保存归一化向量的原因:相似度变成一次逐维乘加,快到足以在浏览器标签页里单线程跑完。副作用是,界面上那个“相似度”就是一个普通的点积,取值在 -1 到 1 之间;自然语言的文本一般落在 0 到 1,越大越接近。
向量栽在哪里:编号、数字、条款号
拿 第 3.2 条 或者 CN-2024-0871 去问向量索引,通常它会给你一个看起来合理但是错的答案。三个原因,全是机制层面的:
- 子词切分。 编号不是词。切词器会把
CN-2024-0871切成若干个碎片,这些碎片还大量出现在无关字符串里,结果就是向量被通用部分主导。 - 训练分布。 数字、编号、版本串在嵌入模型的训练句对里占比极低,它们的向量不稳定,稍微换个上下文,点就飘了。
- 实体盲区。 如果周围句子结构相似,“甲方合同”和“乙方合同”的向量可能很接近,因为模型编码的更多是句子的模式,而不是那个词的身份。
落到实用上就是:向量检索擅长“这份文件对通知期是怎么规定的”,不擅长“第 7.4 条写的什么”,后者要的是精确字符串。
BM25 补上的正是这一块
BM25 是经典信息检索里的排序函数,靠词频、逆文档频率和文档长度归一化打分:一个词在你问题里出现、又在少数段落里出现,它就值钱;一个词在每个段落里都有,它几乎不值钱。它完全不懂意思。
不懂意思恰恰是它的价值。BM25 把 7.4 和 CN-2024-0871 当作词元,精确匹配,而这正是向量修不好的那块。两个检索器的失败方式不一样,于是可以互补:
| 提问 | 向量检索 | BM25 |
|---|---|---|
| “离职要提前多久说” | 强——换说法也能匹配 | 弱——共同词太少 |
| “第 7.4 条写的什么” | 弱——数字是噪声 | 强——精确词元 |
| “公司因不可抗力免责” | 强——稠密语义 | 中——需要那个词本身 |
| “错误码 E_SQLITE_BUSY” | 弱——被切成碎片 | 强——精确字符串 |
| “上面那个条款” | 弱——没有实词 | 弱——没有实词 |
最后一行值得看一眼。两个检索器都救不了一个自身不含任何实词的提问,这更像是分块问题,而不是检索问题。
RRF 怎么把两份排名合起来
现在你有两份有序列表,却没法把分数相加:余弦在 0–1 附近,BM25 没有上界,常见是 0–20。倒数排名融合(RRF)绕开量纲问题,直接丢掉分数、只保留名次:
score(d) = Σ_i 1 / (k + rank_i(d))
rank_i(d) 是文档 d 在第 i 份列表里的位次,从 1 开始数;k 是平滑常数,取惯例值 60,也是本站使用的值。实现里因为数组从 0 开始,多加了一个 1,本质是同一个式子平移了一位。
k = 60 的作用是压平列表头部。没有它的话,第 1 名值 1,第 2 名 0.5,第 3 名 0.33,一个自信的检索器就能独占合并结果;有了这个常数,前几名之间拉不开差距,取而代之的是“两个检索器同时认可”胜出。举个算好的例子:
| 文本块 | 稠密排名 | BM25 排名 | 融合得分 |
|---|---|---|---|
| A | 1 | — | 1/61 = 0.0164 |
| B | 3 | 3 | 1/63 + 1/63 = 0.0317 |
| C | — | 1 | 1/61 = 0.0164 |
B 赢过 A:被两个互不相关的检索器同时排到第三,比被其中一个排到第一更有说服力。这就是混合检索存在的全部理由——稠密接住换说法,BM25 接住精确字符串,RRF 奖励两者都认可的东西。
融合之后,流程从候选池里取前 20 个,用 MMR(λ = 0.7)在相关性和多样性之间做取舍,同时用 0.92 的余弦做近似重复剔除——这一步是必需的,因为 15% 的分块重叠本身就会制造出大量互为改写的邻块——最后把最好的 6 块交给回答环节。
阈值不是可选项
说清楚这道门为什么必须存在。拿一个关于期权行权的问题,去问一个全是菜谱的文档库。每一块都不相关。向量检索照样返回六块,因为“最近”是一个全序关系,永远有第一个——某段菜谱可能只有 0.18 分,仍然排在第一。一个不会说“我不知道”的生成模型,会拿菜谱的文字写出一段通顺的回答,而且失败是无声的:答案像模像样,引用也确实来自文档。
门规则只有一条:最好的稠密余弦低于 0.25,并且最好的 BM25 原始分低于 2.0,就判定文档库沉默。两个条件必须同时成立,因为只要有一个真实的关键词命中,或者一个真实的语义匹配,就足以继续往下走。严格模式下,沉默的文档库得到的是拒答,不生成任何文字;宽松模式下也会明确告诉你置信度很低。
两点如实说明。阈值是标定出来的常数,不是物理定律:0.25 这个余弦下限对短问题与短文本块的组合算是合理取值,它在测试过程中被调整过。另外,门只能拦住“自信的胡说”,它没法把一次平庸的检索变好。
用的时候该怎么想
- 把实词写进问题。 问题里带上你领域里的特征词,两个检索器都会受益。
- 要精确串就用精确串。 条款号、错误码、编号是 BM25 的活,它干得不错。
- 把拒答当信息看。 文档库沉默时,下一步有意义的动作是换一种说法,或者把你以为已经在库里的那份文件导进来。