跳到主要内容

那些在悄无声息中重新排列你整个语料库的嵌入模型升级

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个新的嵌入模型登上了排行榜。它比你 18 个月前发布的模型得分更高,API 只需更改一行代码,甚至维度也一致。有人提了一个工单:“升级嵌入模型”。这看起来就像更换一个日志库一样简单。

事实并非如此。嵌入模型并不是你检索系统的一个普通组件——它就是你检索系统所处的坐标系。更换它并不会改进你的索引,而是会让索引失效。而最残酷的地方在于,系统并不会崩溃。没有异常,没有失败的健康检查。你的搜索只是开始返回微妙不同的结果,而在 RAG 管道中,“微妙不同”意味着不同的文档被喂给了模型,也就意味着不同的答案最终传达给用户。

这种失效模式不会在代码审查中显现。差异只有三行代码,但受波及的范围却是你索引过的每一个文档。

向量仅在其自身的模型内部具有意义

每个嵌入模型在训练期间都会学习其自身的内部含义表示。输出是一个浮点数组,但该数组并不是对文本的通用描述——它是该特定模型所创造的空间中的一组坐标。

某个模型中的第 47 维度可能追踪的是接近“纹理”的东西。而另一个模型中的第 47 维度可能追踪的是情感,或者根本无法命名。这两个模型从未被训练成在轴的含义上达成一致。它们无法做到,因为它们从未见过彼此。

这产生了一个严重的后果:来自模型 v1 的向量和来自模型 v2 的向量是不可比的,即使它们描述的是完全相同的句子,即使两个数组都有 1536 个维度。它们之间的余弦相似度只是一个数字,而这个数字毫无意义。让相似度对应于语义接近度的几何特征,仅存在于单个模型的空间内。

因此,当你“升级”模型时,你并没有为语料库获得更好的向量。你获得的是生活在与你已经存储的数百万个向量不同的宇宙中的向量。旧索引并没有降级。它只是用一种新查询向量无法理解的语言编写的。

唯一正确的做法是全量重新嵌入(re-embed):让每一个文档都通过新模型运行,并从头开始重建索引。没有增量升级。不存在“用 v2 嵌入新文档并让旧文档逐渐淘汰”这种做法。这最后一种想法正是许多团队悄无声息地摧毁其检索质量的原因。

迁移了一半的索引比任何一个单一模型都要糟糕

想象一下那个诱人的捷径。新模型上线了。新文档使用 v2 进行嵌入。旧文档保留其 v1 向量。同一个索引,混合内容,无需停机。这听起来很务实。

你所构建的是一个跨两个不兼容坐标系计算相似度的索引。使用 v2 嵌入的查询会给每个文档评分——但针对 v1 文档的评分是噪声,而针对 v2 文档的评分才是信号。排名会将它们交织在一起。有些结果是真正的邻居;有些则是两个无关几何空间重叠产生的意外。

这种行为产生的后果确实很难调试。同一个查询对于最近索引的文档返回了极佳的结果,而对于较旧的文档返回的结果却很差。精确率和召回率都会下降,但并不是均匀下降——它们是作为文档最后一次嵌入时间的函数而下降的,而团队中没有人会对这个变量进行绘图。你的可观测性仪表盘显示“检索质量下降了 12%”,而这个汇总数字掩盖了一个事实:一部分文档从良好变成了不可用,而另一部分则保持正常。

一个团队会花一周时间调整分块大小和重排序器权重,然后才有人想到去问哪个模型嵌入了哪一行。混合索引并不会自我声明。它只会让你的系统表现得像是有双重人格,而且不给你任何可以拆分指标的字段。

如果你从中学到一条规则,那就是:单个索引或单个集合必须包含来自完全相同的一个模型版本的向量。混合并不是一种有副作用的权衡。它是一个正确性缺陷。

更高的基准测试分数并不是对你语料库的承诺

升级工单获得批准的原因通常是排行榜上的一个数字。新模型在 MTEB 上的表现超过了旧模型,所以它的检索效果一定更好。这种推论比看起来要脆弱得多。

通用嵌入基准测试衡量的是在各种公共数据集混合下的平均性能。你的语料库并不是那种混合体。它有自己的词汇表、自己的文档风格、以及自己的查询措辞分布。针对特定领域检索的研究发现,一个模型在通用基准测试上的得分可能与它在特定领域的得分不相关——一旦你直接在金融、法律、医学以及充满内部术语的语料库上进行测量,排行榜就会被重新洗牌。

因此,一个基准测试得分更高的模型在你特定的数据上检索效果更差,这完全是可能的——而且经常被观察到。新模型可能以某种方式进行了微调,有助于处理通用网络文本,但却损害了你支持团队编写的那些密集的、充满缩写词的文档。在你使用自己的查询在自己的语料库上进行测量之前,你不会知道结果。

