RAG 增量索引一致性:处理更新、删除和重复事件的生产设计
文档进了向量库不代表知识库已经正确。本文讲解增量索引的版本号、事件去重、删除墓碑、重试和一致性验证,避免 RAG 长期引用过期或已删除内容。
本文要点
- 主题聚焦:大模型、RAG、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
RAG 的索引事故往往不是“完全没有数据”,而是更难察觉的半一致状态:原文已更新但旧 chunk 仍被召回,文档权限已收回但向量仍可查到,webhook 重放导致同一段文字出现三次。用户看到的答案语气很肯定,却基于过期知识。这类问题不能靠定期全量重建解决,必须把文档生命周期当成一条可追踪的数据管线。
给文档和 chunk 都加版本
每个源文档需要稳定的 documentId、内容版本、来源更新时间和删除状态。切分后的每个 chunk 也应带上 documentId、documentVersion、chunk 序号和内容 hash。检索时只允许使用当前版本,旧版本可以保留一段时间用于回滚和审计,但不能和新版本同时参加召回。
不要把文件名或 URL 当作唯一身份。知识库常会移动页面、重命名文件或迁移系统,路径变化不应自动制造一份新文档。稳定 ID 可以来自源系统主键;没有主键时,要建立 URL 规范化与内容指纹策略,并明确人工合并流程。
事件至少一次时,写入必须幂等
大多数 webhook 和队列都可能重复投递。索引 worker 接收到“文档更新”时,先比较版本和 hash:版本更旧的事件直接丢弃,版本相同且 hash 相同的事件返回已处理,只有更高版本才进入解析和 embedding。写入向量库时使用由 documentId + version + chunkIndex 组成的确定性 chunk ID,而不是每次生成随机 ID。
这样即使 worker 重启、队列重放或人工点击重试,也不会产生重复向量。失败记录里保留源事件 ID、文档版本、失败阶段和可重试标志。重试不是重新抓取一切,而是从失败阶段恢复,并确认当前版本还没有被更晚的事件取代。
删除不是从 UI 隐藏就够了
删除事件要优先处理。对于可物理删除的索引,删除该文档所有当前 chunk;对于需要保留审计痕迹的场景,写入 tombstone 并让检索过滤 deletedAt is null。无论哪种方式,必须保证缓存、全文索引、向量索引和引用页一起失效。只删向量、不删缓存,用户仍可能在几分钟内读到旧答案。
权限收回也可视为一种逻辑删除。不要等待夜间批处理才移除敏感文档;权限事件应触发即时过滤或索引更新。高风险数据应优先依赖检索时 ACL 过滤,再用异步清理降低存储暴露,而不是只相信异步删除能及时完成。
用对账证明数据真的一致
每天或每小时抽样比对源系统与索引:当前文档数量、版本分布、缺失 chunk、孤儿 chunk、删除延迟和失败队列长度。对账结果应能定位到 documentId,而不是只给一个总数。发布新解析器或切分策略时,先在影子索引中重建并比较召回与覆盖,再切换生产别名。
增量索引的目标不是最快把新文档塞进向量库,而是保证任何时刻都能解释“这条答案来自哪个版本、为什么它仍可见”。有了版本、幂等和对账,RAG 才能像可靠的数据系统一样长期运行。
迁移与回滚也要进入索引设计
切分器、embedding 模型或 metadata schema 改动时,不能在原索引上原地覆盖。建立新索引版本,按固定样本验证文档覆盖、权限过滤、召回质量和延迟,再把查询别名切到新版本。旧索引保留到回滚窗口结束,避免发现解析错误后只能重新等待全量构建。
回滚并不意味着把源文档版本倒退,而是恢复查询使用的索引实现。记录每次索引切换的配置、模型、文档版本范围和验证报告;事故发生时可以快速判断是源内容、解析、embedding 还是检索配置导致差异。这样数据管线的每一步都有可恢复边界,而不是一次不可逆的批处理。
对索引 worker 的监控应包含事件滞留时间、当前版本落后数、每个来源的失败率和孤儿 chunk 数。把这些指标按来源和文档类型拆开,才能发现某个 PDF 解析器或某个知识库连接器正在系统性落后。发现问题后优先阻止旧版本被标记为当前,再进行补偿重放。
延伸阅读
Related