文章

Embedding 模型升级生产实战:用双写、蓝绿向量索引与 Alias 原子切换实现零停机迁移

Embedding 模型升级不只是替换模型名。本文从生产迁移角度拆解双写、后台重嵌入、蓝绿向量索引、Alias 原子切换、回滚窗口与增量更新,避免新旧向量混用导致召回漂移和服务中断。

Embedding 模型看起来只是检索链路里的一个组件,但一旦进入生产环境,它实际上和向量维度、距离函数、文本预处理、切分方式、索引参数以及历史数据绑定在一起。

因此,模型从 embedding-v1 升级到 embedding-v2,不能简单理解为修改一行模型配置。即便两个模型都输出 1024 维向量,也不代表它们处于同一个向量空间。旧文档向量和新查询向量混用后,距离分数往往已经失去原来的语义意义。

生产迁移要解决四个问题:

  1. 存量数据如何重新向量化,同时不阻塞在线检索;
  2. 迁移期间新增和修改的数据如何保持一致
  3. 什么时候可以安全把查询切到新模型和新索引
  4. 新模型线上效果不达预期时如何快速回滚

核心原则:向量索引必须和 Embedding 版本一起发布

生产系统里,真正需要版本化的不是一个 model_name,而是一份完整的 Embedding Contract。建议至少记录以下字段:

embedding_contract:
  model: embedding-v2
  model_revision: 2026-08-01
  dimension: 1024
  distance: cosine
  preprocessing_version: normalize-v3
  chunking_version: chunk-512-v4
  index_version: knowledge-prod-v2

这里最容易被忽略的是 preprocessing_versionchunking_version。如果模型没变,但分词清洗、HTML 去噪或 chunk 边界发生变化,历史向量和新向量同样可能出现不可控漂移。

因此,模型、预处理、切分和索引应该作为一个发布单元治理

方案一:蓝绿向量索引,兼容性最好

蓝绿方案维护两个物理索引:

  • knowledge_v1:当前线上索引;
  • knowledge_v2:使用新 Embedding 模型构建的新索引;
  • knowledge_prod:应用始终访问的逻辑 Alias。

迁移过程中,查询继续访问 knowledge_v1,后台构建 knowledge_v2。完成验证后,再把 Alias 原子切换到新索引。

第一步:先创建新索引,不要原地覆盖

新模型可能改变维度或距离函数,所以新索引应显式配置自己的 schema。

client.create_collection(
    collection_name="knowledge_v2",
    vectors_config=VectorParams(
        size=1024,
        distance=Distance.COSINE,
    ),
)

这类约束说明,把”模型升级”设计成新索引发布通常比原地修改更稳妥。

第二步:进入 Dual Write,而不是先全量重建再切换

如果后台重嵌入需要数小时甚至数天,迁移期间线上数据仍会变化。只做一次全量扫描,最终得到的新索引一定会落后于源数据。

正确顺序应是:

  1. 先开启双写;
  2. 新增/更新数据同时写入 v1 和 v2;
  3. 再后台处理历史存量;
  4. 最后检查增量是否已经追平。

伪代码可以保持简单:

def upsert_document(doc):
    old_vec = embed_v1(doc.text)
    new_vec = embed_v2(doc.text)
    write("knowledge_v1", doc.id, old_vec, doc.metadata)
    write("knowledge_v2", doc.id, new_vec, doc.metadata)

真实生产环境不应把”两次写成功”完全寄托在一次 HTTP 请求里。更稳妥的方式是通过消息队列、Outbox 或 CDC 流来驱动两个版本的索引消费者,并为失败数据提供 retry / DLQ。

第三步:后台 Backfill 时必须防止”旧快照覆盖新数据”

这是向量迁移中最隐蔽的竞态条件。

假设文档 A 在 10:00 被后台扫描,10:01 用户更新了文档,双写链路已经把最新版写入 v2;如果 10:02 后台任务又把 10:00 的旧内容重新向量化后覆盖到 v2,新索引反而被回滚成旧数据。

工程上可以使用:

  • source_version
  • updated_at
  • 单调递增 sequence
  • CDC offset;
  • 内容 hash。

原则只有一个:Backfill 不能覆盖比自己更新的数据

Deletes 和 Partial Update 比 Upsert 更难

很多迁移方案只处理 upsert,却忽略 delete。例如某条记录已经在新索引删除,但后台扫描旧索引时又把它重新创建出来。

因此,大规模生产系统更适合以业务主库或变更日志为 Source of Truth,而不是把旧向量库本身当作唯一迁移源。一个更稳妥的数据路径是:

Primary Data Source
   |
   +--> CDC / Event Log --> Embedding v1 Consumer --> Index v1
   |
   +--> CDC / Event Log --> Embedding v2 Consumer --> Index v2
   |
   +--> Backfill Job ----------------------------> Index v2

这样 delete、update、restore 都可以用统一事件语义处理。

方案二:Named Vectors,适合数据库原生支持多向量的场景

如果 Collection 已使用 Named Vectors,并且版本满足要求,可以在同一条数据里增加一个新向量字段,而不是复制整个 Collection。

point_id: 10001
payload: {...}
vectors:
  embedding_v1: [...]
  embedding_v2: [...]

迁移过程变成:

  1. 增加 embedding_v2 vector schema;
  2. 新写入同时生成 v1、v2;
  3. 后台为历史 point 补齐 v2;
  4. 查询从 using=embedding_v1 切换为 using=embedding_v2
  5. 观察稳定后再删除旧 vector。

