跳到主要内容

单向量版本标签:每个 Embedding 迁移背后的缺失列

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个新的嵌入模型发布了。基准测试数据提升了 4 %。一位 Staff 工程师提交了一个工单:“将 embedding 升级到 v3。”两周后,索引已完成重新嵌入,别名已切换,团队通过特性标志(feature flag)发布了变更。六周后,支持工单堆积如山。搜索结果“感觉不对劲”。复盘会召开了。没人能解释为什么出现了退化,因为没有系统崩溃,每个仪表盘显示的都是绿色。

问题不在于模型的更换。问题在于向量存储根本不知道哪些向量来自哪个模型。数据库里没有这一列。没有用于追踪哪些记录已回填的迁移表。没有 alembic_version 行,没有 schema_migrations 表,也没有先前状态的 pg_dump。团队将 embedding 升级视为一次简单的配置切换,而向量存储在模式(schema)层面缺乏能阻止他们犯错的概念。

Embedding 迁移需要数据库迁移二十年来一直依赖的相同产物:写入每个向量、在每次查询时检索、并作为切换和回滚准入准则的单条记录版本标签。这是大多数团队最容易忘记添加的一列,而后期补救的成本远高于前期添加。

Postgres 教会了我们,但向量存储却忘记了

关系型数据库很早就拥有了迁移工具。对于大多数变更,ALTER TABLE 是在线完成的。像 pg_dumpgh-ostpt-online-schema-change、Liquibase、Flyway 和 Alembic 这样的工具为工程师提供了版本化、可重复且具备回滚意识的模式变更。每次迁移都带有版本号。每一行都知道自己符合哪种模式。工具链假设你至少会搞砸一次,并需要优雅地撤回。

向量存储在发布时并未提供这些功能。没有 ALTER VECTOR TABLE 来原地重新嵌入行。没有迁移框架来追踪已处理的行。除了“从头开始重建整个索引”之外,没有“切换嵌入模型”的回滚语义。Pinecone、Weaviate、Qdrant、Milvus 和 pgvector 都提供了存储带元数据向量的原语,但它们都把迁移规范留给应用团队作为“家庭作业”。

大多数团队直到踩坑才意识到这个差距。他们把向量存储当作一个“只管写入”的 KV 存储。他们对语料库进行一次嵌入,写入向量,然后进行查询。当更好的模型出现时,团队发现向量存储实际支持的唯一操作是“删除并重写”。没有在线模式变更。没有滚动切换。没有回填机制。于是团队在时间压力下仓促地构建了一个糟糕的方案并发布。

这种规范并不神秘。这与一位资深后端工程师在修改拥有 5000 万行数据的表列类型时所采用的规范是一样的。它之所以没被采用,是因为没人将这项工作标记为模式迁移,因为迁移所需的模式列缺失了。

每个向量的版本标签是你遗忘的那一列

出发点平淡而具体:你写入的每个向量都需要在其元数据中包含一个版本标签。不是在索引上,也不是在命名空间上,而是在行上。该标签标识产生它的嵌入模型——模型名称、模型版本以及任何发生变化的预处理参数(分块大小、归一化、提示词模板)。一个合理的格式是像 [email protected]/chunk-512/norm-l2 这样的短字符串。Pinecone、Weaviate、Qdrant 和 pgvector 都支持记录上的任意元数据;添加该字段的成本几乎为零,而以后由于缺少它所付出的成本大约是一个工程师月。

一旦每个向量都有了标签,几件以前不可能的事情就变得可行了:

  • 读取时过滤。查询可以限定在特定的 embedding 版本。在切换期间,生产流量查询新标签,而一小部分验证流量同时查询两者并进行对比。
  • 混合状态安全性。如果回填完成了一半,检索层知道哪些向量使用的是新“语言”,哪些仍在使用旧“语言”。它不会静默地混合它们并产生不连贯的近邻结果。
  • 单条记录回填追踪。迁移作业可以通过 SELECT WHERE version = 'v1' 来查找剩余内容。不需要维护额外的同步账本。向量存储本身就是账本。
  • 无需重新嵌入的回滚。如果新模型发生退化,将读取操作切回旧标签只需修改配置。旧向量依然存在,没有被删除。回滚时间受限于特性标志的延迟,而不是重新嵌入的成本。

在紧急情况下补救这一列的团队通常会发现,现有的向量没有标签,而且在不知道是由哪个模型产生的情况下,无法溯及既往地回填标签。补救办法是假设最坏情况,并将所有内容标记为尽可能早的版本,这在以后可行,但意味着团队实际上丢失了审计追踪。在第一天添加这一列不需要任何成本。在第 800 天添加它,代价是丢失了能解释为什么 2025 年检索看起来很奇怪的审计追踪。

回填完整性是切换标准,而非时间表压力

