跳到主要内容

嵌入偏移:正在杀死你长期运行的 RAG 系统的沉默退化

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的 RAG 系统运行正常。延迟处于常规水平。错误率为零。但一位询问“加州雇佣法”的用户却不断得到关于房地产的搜索结果 —— 而你的日志显示一切正常。

这就是嵌入漂移(embedding drift)在作祟:这是一种不会抛出异常、不会导致错误率飙升,也不会出现在标准可观测性仪表盘上的检索失效模式。当你的向量数据库积累了在不同条件下生成的嵌入时 —— 比如不同的模型版本、不同的分块规则、不同的预处理流水线 —— 向量开始指向不兼容的方向,这种情况就会发生。系统仍在处理请求,但语义坐标已不再对齐,检索质量在数周或数月内悄然恶化。

长期运行的 RAG 系统极易受到这种问题的困扰。你十八个月前部署的系统可能使用了某种嵌入模型对初始语料库进行索引,随后通过稍有不同的预处理流水线添加了新文档,并将查询编码迁移到了更新的模型 —— 而这一切都发生在没有明确决定混合嵌入空间的情况下。当时的每一次单独更改看起来都是合理的。但综合起来,它们产生了一个再也无法可靠地将相关内容排在无关内容之上的向量库。

为什么混合嵌入空间会破坏检索

嵌入模型将文本转换为高维空间中的一个点。语义相似的两个文本应该产生相近的点。无关的两个文本应该产生相距较远的点。余弦相似度(几乎所有向量搜索背后的相似度函数)的整个前提,都取决于这一属性在索引中的所有向量上保持一致。

问题在于,“相近”和“较远”只在单一模型的坐标系中才有意义。不同的嵌入模型以不同的方式划分向量空间。由 text-embedding-ada-002 生成的向量与由 text-embedding-3-large 生成的向量存在于根本不兼容的空间中。用余弦相似度比较它们,就像比较来自两种不同投影方式的 GPS 坐标 —— 数字看起来相似,但它们指向的并不是同一个地方。

当你明确切换了嵌入模型却忘记重新对语料库进行嵌入时,这一点显而易见。但在生产环境中真正伤害团队的情况往往不那么明显:

  • 你更新了一个预处理步骤,更激进地去除了 HTML 伪影。旧文档嵌入时带有噪声,新文档则没有。语义密度发生了偏移。
  • 你为了提高精度将分块大小从 512 令牌改为 256 令牌。旧的分块有更宽的上下文窗口,新的则较窄。边界条件不同了。
  • 你的数据流水线存在不一致性:开发环境去除了末尾空格和 Unicode 变体,而生产环境没有。在开发和生产环境中嵌入的文档最终落在略有不同的位置。
  • 当新模型发布时,你运行了部分重新嵌入,覆盖了过去六个月添加的文档。较旧的文档仍在使用原始模型。

任何这些变化都会产生一个混合向量库。这种不匹配会逐渐降低召回率:结果不会在一夜之间消失,但出现在排名靠前的文档越来越倾向于那些碰巧是最近嵌入的,或是当前流水线下处理的文档。

检索退化究竟是什么样子的

嵌入漂移会产生一种特定的失败模式,如果你不刻意寻找,很容易漏掉。查询与高度相关文档之间的健康余弦相似度应超过 0.85。在混合向量库中,对于同样的语义关系,跨模型比较通常会产生 0.65 左右的相似度。这并不是灾难性的故障 —— 系统不会崩溃 —— 但这足以扰乱排名。

受影响系统的检索召回率通常会从 0.92 下降到 0.74 左右,且没有任何相应的警报。如果你不定期针对基准查询集运行检索基准测试,你就无法察觉。标准的监控 —— CPU、内存、延迟、错误率 —— 都显示一切正常。

这种失效模式会随着时间的推移而加剧。随着查询编码器的偏离,使用原始模型嵌入的语料库变得越来越难以被发现。即便旧文档更相关,在当前流水线下嵌入的新文档也会占据结果的主导地位。用户学会了在查询中添加更具体的限定词,这在一定程度上起到了补偿作用 —— 直到他们再也无法通过这种方式解决问题。