这就是为什么升级决策在需要 API 密钥之前,更需要一个黄金数据集。收集几百个最常用且最关键的查询,每个查询都配对应该被检索到的文档。针对这两个模型运行该集合。进行对比。如果新模型在你自己的数据上没有胜出,那么排行榜就没有投票权。基准测试告诉你该模型擅长作为一个通用模型,它并没有告诉你它擅长作为你的模型。

迁移是 Schema 变更,而非配置更改

一旦你接受了升级意味着需要重新对整个语料库进行嵌入(Re-embedding),这个项目就不再只是一个配置微调,而更像是一次数据库迁移 —— 事实也的确如此。嵌入模型是一项 Schema 决策。请以对待生产环境中表字段类型变更的严谨态度来对待这次切换。

行之有效的模式是影子索引(Shadow Index)。在现有索引运行的同时,创建一个使用新模型构建的第二个索引。保持两者同步更新:在迁移窗口期间,通过双写(Dual-write)路径将每个新增和更新的文档同时发送到这两个索引,这样在你回填(Backfill)历史数据时,影子索引就不会过时。

然后进行回填 —— 在后台以你的 API 配额和预算允许的速率,将现有语料库重新嵌入到影子索引中。在此期间,生产环境继续由旧索引提供服务。用户端尚未发生任何变化。

切换过程是安全性保障的核心,其规则是:永远不要一次性切换 100% 的流量。既然两种表示形式同时存在,就充分利用这一点。将 5% 的查询路由到影子索引,观察检索指标和下游回答质量,然后逐步增加比例。使用 Feature Flag 包裹切换开关,这样回滚只需要动一下开关,而不是重新部署。如果影子索引的表现令人失望,你可以直接丢弃它,你损失的只是计算资源,而不是客户。

这一系列流程 —— 影子索引、双写、后台回填、基于标志的渐进式切换、即时回滚 —— 并不罕见。它是应用于向量数据的标准零停机迁移方案。错误并不在于这个方案有多难,而在于没有意识到向量迁移也需要这样一套方案。

预算应涵盖回填过程,而非仅关注 Token 价格

在评估重新嵌入的范围时,团队往往只关注最明显的账目项:嵌入 API 的账单。那个数字通常小得令人安心。在当前的小型嵌入模型上,重新嵌入一百万个各含几百个 Token 的文档,成本仅为个位数美元。Token 价格并非真正的成本。

真正的成本隐藏在周围。首先是迁移期间并行运行两个完整索引的存储成本 —— 当达到数千万或数亿个向量时,复制索引每月的费用可能比一次性重新嵌入的总成本还要高。其次是重建时间:大型近似最近邻(ANN)索引的构建需要数小时,这个时间窗口必须提前规划。此外,还需要投入工程时间来构建双写路径、回填任务、对比框架和标志控制逻辑 —— 如果你在检索质量已经下降的压力下才仓促构建这些,你付出的代价将极其惨重。

还有速率限制(Rate-limiting)的问题。重新嵌入大型语料库涉及数百万次 API 调用。你会触及吞吐量上限,你需要重试和断点检查逻辑,以便在运行失败时能够恢复而不是重启。对于庞大的语料库,回填是一项耗时数天的工作,而不是一个下午就能完成的。提供商侧的改进会有所帮助 —— 一些较新的模型系列在不同级别之间共享嵌入空间,因此你可以在同一提供商的产品线内迁移而无需完全重新嵌入 —— 但任何跨提供商的迁移,或任何跨大版本的跳跃,仍然意味着需要重新嵌入所有内容。

将嵌入模型视为固定版本的依赖

核心思路是,嵌入模型理应被视为一种 Schema,因为从功能上讲它确实就是。有三个习惯可以让这一点落地。

固定并记录版本。每个索引,理想情况下是每个存储的向量,都应携带生成它的模型标识符和版本号。当出现问题时,第一个问题 —— “这是由哪个模型嵌入的?” —— 应该能从元数据中找到答案,而不是靠“考古”推测。这个单一字段能让混合索引引发的 Bug 无所遁形。

保留特定语料库的评估集。黄金查询/文档集(Golden set)不仅仅是为了做迁移决策。它是你针对检索的常设回归测试,也是唯一能告诉你候选模型真相的工具,因为它测量的是你的数据,而不是别人的平均水平。

在需要迁移之前就规划好迁移。影子索引、双写、回填和带标志的切换应该是已知的操作手册(Runbook),而不是临时即兴发挥。当真正更好的模型出现时 —— 它一定会来的 —— 那些已经将嵌入模型视为版本化依赖的团队会执行一次计划内的迁移。而那些将其视为可随时插拔组件的团队,只会发布一个三行代码的改动(Diff),然后用接下来的一个星期向用户解释为什么回答质量变差了。

升级本身不是风险,无声的重排序(Re-ranking)才是。没有计划的模型更换不会剧烈报错 —— 它只是悄无声息地改写系统信任的文档,一次一个查询,直到有人察觉到答案发生了偏差。将嵌入模型变成一个你刻意进行版本控制、测量和迁移的对象,这种“沉默”就不再危险。

References:Let's stay in touch and Follow me for more thoughts and updates