跳到主要内容

你的向量索引是一个没有失效策略的缓存

· 阅读需 11 分钟
Tian Pan
Software Engineer

向量索引感觉就像一个数据库。你向其中写入文档,查询它,它返回结果。但它并不是数据库——它是存储在其他地方的数据的“派生、非规范化副本”。你的事实来源是 wiki、工单系统、CRM 或 PDF 文件夹。嵌入 (embeddings) 是这些事实的投影,冻结在你运行摄取任务 (ingestion job) 的那一刻。

这使得你的向量索引变成了一个缓存。就像所有缓存一样,它会失效。不同之处在于,大多数团队是有意识地构建缓存层,带有 TTL 和失效钩子 (invalidation hook),而几乎没有人会有意识地将向量索引作为缓存来构建。他们将其构建为“知识库”,然后在它提供三周前就已经过时的知识时感到惊讶。

任何在生产环境中运行 RAG 的人对这种症状都很熟悉:用户更新了文档,智能体 (agent) 却一直在引用旧版本。员工离职了,智能体却不断向那些本不该看到文档的人展示他们编写的内容。一个页面被删除了,智能体却自信地引用一段在互联网上已不复存在的段落。这些都不是模型的失败。模型完全按照指示行事:检索最近邻并据此回答。只不过那些“邻居”全是谎言。

这不是一个新问题。它是计算机科学中最古老的难题换了副新面孔。Phil Karlton 的名言——“计算机科学中只有两件难事:缓存失效和命名”——本来是一个玩笑。但在 RAG 中,它是一份路线图。

索引就是缓存,所以请这样命名它

这种重新定义很重要,因为它改变了你所采取的策略。如果你把向量库看作知识库,那么自然的维护计划就是“定期重新索引”——一个每晚或每周进行全量重新嵌入的批处理任务。这在缓存设计中相当于每晚刷新整个 Redis 实例,并希望没人注意到中间的空档。

一旦你接受了索引就是缓存,问题就会变得具体且可回答。一致性模型 (consistency model) 是什么——允许索引有多旧?什么会触发失效 (trigger invalidation)——是时钟、源数据变更,还是显式的清除?对于源文档已不存在的条目,淘汰策略 (eviction policy) 是什么?缓存未命中 (cache miss) 时会发生什么——检索是报错提醒,还是无声地返回空结果?

缓存理论对所有这些问题都有答案。RAG 团队之所以不断以惨痛的方式重新发现这些问题,是因为向量数据库厂商向他们推销的是“数据库”,而一致性问题在营销辞令中被遗忘了。真正的数据库会在写入和随后的读取之间提供事务保证。而向量索引充其量只能提供最终一致性,通常是“数据团队上次运行流水线时的一致性”。

将缓存视为数据库会导致以下三种失败模式。

失败模式一:永远无法传播的编辑

源系统中的文档发生了变化。有人修正了定价文档中错误的数据,重写了入门指南,或者将某项政策标记为废弃。原始数据现在是最新的,但嵌入 (embedding) 不是。

天真的解决方法是按计划进行全量重新嵌入。这在规模扩大之前是有效的。一旦你拥有数十万个分块 (chunks),重新嵌入所有内容将耗费不菲的资金,耗时数小时,而且——人们容易忽略的一点——这会使你的索引在整个运行期间处于“部分过时”的状态。一半的分块反映的是今天,另一半反映的是昨晚,而在此期间的查询可能会跨越这两部分。你并没有消除陈旧性;你只是让它变得不可预测。

生产环境的答案与几十年前数据工程领域得出的结论一致:变更数据捕获 (Change Data Capture, CDC)。不要轮询整个语料库询问“什么改变了?”。订阅源系统的变更流——数据库的预写日志 (WAL)、来自文档系统的 webhook 或文件哈希差异——并只对真正发生变动的记录进行重新嵌入。这是一个写透式 (write-through) 缓存。成本随变化率而非语料库规模而扩展,这是唯一能在接触大型知识库后存活下来的扩展特性。

CDC 还会迫使团队做出一个平时会逃避的设计决策:分块身份 (chunk identity)。如果一份 40 页的文档改动了一个句子,你肯定不想重新嵌入其所有的 38 个分块。你需要稳定的分块 ID 和每个分块的内容哈希,这样流水线就只会重新嵌入那个变动的分块,而保持其他分块不动。这就是失效策略与“暴力推倒重来”的区别。

失败模式二:留下“幽灵”的删除

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates