《提示与上下文学习》说提示是在模型内部检索最匹配的区域; 这一章把检索搬到模型外面——词向量的句子版:整段话压成一个向量, 只要能把"意思"变成坐标,搜索就退化成了一个几何问题。
最直觉的"算相似度"做法是:把查询和每一篇文档拼在一起,喂给模型,让它输出一个分数。 这确实最准,但完全不可用——你有 100 万篇文档,就得跑 100 万次模型前向。
向量检索换了个做法:用两个
两种架构的对比
因为两个
文档的向量可以提前算好、存起来。你半夜把 100 万篇文档全部编码一遍,
存进一个索引文件。第二天用户来搜索时,只需要编码那一条查询,
然后在预存的向量里找最近的几个。
这就是双塔的全部价值:把"每次查询都算一遍"变成了"离线算一次 + 在线比距离"。
想象图书馆找书。
交叉
双塔的做法是:提前给每本书贴好一张"内容坐标"标签(离线做完),
读者说出需求时也换算成一个坐标,然后直接在书架里找坐标最近的几本。
代价是:坐标标签毕竟是摘要,会丢信息。所以双塔精度略低,
但速度快了好几个数量级——这就是它成为主流的原因。
这个类比管到「提前贴标签」为止:图书馆的标签是人定的、能读懂,
嵌入的坐标是模型从数据里学出来的,没有哪一维能被单独指认成「动物性」。
互动 · 双塔为什么快:要给几个句子两两打分
纵轴是一共要跑多少次模型前向(对数刻度)。把两个句子拼起来打分,n 个句子要跑 n(n−1)/2 次(约 n²/2);
双塔每个句子只跑一次,之后全是内积。一万句话:交叉
两个向量怎么比"像不像"?最常用的是余弦相似度——看两个箭头的夹角,
而不看它们的长度。在
它算出来是一个 −1 ~ 1 的数,越接近 1,两根箭头越同向、意思越近。式子长这样:
这个式子里藏着一个极其实用的推论。看分母:‖a‖ · ‖b‖
是两条箭头各自的长度。
如果你在建索引之前就把所有向量都除以自己的长度(L2 归一化),
分母就恒等于 1——余弦相似度退化成一次点积。
这不是优化技巧,是一个恒等式。而它带来的后果很大:
点积能直接用 GPU 的矩阵乘算,带除法的余弦不能——
几亿条向量的检索能跑得起来,就靠这一步提前归一化。
互动 · 真实的余弦相似度矩阵
这张矩阵是全章的地基:把「意思」变成坐标以后,「像不像」就只剩一件事——两个箭头夹角有多小。 点左边任意一句,看它那一行/列亮起来;越亮的格子,夹角越小、意思越近。 后面几节讲的索引、维度、各向异性,都是在这张图上加省钱的装置。
真实的嵌入模型(BGE、E5 这类)是在上亿文本对上训练出来的, 浏览器里跑不动。所以这一章用的是沿着 8 个语义维度手工构造的嵌入—— 比如「动物性」「体型」「科技感」「情绪」。
这不会让结论变假:余弦相似度的计算、矩阵的对称性、 归一化之后的等价性,都是真的。变的只是"这些坐标从哪来"。 真实模型做的事情,本质上就是学出一组更好的维度。
互动 · 词的空间:「国王 − 男人 + 女人」落在哪
有了向量之后,"找最相近的几个"这个问题,有从笨到快的三条路。 下面反复出现的召回率,说的都是「真正最相近的那些被找回了几成」—— 右边互动里的「召回率 @ 10」就是"真正最近的 10 个里找回了几个"。
| 对比项 | 暴力搜索 Flat | IVF 倒排文件 | HNSW 分层可导航小世界 |
|---|---|---|---|
| 做法 | 和每一个向量都算距离 | 先聚类(按距离把相近的向量归堆)成几千个桶,按聚类中心建一张倒排表(每个桶里存了哪些向量的目录),查询时只搜最近的几个桶 | 建一张多层近邻图,从顶层贪心往下走 |
| 复杂度 | O(N) | O(nprobe · N / k) k = 桶数 nprobe = 每次查几个桶 |
O(log N) |
| 召回率 | 100%(精确) | 与桶数相关,边界处易漏 | 可调,通常 95%+ |
| 支持增量插入 | 天然支持 | 可以先插入;分布漂移后再重新聚类 | 支持 |
| 用在哪 | 小于 10 万条时 | 超大规模、批量场景 | 今天绝大多数向量数据库的默认 |
想象你要从一堆城市里找离目标最近的那个。
暴力法:从每座城市出发量一次距离。
HNSW:先建一张分层地图——顶层只有几个大城市(稀疏),
往下每层城市越来越多,最底层是全部城市。
查询时:先在顶层找到大概方向(跳得远但很粗糙),
逐层下降,每一步都细化,最后在最底层做细致搜索。
这就是"要 O(log N) 而不是 O(N)"的来源——和跳表(一种分层链表:上层稀疏、下层密集,查找时先在上层大步跳)是同一个思想。
互动 · HNSW 索引与检索(统计访问节点数)
ef_search 是 HNSW 最重要的参数:它决定"在最底层要看多少个候选"。
· ef 很小 → 访问节点少、快,但会漏掉真正的最近邻
· ef 很大 → 召回率接近 100%,但访问节点数逼近暴力搜索,HNSW 就没有意义了
把滑块从左拉到右,看这两个读数怎么权衡。
实际工程里通常取 ef = 50~100:访问节点数降到原来的 20%~33%,召回率还能保住 95% 左右——
这个交换比才是 HNSW 值钱的地方。
理论上维度越高能装的信息越多。但实际上:
① 存储和计算成本线性增长——1000 万条 × 3072 维(今天不少模型的默认)× 4 字节 = 123 GB,光存就受不了;
压到常见的 1024 维,也还要 41 GB。
② 高维空间里"距离"会失去区分度(维数灾难):
当维度趋近无穷时,任意两个随机点的距离都会趋近同一个值,"最近邻"就没有意义了。
所以在效果和成本之间,业界常见的生产默认是 384 ~ 1536 维(也有 3072、4096 的模型,那是拿成本换效果)。
下面那张互动图里有三个控件在互相拉扯:维度 D(每个向量多长)、 共同方向强度(所有向量有多"抱团"、一起偏向同一个方向的程度), 以及最后那个「是否减掉均值」开关。把鼠标移到下面公式里的符号上,对应的控件会亮起来。
鼠标停在 μ 上——下面那个「是否减掉均值」按钮会亮。 公式里的那个减号,就是那个按钮在做的事。
互动 · 各向异性:为什么"相似度 0.8"可能毫无意义
真实的嵌入模型有一个叫各向异性(anisotropy)的问题:
所有向量都被挤在空间里的一个很窄的锥形区域里,而不是均匀分布在整个球面上。
后果是:任意两句话的余弦相似度都偏高——把共同方向强度拖到 1.15(默认值),
不相关的一对也常跑到 0.68~0.82(右边读数能直接看到)。
于是你设的阈值 0.8 筛出来的根本不是"相关",而是"所有东西"。
为什么会这样:语言本身有很强的共同成分(语法、常见词、通用语义),
而且训练目标里高频词主导了梯度,模型学到的向量都被拉到一个共同方向上。
怎么缓解:减掉均值(把整个向量集合减去它们的平均向量)再归一化——
这一步叫 centering(中心化),能显著改善检索效果。
完整的 whitening(白化)还要再做一个线性变换把协方差也拉平,centering 只是它的第一步。另一个实用做法是不要用绝对阈值,
而是用"相对排名"(取 top-k)来筛选。
| 代际 | 代表 | 关键变化 |
|---|---|---|
| 静态词向量 | Word2Vec · GloVe | 一个词一个向量,多义词只能取一个平均值;整句要用词向量平均这类弱基线来拼(见阶段 3) |
| 上下文化 | ELMo · BERT | 同一个词在不同句子里有不同向量。但要用它做相似度,得额外拼一个池化层 |
| 句向量 | Sentence-BERT | 专为"算相似度"训练的句子 |
| 对比学习大爆发 | SimCSE · E5 · BGE · GTE | SimCSE 用 dropout 把同一句话造出两个视图,不需要任何外部数据; E5 / BGE / GTE 才用大规模弱监督数据(标题-正文、问答对)做对比学习, 通用检索能力大幅提升 |
| 多语言与长文本 | multilingual-E5 · BGE-M3 · jina-embeddings | multilingual-E5 跨语言强,但最大长度只有 512 token; BGE-M3 · jina-embeddings-v2/v3 支持 8192 token;BGE-M3 还能稀疏 + 稠密混合输出 (一个稠密向量给语义、一个稀疏向量给关键词,两份一起用) |
| API 与专用模型 | Cohere Embed · OpenAI text-embedding | 托管服务,免运维;但换模型必须重建索引,迁移成本要提前算 |
| LLM 基座嵌入 | Qwen-Embedding · NV-Embed | 直接用大模型做 |
前面几节全是密密麻麻的数字和一个大矩阵。可「相似度 0.83」到底是个什么动作? 它其实只有一件事:两个箭头之间的夹角有多小。下面把它真的算一遍—— 公式里每一个符号,图上都有一块看得见的东西。
互动 · 相似度就是夹角的余弦;把箭头拉长,它一点不变
公式卡 · 三个符号,一张几何图
互动 · 三种算「像不像」的方法:只有余弦不管长短
横轴是把右边那根箭头拉长了多少倍。内积(点积)跟着线性涨,余弦是一条水平线—— 因为分母把长度除掉了。这就是「先归一化」的全部理由:不归一化,长的句子天生「更像」。
公式卡 · 那个反直觉的坑:所有箭头挤在一个锥体里
互动 · 检索一层一层省:全扫 / 分桶 / 小世界图
纵轴是一次查询要比多少次距离(对数刻度)。一百万条向量时:全扫一百万次, 分桶约八千次,小世界图约两百次。第 3 节那三层境界,差别就是这条纵轴。
互动 · 分桶为什么省:只扫几个桶
互动 · 维度越高,随机两个向量越接近垂直
| 问题 | 具体表现 | 什么时候真的会痛 → 换什么 |
|---|---|---|
| 换模型 = 必重建索引 | 向量空间是模型私有的。用 A 模型建的索引,换成 B 模型后完全失效—— 两个空间的坐标含义不同,距离没有可比性。 | 上线后想换更强的模型 → 全库重算,千万级要几小时到几天(下面那个滑块)。 把选模型当成长期决策;想把新旧空间对齐(蒸馏、投影)能做,但精度会掉,生产里通常直接重算。 |
| 长文本被截断 | 嵌入模型有最大长度(常见 512 或 8192 token)。超出部分被直接丢掉, 很多库还会静默截断,你会以为它读完了。 | 文档比窗口长 → 先切块逐块编码;再取各块最高分(或均值)作为整篇得分合回去。 |
| 领域不匹配时断崖式下跌 | 通用模型在医学、法律、代码上表现常常很差:这些领域的"相近"和日常语言不是一回事。 | 专业语料 → 换成在该领域训练过的模型,或做领域微调。 |
| 多语言能力差异巨大 | 同一个模型在英语上好用,在小语种上可能接近随机。 | 跨语言检索(中文查询、英文文档)→ 选多语言模型,并单独评测小语种。 |
| 阈值不可跨模型迁移 | "相似度 > 0.8 就算相关"这个经验,换个模型就完全不准—— 每个模型的相似度分布尺度都不一样(各向异性程度不同)。 | 要一条确定的判定线 → 别用绝对阈值,用相对排名(top-k); 你手里那个 0.83 只拿来排先后,不用来判断"相不相关"。 |
| 对专有名词和代码符号很差 | 向量检索擅长"语义相近",抓不住精确匹配。 「GPT-4o」和「GPT-4O」在语义空间里几乎一样。 | 要找精确型号、报错码 → 配合关键词检索(《RAG 检索增强》的混合检索)。 |
互动 · 换一个嵌入模型,重编码要多久
回到第 4 节那个「各向异性」互动。 把第二个滑块「共同方向强度」从 0 拖到 1.6,慢慢拖。
盯住右侧读数里的 「相似度均值」和「相似度 > 0.7 的比例」: 它们会跟着往上爬。等到共同方向强度很高时, 一大堆本来不相关的向量,相似度都在 0.7 以上。
那个瞬间你看到的就是「阈值 0.8 筛出来的根本不是相关,而是所有东西」。
然后再把 「是否减掉均值」那个按钮从「原样」切到「减掉均值」——
相似度均值和那个比例会明显掉下来。那个按钮,就是 centering(中心化)。
如果你刚才拖的时候只是觉得「数字在变」,那这一节对你就是没用的
——回到第 4 节,先看那个「> 0.7 的比例」再看均值。
| 暗线 | 这一章的回答 |
|---|---|
| A 信息流动 | 一句话 → 一串定长数字。查询和文档走两个不同的塔,吐出来的向量却在同一个空间里——这是能直接算相似度的前提。 |
| D 跑在什么上 | 建索引时算力受限,检索时带宽受限:最贵的是搬数,不是算点积,把 float32 量化到 int8(字节少到四分之一)吞吐就能翻几倍。 见 《硬件与算力账本》里那张判断瓶颈在算力还是带宽的 Roofline 图。 |
| E 它假设了什么 | 「语义相似 = 夹角小」是地基;最致命的是「检索出来的就是该看的」——检索错了,后面生成得再好也没用(下一章展开)。 |
向量检索 = 把"意思相近"翻译成"距离很近"(向量离线算好,只编码查询),再用 HNSW 把百万次比较压到几百次。
最大的坑是各向异性——向量挤在窄锥里,"相似度 0.8"这个阈值不可信,要看相对排名。
它接住《提示与上下文学习》——把模型内部的检索搬到外面。
上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。