预示漂移开始的六个信号

及早捕捉嵌入漂移需要超越基础设施健康状况的指标。最可靠的诊断方法是针对相同文本的余弦距离:定期对语料库中的文档样本进行重新嵌入,并将新向量与存储的向量进行比较。在一个稳定的系统中,文档当前的嵌入与其重新计算的嵌入之间的距离应接近于零 —— 通常在 0.0001 到 0.005 之间。当你开始看到距离超过 0.05 时,说明你的嵌入流水线发生了影响向量位置的变化。

一个补充检查是最近邻稳定性(nearest-neighbor stability):每周运行相同的基准查询集,并衡量结果集的变化程度。在一个稳定的系统中,每周 85–95% 的结果应该是相同的。当重合度降至 70% 以下时,检索质量正在主动退化。

除了这两个主要信号外:

  • 嵌入维度分布:跟踪存储向量中 L2 范数的统计分布。方差或均值的偏移表明新向量占据了与旧向量不同的空间区域。
  • nDCG@k 趋势:如果你有任何形式的相关性判定或点击数据,请跟踪随时间变化的归一化折损累计增益(nDCG)。在稳定查询集上的下降趋势是排名退化的最明确信号。
  • 检索召回率基准测试:维护一个包含已知相关文档的小型评估集,并定期运行。在延迟保持不变的情况下召回率下降,是漂移的典型特征。
  • 向量计数差异:将索引中的向量计数与源文档计数进行比较。无法解释的增量揭示了摄入失败,这会导致覆盖不全并随时间累积。

将每周漂移检查作为常规运维实践,可以在用户遇到问题之前捕捉到退化。实现这一点的工具 —— 监控余弦距离分布、跟踪邻居稳定性 —— 并不复杂。差距通常在于组织层面:团队往往直到吃过亏后才会想到构建这些检查。

何时触发重新嵌入

重新嵌入的决定是一个迁移决策,而不是一次配置更新。请务必以此为前提进行处理。

最明确的触发因素包括:模型版本变更(当新的嵌入模型在你的检索基准测试中表现显著优于当前模型时)、分块策略变更(这应当始终触发全量语料库的重新嵌入 —— 部分迁移几乎总是错误的决策),以及改变了编码前文本规范化或清洗方式的预处理规则变更。

更细微的触发因素是质量阈值的跨越:当你的 nDCG@k 指标显示出持续下降的趋势,且无法归因于查询分布的变化时,嵌入本身可能已经陈旧。某些领域 —— 如法律、医疗、监管 —— 的更新频率非常高,即使文本本身没有变化,文档中的语义关系也会发生变化,此时重新嵌入就变成了一项维护任务,而不再是一次性的迁移。

减少重新嵌入成本的一种有用优化是变更显著性过滤 (change significance filtering)。并非每一次文档更新都需要新的嵌入:如果一次拼写错误修正后的语义相似度与原始嵌入达到 0.99 以上,则可以跳过重新嵌入。而低于 0.95 相似度的实质性重写则必须重新嵌入。通过变更数据捕获层(如 Postgres 触发器、Flink CDC)实施这一方案,与对每次更新都进行盲目重新嵌入相比,可以将嵌入 API 的调用次数减少 60–80%。

尽管如此,通用的规则是:如果流水线的任何部分发生了变化 —— 无论是模型、预处理还是分块 —— 请重新嵌入整个语料库。混合模型向量库会产生不可靠的排名,而且全量重新嵌入的成本通常低于运行一个性能下降的检索系统的成本。对于一个拥有千万级文档的语料库,根据模型选择的不同,全量重新嵌入的成本约为 300–650 美元,这通常是一项一次性或低频率的开支。

零停机迁移模式

在不停机的情况下迁移嵌入,需要你在并行构建新索引的同时,保持旧索引继续提供服务。在实践中,有三种行之有效的方法。

