向量索引感觉就像一个数据库。你向其中写入文档,查询它,它返回结果。但它并不是数据库——它是存储在其他地方的数据的“派生、非规范化副本”。你的事实来源是 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 和每个分块的内容哈希,这样流水线就只会重新嵌入那个变动的分块,而保持其他分块不动。这就是失效策略与“暴力推倒重来”的区别。
失败模式二:留下“幽灵”的删除
删除比编辑更糟糕,因为编辑至少会产生一个新的嵌入来与旧的竞争。而删除什么都不产生。源文档消失了,而嵌入却……依然徘徊。它依然匹配查询。它依然排名靠前。智能体检索到它,并引用一个如果有人点击就会返回 404 的文档。
向量数据库很容易在这方面出错,因为大多数数据库将删除实现为“软删除” (soft delete)。向量被标记上墓碑 (tombstone) 并在搜索结果中被过滤掉,但它物理上仍保留在索引中,直到压缩任务 (compaction job) 回收空间。作为存储引擎的细节,这没问题。但当你的应用程序根本没有发出删除指令时,这就变成了 bug——因为你的摄取流水线只知道如何添加和更新,而不知道如何删除。
这是我见过的最常见的遗漏。团队构建了一个“upsert”流水线。新文档:嵌入它。更改的文档:重新嵌入它。删除的文档:……什么都不做。没有相关的代码路径,因为源系统不再提及该文档,而除非你专门为此设计,否则“消息的缺失”并不是你可以订阅的事件。
两种模式可以弥补这一缺陷。第一种是应用层的墓碑 (tombstones at the application layer) :当 CDC 报告删除时,你不只是丢弃向量,而是记录下这个分块 ID 已被删除,以便稍后的对账环节确认它确实消失了。第二种是集合对账 (set reconciliation) :定期将索引中的分块 ID 集合与源系统中的活动文档 ID 集合进行比对,并剔除索引中没有现存父文档的任何内容。CDC 处理快速路径;对账则捕获 CDC 漏掉的删除。你需要两者兼备,原因就像你既运行增量备份,也会偶尔运行全量备份一样。
失败模式三:权限寿命长于访问权 最危险的陈旧条目并非错误内容——而是读者已不再被允许查看的“正确”内容。
在任何真实组织中,访问权限都在不断变化。承包商的项目结束了。员工调换了团队。一份文档从“内部”被重新归类为“受限”。在源系统中,这种变化是即时的:当该人员下次打开文件时,他们会看到“拒绝访问”页面。而在向量索引中,嵌入(embedding)——以及至关重要的、附加在其上的权限元数据——反映的是抓取时的情况。
如果你的检索层通过读取索引分块上的权限标签来过滤结果,那么这些标签也是一种缓存,而过时的缓存会将机密数据泄露给那些在上个季度就已被取消访问权限的人。这并非虚构的极端情况;它是一类公认的 RAG 漏洞,而且正是那种会让安全审计变成漫长噩梦的发现。
有两种可行的设计方案,以及一种流行但不可行的方案。不可行的方案是在索引中缓存权限并信任它们。第一种可行的设计是绝不缓存授权决策 :在每个分块(chunk)上只存储一个稳定的资源 ID,并在查询时针对实时 访问控制系统检查当前 用户的当前 权限。授权在每次检索时都会重新计算;只有嵌入是被缓存的。第二种方案是将权限变更视为头等 CDC(变更数据捕获)事件——当源系统中的 ACL 发生变化时,该变动会使受影响分块的元数据失效,就像内容编辑一样。
这两者背后的原则是:提供稍显陈旧的内容 是可以接受的,因为一天前的段落通常仍然有用。但提供陈旧的授权 是绝不可接受的,因为过时一天的权限就意味着安全违规。缓存那些昂贵且变化缓慢的东西——即嵌入。永远不要缓存廉价但关乎安全的决策——即访问决策。
在需要之前构建失效策略 这三种失败模式的共同点在于,它们都不是检索质量问题。你的相似性搜索可能完美无缺——找到正确的最近邻并进行了完美排序——但系统仍然可能提供已删除的文档、修改前的数字或已取消访问权限的文件。相似性搜索从来不是难点。近似最近邻(ANN)是一个已有出色开源实现方案的成熟问题。难点在于保持缓存的真实性,这是一个被“向量数据库”架构有意隐藏的数据工程问题。
如果你目前正在运行 RAG,可以采取以下具体行动:
制定一致性 SLA。 “编辑在 5 分钟内可见,删除在 1 分钟内生效,权限变更立即生效”是一个真实的规范。而“我们每周重新索引一次”则不是——它是缺乏规范的表现。
由变更事件而非计划任务驱动数据抓取。 遍历全球的批处理任务无法扩展,且在每次运行时都会导致部分数据陈旧。CDC 则能随数据变动规模化扩展。
显式构建删除路径, 并通过索引与源系统之间的定期集合对账(diff)来支持。消息缺失并不代表事件发生;你必须主动去核查。
为每个分块打上内容哈希和源版本标记, 以便检索系统能够检测(并可选地拒绝)那些不再与源匹配的条目。
衡量新鲜度,而不只是相关性。 你的评估套件几乎肯定会对检索是否返回相关分块进行打分。增加一个衡量这些分块是否仍与实时源匹配的指标。在对其进行检测之前,陈旧性是不可见的;如果你不关注正确的指标,由陈旧嵌入导致的 20% 准确度下降看起来就像是模型变“笨”了。
交付可靠 RAG 的团队并不一定是拥有最佳嵌入模型的团队。而是那些审视向量索引并认清其本质——即缓存——并构建了“缓存”一词所要求的失效策略的团队。检索增强生成中的难题从来不是寻找最近邻。而是确保该邻居依然存在、其内容依然准确,并且依然属于有权阅读它的人。
会员专享
余下内容仅对会员开放。 会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
— 完整文章,包含未公开存档的部分 — 可落地的工作框架,附带权衡与决策依据 — 新文章抢先看,先于公开发布 登录以继续阅读→ 随时取消 · 一次订阅,畅读全部