Embedding 的三个代价
选 Embedding 模型时,大家第一眼看的是榜单分数。但真正会在项目里咬人的,是分数旁边没人提的三件事:换模型的代价、维度的代价、以及"相似度"到底在比什么。
1. 代价一:换模型等于全库重灌
Embedding 模型把文本压成一个固定维度的向量。查询向量和文档向量必须来自同一个模型——否则两个向量处在不同的坐标系里,算出来的距离毫无意义,就像用英里去减公里。
这意味着:换模型,全量重灌。
重灌的成本不只是时间和算力,还有三个连带问题:
- 重灌期间索引处于混合状态,一半是旧向量一半是新向量,检索结果不可信。要么建新索引然后切换,要么接受这段时间的混乱。
- 模型名和版本必须写进配置并一起版本化,不能散落在代码里。否则半年后没人知道库里的向量是哪个模型生成的。
- 维度一旦变了,表结构、索引、缓存全要跟着改。
所以选模型的时间应该花在开头,而不是等效果不好了再换。选之前至少确认三件事:目标语种有没有专门优化、最大输入长度够不够覆盖你的切片、以及能不能自部署(数据合规要求时这是硬条件)。
2. 代价二:维度的账要算清楚
维度越高,表达力通常越强,但代价是存储、内存和计算。
一个粗略的估算方式:一条 1536 维 float32 向量约 6KB。一百万条切片就是 6GB 的裸向量数据,再加上索引结构(HNSW 的图边),实际占用会更大。这也是为什么"降维"和"量化"这两件事在规模上去之后必然会提到台面上。
几个常见的降本手段:
- 降维。 部分模型支持输出较短的向量(比如把 1536 维压到 256 维),代价是召回有一定损失,但存储和内存线性下降。
- 半精度。 用 float16 存(pgvector 的
halfvec类型),内存直接减半,召回损失通常很小。 - 标量/二值量化。 有些 Embedding 服务直接提供 int8 或二值向量,配合重排使用——用量化向量做粗召回,再用原始精度重排,是常见的组合。
选哪个先别拍脑袋:先用真实评测集测一遍降维或量化后的 Recall@K,掉得不多就用。
2.1 维度的硬上限:先看库,再选模型
降不降维不完全是性价比问题,有的库直接卡死:pgvector 的 vector 类型只能建到 2000 维的索引(halfvec 到 4000 维,bit 到 64000)。超了不是变慢,是索引建不出来,查询永久退化成顺序扫描。
这意味着"选一个 3072 维的模型"这种决定,在 pgvector 上是会被卡住的——而 3072 维恰恰是多语言场景常见的选择(见《跨语言检索的三条路》里那笔维度与存储的账)。出路有三条:用支持 Matryoshka 截断的模型把维度降到 1024 或 1536、改用 halfvec 列类型存、或者换掉 pgvector。
所以顺序是:先查清目标库能索引多少维,再定模型。 反过来的代价是全库重灌。详见《HNSW 还是 IVFFlat》。
3. 代价三:相似度到底在比什么
三种常见的距离度量:余弦相似度、内积、欧氏距离(L2)。
有个常被忽略的事实:如果向量做过归一化(长度为 1),这三者给出的排序是等价的。所以很多系统干脆统一归一化后用内积,计算最快。
真正需要判断的是"要不要归一化",而这取决于模型:有的模型输出的向量天然归一化,有的不是。判断办法很简单——查模型文档;查不到就自己算几条向量的模长。
3.1 索引必须和算子匹配
这一条在实现上最容易出错:向量索引是绑定某一种距离算子建的。用余弦算子建的索引,ORDER BY embedding <=> $1 能用,换成别的算子就用不上索引,直接退化成全表扫描——数据量一大就是秒级变分钟级。
在 pgvector 里表现为建索引时指定的算子类(vector_cosine_ops、vector_l2_ops、vector_ip_ops),查询时的 ORDER BY 必须与其一致。详见《HNSW 还是 IVFFlat》。
4. 微调 Embedding:和"换模型"是两条不同的路
召回不够时,第一反应通常是"换个更好的模型"。换模型确实有效,但它解决的是模型能力问题,代价是全库重灌(见第 1 节)。
另一条路是在你的数据上微调现有模型——它同样要重灌一次,但重灌完之后模型就固定了,收益持续。它解决的是另一个问题:通用模型不懂你的领域词。内部产品代号、行业缩写、公司特有的说法,这些在通用语料里没出现过,向量空间里自然也聚不到一起。
三件事决定微调效果:
领域语料本身。 不需要人工标注。文档里的"标题与正文""相邻段落""同一小节内的句子"都能直接构造弱监督正样本,几千到几万对就够起步。语料要来自真实知识库,而不是随便抓的公开数据。
硬负样本。 这是最影响效果的一项。随机负样本太容易,模型学不到东西;真正有用的是看起来很像但实际不相关的样本。常见做法是用当前检索系统跑一遍,取排在前面却被判为不相关的那些;也可以拿稠密一路和 BM25 一路的结果交叉,取"一路认为相关、另一路不认为"的那些。
合成 query。 文档侧有语料,查询侧没有。用一个生成模型给每个切片生成若干条"用户可能会怎么问",把切片和合成 query 配成对。这里有个坑:合成 query 的分布要和真实日志对齐,否则模型会被微调到一个根本不存在的查询分布上,榜单涨、线上没变化。
判断该走哪条路的办法是看差距出现在哪:通用榜单上候选模型差距明显 → 换模型;榜单差不多但在你的语料上都不行 → 微调。前者解决能力,后者解决适配。
微调之后同样要重灌,所以第 1 节那套"建新索引再切指针"的流程一样适用(见《知识库的重灌与切换》)。
5. 除了模型,还有两个更影响效果的因素
模型选型的差距,往往没有下面两件事的影响大。
第一,查询侧和文档侧的处理是否一致。 很多模型对"查询"和"文档"有不同的前缀要求(比如查询要加特定前缀)。如果入库时加了前缀、查询时没加,召回会明显变差。这类细节不在榜单里,只在模型文档里。
第二,切片质量。 前面几篇反复提过一次:同样一个 Embedding 模型,切片从中间切断和不切断,召回能差出一截。模型选型带来的提升,通常小于把切片做对带来的提升。参见《切片决定检索上限》。
6. 一个最小可用的选型流程
- 列三个候选模型,限制条件先卡死:语种覆盖、最大输入长度、能否自部署、许可
- 用真实语料切 200 条切片,各建一个小索引
- 用 30 条真实问题跑 Recall@10,不看榜单看自己的数据
- 顺带测一下降维/半精度的版本,记下掉多少
- 把最终模型名、维度、是否归一化、算子类型写进配置,并纳入版本管理
跑完这五步大概半天。相比后面因为"模型效果不好"反复重灌,这半天花得很值。
至于检索本身怎么做得更准——两路召回和融合排序——在《混合检索与 RRF 融合》里;本文只负责把"向量是怎么来的、换来换去的代价是什么"讲清楚。