这个方案的优势是 payload 和 point ID 无需复制,回滚也比较直接。但它依赖数据库能力和原 Collection 的结构,不能视为所有向量数据库的通用做法。

Alias 是切流开关,而不是迁移过程本身

很多团队看到 Alias 支持原子切换,就误以为有了 Alias 就完成了零停机迁移。

实际上,Alias 只解决最后一跳的流量切换。真正复杂的是切换之前的数据准备和切换之后的验证。推荐的发布状态机可以定义为:

BUILDING -> DUAL_WRITE -> BACKFILLING -> CAUGHT_UP -> SHADOW_VERIFY
         -> CUTOVER -> OBSERVING -> STABLE

任何阶段失败,都应该能回到旧索引,而不是继续强制推进。

切流前不要只比较数据条数

count(v1) == count(v2) 并不能证明迁移成功。至少应做四层检查。

1. 数据完整性

检查 point/document 总量、缺失 ID、重复 ID、source version、最近一段时间的增量 lag,以及 embedding error / DLQ 数量。

2. 向量契约

检查新索引中的 dimension、distance metric、model revision、preprocessing version、chunking version。这些信息最好写入索引 metadata,而不是只存在部署文档里。

3. 离线检索质量

不要拿新旧模型的 cosine score 绝对值直接比较,因为不同向量空间的 score 分布可能不同。更有意义的是使用固定 query 集比较:

  • Recall@K;
  • MRR / NDCG;
  • Top-K overlap;
  • 关键业务 query 的人工验收结果。

4. Shadow Query

正式切流前,可以把真实请求复制一份到 v2,只记录结果,不影响用户响应。重点观察:

  • Top-K 差异;
  • 空结果率;
  • p95/p99 检索延迟;
  • embedding 调用失败率;
  • 特定租户、语言、文档类型是否出现系统性退化。

增量更新:模型迁移结束后,索引仍然会继续变旧

模型升级只是一次大规模变更。日常运行时,原始文档不断修改,如果向量没有同步更新,检索结果同样会逐渐失真。

小规模数据可以周期性做完整 diff;当数据规模继续增长后,扫描整个 corpus 的成本会上升,更适合改为 CDC、事件日志或可枚举的增量版本机制。

换句话说,Embedding Freshness 应该是一条长期数据管道,而不是上线前跑一次的脚本

适用场景

这套迁移方法尤其适合以下情况:

  • 更换 Embedding 模型供应商;
  • 模型版本升级导致向量空间变化;
  • 维度从 768 调整到 1024/1536;
  • dense encoder 替换或升级;
  • chunking / preprocessing 规则大改;
  • 需要同时升级向量索引参数;
  • 对搜索服务停机时间非常敏感。

如果只是少量离线数据、允许维护窗口,而且可以一次性完整重建,则不必引入复杂的双写链路。

常见误区

误区事实
维度一样就可以混用维度只是张量形状,不代表两个模型共享同一语义空间
先全量重建,最后补一次增量就够了迁移期间持续有更新、删除和部分修改,“最后补一次”很容易漏事件或产生顺序问题
Alias 切换成功就可以马上删除旧索引Alias 只解决切流,线上质量还需要观察,旧索引应保留 rollback window
只看离线 Recall,不看线上分布线上真实请求分布可能与离线数据集不同,Shadow traffic 和分群指标很重要
Embedding 版本只记录模型名称应同时记录 model revision、dimension、distance、preprocessing、chunking 和 index version

上线检查

切换生产 Alias 前,建议至少确认:

  • 新旧索引双写已经稳定运行;
  • Backfill 已完成,且增量 lag 已收敛;
  • delete / partial update 语义已经验证;
  • 无持续增长的 embedding error 或 DLQ;
  • 新索引的维度、距离函数和模型版本正确;
  • 固定检索集通过质量门槛;
  • Shadow Query 没有发现明显分群退化;
  • Alias / routing 切换操作已经演练;
  • 回滚路径和旧索引保留时间已明确;
  • 切流后监控包含质量、错误率和延迟,而不只是资源利用率。

参考资料

  1. Qdrant — Migrate to a New Embedding Model with Zero Downtime:https://qdrant.tech/documentation/tutorials-operations/embedding-model-migration/
  2. Qdrant — Incremental Embedding Updates:https://qdrant.tech/documentation/tutorials-operations/incremental-embedding-updates/
  3. Weaviate — Switching vectorizers:https://docs.weaviate.io/weaviate/tutorials/vectorizer-migration
  4. Milvus — Manage Aliases:https://milvus.io/docs/manage-aliases.md
  5. Pinecone — Configure an index, API 2026-04:https://docs.pinecone.io/reference/api/2026-04/control-plane/configure_index

常见问题

Embedding 模型升级时,能不能直接把新模型生成的向量写进原索引?
通常不建议。不同模型甚至同一模型不同版本产生的向量空间可能不兼容,维度、距离度量或语义分布也可能变化。生产环境应显式版本化模型与索引,并通过双写、重嵌入和切流完成迁移。
蓝绿向量索引和 Named Vectors 应该怎么选?
蓝绿索引兼容性最好,适合模型、维度、索引参数一起变化;Named Vectors 更节省数据复制成本,但要求数据库支持同一对象的多向量,并且现有集合结构满足迁移条件。
什么时候可以删除旧向量索引?
不要在切流后立即删除。至少应完成数据完整性校验、离线检索评估、线上观测和回滚演练,并保留一个明确的回滚窗口,再清理旧索引与旧模型依赖。