那场无需部署就让你检索召回率减半的 Embedding 弃用事件
在一个 RAG 系统中,可能上线的代价最高昂的嵌入 (embedding) Bug,是那种你的代码库没有任何变化、检索代码没变、索引没变、查询路径也没变的 Bug。然后在第六周的某个周二,有人注意到答案的质量不如从前了。
服务商为你十二个月前构建索引时所使用的嵌入系列发布了停用公告。平台团队将其归档在了一个拥有一年缓冲期的停用仪表盘中,然后就继续处理其他事情了。停用路径并不是一个生硬的截止——而是一个悄无声息的质量退化:被停用的端点开始路由到一个“兼容性”继任者,它返回相同维度的向量,但语义几何空间却有微妙的不同。查询嵌入开始与你一年前嵌入的语料库发生漂移。在六周的时间里,你的常规评估中的 Recall@10 下降了 47%。团队直到一个无关的质量仪表盘达到阈值时才追溯到原因,迫使一名高级工程师进行根因分析,最终发现问题指向了一个在这一年里没人动过的嵌入端点。
这篇文章探讨的是该事件背后的架构错误:将嵌入端点视为可替代的 URL,而不是将其视为与其生成的语料库绑定的版本化依赖。服务商并不总是通过版本升级来承认行为变更,停用时间线通常是上限而非确定的日期,而你的检索召回率正在被供应商以一种你的评估无法察觉的节奏重新定义。
为什么 “兼容性” 是停用通知中最危险的词
从 embeddings-v1 到 embeddings-v2 的版本升级是显性的。你的客户端代码会改变,你的索引文档会更新,你的工单会堆积,而且肯定会有人运行评估。系统有机会在变更发生的瞬间暴露出回归问题。
而“兼容性”继任者则完全相反。供应商保留了 URL,保留了维度,也保留了响应结构。唯一改变的是从文本到向量的映射函数。相同的输入,略有不同的输出,相同的形状。对于你客户端代码的每一行来说,这次调用看起来与昨天完全一样。
这恰恰是问题所在。余弦相似度和点积只有在比较的双方都处于同一个空间时才有意义。一旦你的查询嵌入来自与文档嵌入不同的模型,让邻域 (neighborhoods) 具有意义的几何结构就会失效。从业者将其描述为“拿苹果和橘子做比较”:数字依然会返回,索引依然会返回十个邻居,但大多数邻居现在都是错误的,而且你的技术栈中没有任何环节能检测到这一点。
这种损害的程度取决于两个模型之间的分歧程度,也就是供应商对继任者进行调优的激进程度。一些“兼容性”继任者被校准为近乎同构;而另一些则不然。你无法选择,而且通常也不会被告知。
停用是一种行为演变,而非一个日期
大多数团队将停用日期视为截止日期:在 T 时刻,端点停止工作。这种心理模型允许你通过消耗直到 T 的时间来资助迁移。对于硬性切断,这是一个有用的模型。
但对于软性切断,这就是一个错误模型。管理着数百万客户集群的供应商会优先考虑可用性——他们宁愿让你继续收到 200 响应,也不愿用 410 错误中断你的业务。因此,停用路径变成了一条曲线:在发布公告和正式退役之间,端点的行为会被重新定义,以降低底层基础设施的运营成本。返回向量的质量就是可调节的旋钮之一。
供应商倾向于用“功能对等调整”或“基础设施改进”这类语言来描述这些变化。发布说明在技术上是准确的,但作为检索系统变差的信号,它们毫无用处。在停用窗口期内,你与端点之间的实际契约是“我们将返回一个形状相同的向量”——而不是“我们将返回一个来自相同语义分布的向量”。
负责检索的团队需要意识到,公布的停用日期是上限,而且在缓冲期内的契约是形状稳定,而非语义稳定。主流服务商现在以 12 到 18 个月的频率更新模型,甚至有几家为了加速淘汰,只给出了 2 到 4 周的停用窗口。你以为拥有的宽限期比公告暗示的要短,而且行为在窗口期内就已经在发生偏移。
异步评估盲点
几乎每个团队都有一个用于基准测试嵌入模型的评估 (eval)。但几乎没有人拥有一个用于基准测试已部署索引的评估。这种区别正是这种失效模式能够保持隐形的原因。
标准的嵌入评估是这样的:获取一组带标签的查询集,用模型 X 嵌入查询,用模型 X 嵌入语料库,运行检索,计算 recall@k。当你升级到模型 Y 时,你用 Y 重新嵌入双方并重新评分。评估能正确地告诉你,当双方都处于 Y 空间时,Y 是否比 X 更适合检索。
但这并不是生产环境中发生的情况。在生产环境中,你的文档嵌入是一年前写入索引的,其成本是你那个季度嵌入整个语料库的费用。重新嵌入语料库是一个大工程。因此,真正匹配生产环境的“评估”应该是:由今天的端点嵌入的查询,对比去年端点嵌入的文档,按今天的标签评分。大多数团队从未运行过这种评估,因为他们从未建立过用“今天的端点”去对比“去年的向量”来嵌入查询的基础设施——他们只有一个端点,并且信任它。
当供应商悄悄将该端点迁移到兼容性继任者时,“今天的端点”就不再是“去年的端点”了——但这仅发生在查询阶段。语料库端在索引中是冻结的。如果你按标准方式运行评估(重新嵌入双方),结果看起来会很好,因为它把双方都放回了相同的空间。而你的生产流量由于只能重新嵌入一侧,质量将会下降。
语料库端嵌入(支付一次,难以重做)与查询端嵌入(按请求支付,自动反映今天的端点)之间的不对称性,正是供应商悄然迁移的隐匿之处。任何不保留这种不对称性的评估都是在测试错误的系统。
锁定你真正需要的契约的 模式
如果你需要的契约是“查询和文档嵌入来自同一个模型”,而提供商提供的契约仅仅是“相同维度的向量”,那么其中的差距就需要由你来弥补。在问题发生之前,有几种模式值得提前建立。
在语料库层面锁定嵌入模型版本。 你索引中的每个文档都应该有一个挂载字段(sidecar field):embedding_model_id、embedding_model_version、embedding_endpoint_url,理想情况下还应该有一个在索引时由该端点生成的固定金丝雀字符串(canary string)的内容哈希。你的写入路径拒绝插入没有这些字段的向量。你的读取路径拒绝针对模型标识符与查询来源不匹配的文档进行评分。错误应该是响亮且即时的,而不是无声的召回率下滑。
将金丝雀字符串视为契约测试。 挑选一小组固定的字符串 —— 20 到 50 个即可 —— 在构建索引时嵌入每一个字符串,并将生成的向量作为契约制品存储。在查询时,针对采样的一部分请求,重新嵌入其中一个金丝雀字符串,计算其与存储向量的余弦相似度,并断言其高于某个阈值(如 0.9999)。一旦提供商的端点开始为已知固定输入返回实质上不同的向量,断言就会触发。这是你能针对无法控制的提供商部署的最廉价的行为变更检测器。
监控 recall@k 的斜率,而非仅仅是数值。 一个常设的标注查询集(200 到 500 个查询-文档对就足以发挥作用)应该每晚针对实时检索路径运行 —— 使用当天的端点生成查询嵌入,针对实际存在的索引进行检索 —— 并报告 recall@k。警报不应针对绝对数值,而应针对多日滚动斜率。如果阈值是在系统健康时设置的,那么六周内 47% 的下滑对于任何基于阈值的警报来说都是不可见的;但在趋势图上,它一目了然。
运行强制切换登记簿,而非弃用跟踪器。 一个记录“提供商说这将在日期 T 消失”的弃用仪表盘会助长拖延。而一个记录“我们将在日期 T 减去 90 天前完全迁移出此端点”的切换登记簿则将压力放在了你的时间线上。强制切换日期应该根据提供商的日期倒推,并留出利润空间,以涵盖你必须重新嵌入的语料库、必须重新运行的评估以及必须切换的索引。如果你无法按期完成,你会足够早地发现并与提供商协商,而不是在需要紧急修复时才姗姗来迟。
将重新嵌入项目规划为基础设施,而非冲刺任务。 重新嵌入一个耗时一个季度构建的语料库,将耗费相当的时间和成本。现代化的指导意见将其视为重大的基础设施事件:并行索引、双写阶段、比较新旧索引结果的影子查询阶段、带有回滚计划的切换,以及仅在新索引承载全部流量一周后才弃用旧索引。这项工作量巨大,如果你因为召回率崩溃而在周五才发现需求,那么你已经陷入麻烦了。
对于那些确实无法及时重新嵌入的团队,最近的研究探索了可学习的转换层(learnable transformation layers),这些图层将新模型的查询嵌入映射到旧索引的空间中,以少量的延迟开销恢复完整重新嵌入的大部分召回率。这些作为迁移期间的过渡桥梁很有用,但不能替代迁移 —— 它们只是掩盖了几何不匹配,而非修复它。