团队犯的第二个错误是在回填完成之前就进行切换。这种模式显而易见:迁移任务启动了,由于大规模重新嵌入语料库需要耗费真金白银和大量时间,导致任务进展缓慢;而三天后就要召开全体领导会议,于是有人提议切换读取路径以“开始展示成果”,同时让回填任务继续运行。现在,生产环境从一个仅部分填充的新索引中读取数据。一半的语料库对检索不可见。一半的查询返回的结果候选集比应有的要小。在预发环境中针对完全回填的语料库进行评分的评估套件无法发现这个问题,因为评估集足够小,足以被完全回填。

生产环境的检索质量是候选池大小的函数。在语料库仅回填了 60% 时进行切换,你实际上是在默默地告诉系统:“这周有 30-40% 的文档不存在。” 某些查询会命中已回填的文档,看起来很正常;而另一些查询则会掉进覆盖范围的空洞,返回通用且相关性低的结果。在用户群中的表现看起来像是“某些查询的搜索效果变差了”,而不是“搜索挂了”,这是最难诊断的失败模式。

必须建立的规矩是将回填完整性闸门作为正式的切换标准。用数字来定义它:“当新版本的标签回填率超过旧版本记录数的 99.5%,且剩余 0.5% 已纳入新鲜度预算时,才进行切换。” 将这个闸门显示在展示发布日期的同一个仪表盘上。让发布取决于这个闸门,而不是让发布覆盖它。迁移是否完成取决于数据,而不是日历。

这里有一个推论:回填比例应该与评估得分放在同一个仪表盘上。评估套件是在假设语料库完整的情况下测试语义质量的。如果语料库只完成了 60%,那么评估套件测试的系统就与生产环境流量命中的系统不同。在评估得分旁边展示回填比例,可以防止团队在看到绿色的评估数字时,误以为迁移是安全的,而忽略了未完成的回填才是真正的风险。

无需重新嵌入的回滚

第三个错误是将切换视为“单向门”。典型的模式是:团队完成了回填,切换了别名,验证了一周的指标,然后删除了旧向量以释放存储空间。六周后,一个面向用户的回归问题出现了——新模型在处理某些查询模式时效果比旧模型差,而这些模式并未出现在评估集中。此时的回滚计划是“使用旧模型从头开始重新嵌入整个语料库”,这不仅成本与最初的迁移相同,而且还需要数周的时间。于是团队不得不忍受这个回归问题而不选择回滚,因为回滚的代价比 Bug 还要大。

存储是系统中成本最低的。重新嵌入则是最昂贵的。这种数学关系是不对称的:一个 1536 维 float32 格式的重复向量索引,每百万条记录仅占用几个 GB,这对于任何向量数据库来说都是基本开销。而通过付费嵌入 API 对同样的百万条记录进行重新嵌入,其成本随语料库规模增加,且仅受限于你的预算。正确的策略是保留旧向量,直到新模型在生产环境中积累了足够的证据证明其值得信赖——这应该以数周的稳定检索质量来衡量,而不是数天。存储成本是获得回滚期权的价格,而且这是一个廉价的期权。

单向量版本标签使这一切变得微不足道。检索层从 version = 'new' 中读取。旧向量依然存在,标记为 version = 'old'。回滚只需在应用层更改配置,翻转版本过滤器即可。没有数据迁移,没有重新嵌入。回滚在几秒钟内即可完成。在新版本有了几周的生产环境证据——全量流量下的使用情况、跨多个周期的评估稳定性、没有客户端回归——之后,旧向量才可以被删除。在那之前,重复并不是浪费,而是回滚计划。

在迁移设计评审中,一个很有用的问题是:“我们今天的回滚时间表是怎样的?需要做些什么才能将其降至五分钟以内?” 如果答案涉及重新嵌入语料库,那么迁移设计就不完整。如果答案是“翻转版本过滤器”,那么迁移设计才是合理的。

嵌入流水线需要的迁移纪律

这些环节组合成了一种任何交付过数据库迁移的人都能识别的模式:增加版本列。标记每条记录。在后台回填。将完整性作为指标而非凭感觉来追踪。以该指标作为切换的闸门。保留旧数据直到新数据赢得了信任。无需重写语料库即可实现向前推进和向后回滚。

嵌入迁移的特殊之处在于,单靠评估套件无法捕捉到所有的失败模式。当索引的几何结构在底层发生旋转时,逐点相关性得分可能依然显示为绿色。版本标签让团队能够理清他们正在评估的是哪一个索引,仅在有意为之时才混合版本,并生成真正对应于连贯嵌入空间的分版本评估得分。没有标签,每一次评估都是可疑的,因为团队无法确定是哪个模型生成了他们正在评分的向量。

对于处于嵌入升级规划阶段的团队来说,结论很简单:在运行迁移之前,先增加那一列。如果你的记录中已经有了这一列,审计每一个写入路径是否都在填充它。如果没有,先修复这个问题,并将缺失该列视为 P0 级的架构缺陷。迁移早晚会发生——模型提供商会按照他们自己的节奏发布新版本。拥有版本标签的团队会将迁移作为常规操作运行。没有标签的团队则会将其作为一次事故来处理。

向量数据库没有 ALTER TABLE。版本标签就是当 ALTER TABLE 不存在时,你为每一行数据添加的东西。

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