并行列架构 (Side-by-side column architecture) 对于使用 pgvector 或类似关系型向量库的团队来说是最直接的。在现有列旁边添加一个新的嵌入列(ALTER TABLE documents ADD COLUMN embedding_v2 vector(768) CONCURRENTLY),用新模型的嵌入进行回填,通过对比基准查询集在旧列和新列中的搜索结果进行验证,然后在确认稳定后通过特性标志 (feature flag) 进行切换,并删除旧列。这种方式可以让你实现即时回滚,并防止在创建索引期间锁定表。在一个实施案例中,同一查询在旧模型和新模型搜索结果中的平均重合度达到了 82% —— 剩下的 18% 则是新模型找到了真正更好的匹配结果。

Drift-Adapter 解决了全量重新嵌入成本过高的问题。在 2024 年发表的研究中,Drift-Adapter 通过学习一种转换方式,将新模型编码的查询映射到旧模型的向量空间中。这让你可以使用新模型的查询编码来查询未经修改的现有索引 —— 无需对文档进行重新嵌入。与全量重新嵌入相比,实测的召回率恢复(recall recovery)达到了 95–99%,每次查询仅增加不到 10 微秒的延迟,且成本比传统的重新索引降低了 100 倍以上。该适配器仅需在少量数据样本上训练几分钟即可。对于全量重新嵌入需要耗时数天或花费数千美元的大规模语料库,这是一个非常有吸引力的选择。

向量数据库提供商提供的托管迁移服务可以处理同步的复杂性:它们获取初始快照,持续将变更从源索引复制到目标索引,并在你准备就绪时执行切换。权衡之处在于,在初始同步期间会有 12–18% 的临时读取延迟增加。对于高流量系统,需要仔细规划这个窗口期。

嵌入溯源的 Schema 设计

大多数嵌入漂移事件的根本原因是缺乏溯源 (provenance):团队没有追踪哪个模型版本生成了每个存储的向量,因此他们不知道向量何时发生了失配。

向量库中的每个嵌入都应该携带元数据,告诉你:是哪个嵌入模型和版本生成了它、应用了哪些预处理规则(理想情况下是预处理配置的哈希值)、使用了什么分块策略(块大小、重叠度、边界规则)以及生成时间。有了这些元数据,你可以:

  • 查询版本过时的向量,并优先对它们进行重新嵌入
  • 检测摄取流水线的变更何时开始生成与现有语料库溯源不同的向量
  • 构建仅在当前版本嵌入上搜索的部分索引 (WHERE is_current = true)
  • 如果新模型在你的特定检索任务中表现不佳,可以回滚到之前的嵌入

这种做法的开销很小 —— 只是元数据存储中的几个列 —— 但运维价值却是巨大的。嵌入溯源是系统化检测漂移与通过用户投诉才发现问题之间的本质区别。

运营思维的转变

RAG 检索质量是一个持续的运营关注点,而非一个部署里程碑。那些在发布六个月后依然保持可靠的系统,在构建之初就遵循了这样一个假设:Embedding 流水线会发生变化,模型会更新,分块(chunking)策略会被重新审视,而其中的每一项改变都会带来漂移风险。

在系统内部构建每周漂移检查、维护用于检索评估的基准查询集,并设计从一开始就记录 Embedding 来源的模式(schemas),前期仅需花费几天的工程时间。相反,通过调查用户投诉模式来发现 Embedding 漂移,不仅成本更高,修复更晚,而且会让你无法回答关于自身索引状态的基本问题。

所谓“静默退化”中的“静默”其实是可选的。你可以构建一套系统,在用户察觉之前就告知你检索质量的变化。


本文中实用的基准数据:健康的余弦距离为 0.0001–0.005;漂移预警值在 0.05 以上;邻域稳定性(neighbor stability)低于 70% 表示正在发生退化;Drift-Adapter 与完全重新 Embedding 相比,可恢复 95–99% 的召回率;1000 万份文档的完全重新 Embedding 成本约为 300–650 美元。

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