RAG 系统上线后的第一类生产事故,往往不是检索效果不好,而是索引和现实对不上:源文档改了三个月,检索到的还是旧版;某个 embedding 模型升级了,库里的向量一半新一半旧,相似度排序悄悄失真。这些问题都不难修,难在意识到——因为它们从不报错,只是安静地给出过时或矛盾的答案。这篇把 RAG 索引的所有变更来源分成三类,每类给出正确姿势与成本估算,目标是让「索引一致性」从玄学变成一套可执行的规程。
第一类:文档增删改——变更必须级联
单篇文档的变更看似简单(重嵌入那份文档就行),实际要处理级联。文档改动后,切块结果可能整体变化——原来 20 块现在 18 块,父子检索的父块映射要重建(《父子检索》),RAPTOR 摘要树的上层节点要重算(《RAPTOR 与层级索引》),GraphRAG 的实体关系要重新抽取。漏掉任何一级,检索层就会出现「新旧杂交」:正文是新的,摘要是旧的。
工程上正确的抽象是:以「源文档」为最小变更单元,维护一棵完整的派生树——文档 → 块 → 向量 → 摘要节点/图谱节点,每个派生物都记录它的源文档版本号。变更流程是原子化的删除-重建:删掉该文档的全部派生物(按 source_id 级联),重新走一遍幂等的摄入流程(同输入必得同输出,重复执行不产生重复数据),而不是试图「就地更新」部分块。删除文档同样要级联:块、向量、父子映射、摘要树、缓存(《语义缓存》的失效机制在这里被触发)。变更的判定别靠人工:源文档系统里挂 webhook 或定时比对哈希(mtime、内容 hash 都行),差异即触发重建。
第二类:embedding 模型迁移——必须全量重建
换 embedding 模型(或它的版本)是最容易出「安静事故」的变更,先立一条铁律:新旧模型的向量不在同一个语义空间里,绝对不可混用。两个模型对同一句话给出的向量,哪怕维度相同,坐标系也完全不同——新查询向量对着旧库向量算余弦,结果是数字,但毫无语义。混库的后果是召回率无声劣化:最近的向量可能是「同模型的巧合」,真正相关的条目(另一边模型的)排名沉底。
正确的迁移姿势是全量重建 + 双索引灰度:
- 新索引并行建:新模型对全库重新嵌入,建一个全新的索引(新 collection/新表),旧索引原封不动继续服务。
- 影子验证:用评测集(《评测集构建》的尺子)同时打两个索引,对比 recall@k、MRR 等指标——新模型必须在验证上赢了才谈切换,而不是「听说新模型好」。
- 双查询过渡(可选):切换期同时查新旧两个索引,用 reranker 统一重排合并——规避两空间不可比的分数问题(reranker 对文本打分,不看向量空间)。
- 切换与回滚预案:流量切到新索引,旧索引保留一至两周作为回滚保险,确认无回归后下线。
成本估算的核心项是全库重嵌入的 token 费:语料总字符数 ÷ 模型单价,千万字级语料通常在几十到几百元的量级(以主流嵌入服务定价计),真正的成本是第 2、3 步的验证工程。顺带一提,切块参数(块大小、重叠)的变更与模型迁移同阶——切块变了,全库的块全变,等于全量重建,别把「调个参数」当成轻操作。
第三类:元数据与结构变更
第三类变更不动向量,动的是附属结构:权限变更(文档从公开转受限——《权限感知检索》的 ACL 同步)、元数据 schema 变更(新增过滤字段需要回填历史数据)、树结构重排(知识库目录调整,注意《层级索引》类结构里节点 id 的稳定性)。这类变更不触发重嵌入,但必须有回填工具:写一次性的批处理脚本遍历全库更新元数据,跑完校验计数对得上。元数据变更的风险不在成本在覆盖——漏改几条,权限过滤就会出现漏洞或过滤条件查不全。
重建任务的调度与限流
全量重建(或大规模批量回填)跑在生产基础设施上,调度纪律要立好。限流:重嵌入的 API 调用与索引写入都要限速——前者防打爆嵌入服务的配额(同步下游限流),后者防把线上检索的写入带宽挤占(部分引擎批量写入会引发段合并,查询延迟会抖)。参考做法:重建任务按「每秒 N 条向量」的速率上限跑,N 按线上服务的空闲容量倒推;大任务拆成带断点的批次(每批记录水位,中断续跑不重来)。错峰:重建窗口避开业务高峰,尤其避开「检索 + 写入」双重高峰的时段。观测:重建任务的进度、速率、失败率要有仪表盘——「跑一半悄悄卡住」是批量任务的经典死法,没有心跳监控就是等事故。重建演练(《机制三》)正好把这套调度验证掉:演练跑的就是真实的生产路径,只是流量打在影子索引上。
版本戳的具体 schema
「每个向量记录都带版本戳」落到表结构上长这样:向量记录除 payload 外带四个元数据字段——source_id(源文档 id,级联删除的锚点)、source_version(源文档内容哈希或版本号,巡检比对的依据)、embed_model(嵌入模型名+版本,如 bge-m3@1.0 或 openai/text-embedding-3-large@2024-01)、chunk_config(切块参数指纹,几个关键参数拼串取哈希)。查询端的守门逻辑:服务启动时读配置里的「当前期望模型与切块指纹」,与库内统计分布比对(抽 1000 条查 embed_model 字段的众数),不一致即拒绝启动并告警——宁可起不来,不可新旧混跑。这四个字段的成本是每条几十字节的存储与一次启动检查,换来的是「索引和现实对不对得上」永远有据可查——一致性工程的全部地基就在这四个字段上。
回滚的细节:留着旧索引的那一周
双索引切换的回滚窗口期,三个细节决定回滚是否顺畅。旧索引只读冻结:切流后旧索引停止写入(变更都落新索引),但保留完整数据——回滚时它作为「已知的良好状态」整体接回流量,新窗口期的变更缺失量单独评估(通常一两周的增量,回放摄入管道即可补齐)。切换开关集中在配置:流量指向哪个索引必须是一个配置项(或路由层的一个参数),回滚 = 改配置 + 热加载,分钟级完成——回滚动作如果需要改代码重新发布,它就不可能在凌晨的事故里被执行。回滚判据事先写好:什么算「新索引回归」(错误率、延迟、抽样答案质量的阈值)在切换前就定下并贴在发布文档里,事故现场只做比对不做讨论。回滚方案的价值不在概率上——大多数迁移不会触发它;在心理上:知道随时能退,团队才敢真切。
把一致性变成日常:三个机制
机制一:版本戳贯穿全链路。 每个向量记录都带:源文档 id 与版本、嵌入模型标识(模型名+版本号)、切块参数指纹(哈希)。查询端校验「库内模型标识与当前配置一致」,不一致就告警——这是防「新旧混杂」的最后闸门。机制二:一致性巡检。 定期抽样比对:源文档哈希 vs 库内记录、派生树各层计数(块数、摘要节点数是否符合预期结构)、孤儿记录扫描(源文档已删、向量还在)。机制三:重建能力演练。 全量重建是所有事故的最后恢复手段,平时就要验证它跑得通、跑得完——重建管道平时不通,出事时就是灾难。
定期巡检清单
定期一致性巡检的五项清单,按顺序跑:抽样哈希比对(随机抽 1% 源文档比对库内版本号,发现内容漂移即告警);孤儿扫描(库内有向量、源系统无文档的记录清单——删除级联漏掉的证据);派生树计数(每篇文档的块数、摘要节点数与预期结构比对——父子映射断裂的信号);模型指纹普查(嵌入模型字段的众数与当前配置比对——新旧混跑的最后防线);缓存依赖抽查(随机抽缓存条目,验证其依赖块仍存在且版本一致——失效链路的完整性检查)。五项合计几小时跑完,每周一次的频率足够;全部进同一个告警通道,任何一项异常都意味着索引与现实出现了缝隙。
小结
RAG 索引的一致性工程可以压缩成三句话:文档变更走级联重建(以源文档为原子、派生树全删全建);模型与切块变更走全量迁移(新向量绝不与旧向量混库,双索引灰度 + 评测集验证后再切);元数据变更走批量回填(幂等脚本 + 计数校验)。再配版本戳、巡检、重建演练三个机制兜底。这些工程投入平时看不见价值——直到某天用户拿着系统给出的「三个月前的旧制度」来质问,你会庆幸版本戳和级联重建早就是现成的。
参考资料
- 本系列前篇:《父子检索》《RAPTOR 与层级索引》《语义缓存》《权限感知检索》《评测集构建》
- LlamaIndex: Ingestion Pipeline & Caching —— 摄入管线的幂等与文档级缓存设计
- 提示:双索引灰度期间的重排序合并,实现参考《混合检索与重排序》的 RRF 一节,把「新旧索引」当作两路召回处理
读者留言
COMMENTS 暂无还没有留言,来说第一句?