在接下来的 15 分钟里,你的检索索引包含了一个在她的脑海中从未在任何单一时刻存在过的 Wiki 状态。入职指引页仍然保留着那个章节。运维手册里却还没有。那个草稿占位符在被删除到一半时被捕获了,里面包含了一句她从未打算发布的占位语句。旧的弃用警告仍然被索引着。当一名工程师询问智能体“我们如何在这个服务中处理凭证轮换”时,模型从同一个来源检索到了矛盾的分块,并自信地合成出评分较高的那一个。答案呈现出一种任何人都没写过的错误形态。
“飞行途中”的读取还有一个类似的失效模式,它更容易复现,却更难修复,并且在“智能体告诉了我一些甚至已经不在 Wiki 里的东西”这类工单中占了很大比例:复活的草稿。
一名作者开始写一个新页面,写了两个段落,然后去吃午饭,回来后觉得构思不对,删掉了整个页面。总寿命:40 分钟。你的摄取任务恰好在第 25 分钟运行了。这两个段落现在进入了你的向量索引。而它们来源的页面在 Wiki 端已不复存在。
在下一次摄取过程中,该页面消失了——但“消失”是摄取任务必须主动检测并采取行动的信息。如果流水线只是遍历当前 Wiki 中的内容并更新插入它发现的内容,它就收不到任何东西消失了的信号。这些孤儿分块将永远留在向量库中。它们会被检索。它们会被引用。作者废弃的草稿在一种真实且可观察的意义上,变成了知识库的永久组成部分,只要索引还在,就能在智能体的回答中被检索到。
这种失效模式会永久破坏 Wiki 与智能体之间的信任。用户会发现他们删除的东西其实并没有消失。一旦接受了这个教训,Wiki 就不再是人们放置高可信度草稿的地方。写作任何内容的成本都会上升,因为“删除”不再是一个起作用的原语。
架构上的修复方案是停止轮询。现代 Wiki 平台都会发出编辑事件。Notion 有 Webhook。Confluence 有事件监听器。两者都可以在编辑落地的亚秒级延迟内,向你的流水线推送变更通知。摄取变成了事件驱动:一个 CDC 订阅者,接收“页面 X 的修订版本 Y 刚刚保存”的消息并处理该单一变更,而不是一个每隔 N 分钟遍历整个世界的任务。
CDC 缩小了新鲜度差距,但它本身并不能解决孤儿切片(orphan-chunk)问题。如果一个页面被删除,该删除操作必须进行传播。如果一个切片被更新的版本替换,旧版本就必须消失——而在向量数据库中,“消失”比听起来要复杂得多,因为新版本通常不会生成完全相同的切片。重新表述的段落会产生具有新嵌入、新 ID 和新位置的切片。旧切片不会被 ID 覆盖;它们会因为缺失而变得孤立。
之所以奏效,是因为事实来源(Source-of-truth)——维基(Wiki)——已经将版本号作为一个一等公民概念来维护。摄取流水线的工作是将该版本概念镜像到向量存储中,从而使“最新”成为检索层可以执行的属性。前一节提到的孤儿草稿之所以会消失,是因为删除事件将页面版本 N 标记为墓碑,而所有版本号小于 N 的切片在查询时都会被过滤掉。
对于删除操作,同样的原则也适用:删除事件会创建一个最终的“页面在版本 N 被删除”的记录,任何标记有该页面的切片都会被排除在检索之外,直到垃圾回收将其移除。向量存储不是内容存储——它是一个派生索引,其生命周期必须从属于源系统的生命周期。
Neither pattern is perfect. Both are dramatically better than the implicit assumption that fifteen-minute polling will produce a consistent index. The general principle: the ingest pipeline has to know that the source is concurrent, and has to model that concurrency rather than ignore it.
这两种模式都不是完美的。但它们都比“假设 15 分钟轮询就能产生一致索引”的隐含假设要好得多。通用原则是:摄取流水线必须知道源系统是并发的,并且必须对这种并发性进行建模,而不是忽略它。
做得好的团队会像对待数据库客户端一样对待他们的 RAG 流水线。尽可能采用事件驱动。全程具备版本意识。即使在源头没有明确定义事务时也能感知事务。立足于能探测出不一致性而非将其通过平均值抹平的评测(evals)。并且诚实地面对这样一个事实:对于“你的索引现在是否与你的 Wiki 保持一致”这个问题,默认的回答是“不”——而工程工作的重点在于不断缩小那个时间窗口,直到回答为“是”的频率足以维持信任。