跳到主要内容

AI缓存失效:为什么答案可以改变时每个缓存层都更难处理

· 阅读需 11 分钟
Tian Pan
Software Engineer

Phil Karlton有句名言——"计算机科学中只有两件难事:缓存失效和命名"——这句话诞生于语言模型进入生产之前。将AI加入技术栈后,缓存失效不只是变得更难;它在每一层同时变得更难,而且每一层的原因从根本上不同。

传统缓存存储的是确定性输出:数据库行、渲染的HTML、计算出的价格。当源数据变化时,你使该键失效,下一个请求获取新数据。契约很简单,因为答案是一个事实。

AI缓存存储的是不同的东西:对查询的响应,而这些响应的"正确"答案取决于上下文、时效性、模型行为以及模型所获取的源文档。这里的"陈旧"不意味着过时——它意味着在语义上出错,而你的监控不会发现,直到用户注意到为止。

你可能没有区别对待的四个缓存层

一个生产级LLM应用通常至少有四个不同的缓存层,每层都有不同的失效特性:

提示缓存(由Anthropic和OpenAI提供)存储提示前缀的KV计算。这些最接近传统缓存——它们对token序列进行精确匹配,对缓存前缀的任何更改都会使下游所有内容失效。失效规则是机械的但不容忽视:重组系统提示,将频繁变更的内容移到文档更高处,就会驱逐整个缓存。

语义缓存(如GPTCache、Redis语义缓存等工具)基于嵌入相似度而非精确字符串匹配来存储LLM响应。余弦距离在阈值内的新查询会得到缓存的响应。这些是最难推理的危险缓存——因为没有明确的失效边界——同一个缓存响应可能为数千个语义相似但不完全相同的查询提供服务。

RAG检索缓存存储向量相似度搜索的结果:给定查询嵌入,返回这些文档。当源文档更新时,缓存的检索结果可能指向陈旧的文档版本,或遗漏新近相关的内容。

嵌入索引是底层——文档的预计算向量表示。当文档更改时会重建,但重建代价高昂且通常是异步的,这意味着存在一个窗口期,你的检索在陈旧的嵌入上运行。

每一层都有不同的故障模式、不同的适当一致性模型,以及不同的等待爆发的生产事故。

为什么传统失效在每一层都失效

核心问题不是技术性的——是语义性的。传统缓存存储事实。AI缓存存储在特定模型、特定源文档集合和特定时刻下适当的响应。TTL和基于标签的失效假设你能在"仍然有效"和"陈旧"之间划清界限。AI缓存不提供这条线。

语义缓存会放大幻觉。 如果LLM对某个查询生成了错误的响应,未来所有语义相似的查询都会从缓存中获取那个幻觉内容。错误不会衰减——它会复利累积。标准缓存可能提供陈旧数据直到你推送修复;语义缓存会提供错误数据,直到你识别出那个特定的查询簇并明确使其失效。

嵌入模型升级是你不会注意到的缓存破坏事件。 当你升级嵌入模型——从旧模型迁移到更新、更强大的版本——新嵌入在数值上与旧嵌入不兼容。模型A的0.9相似度分数与模型B的含义完全不同。如果你没有将模型版本作为缓存键的一部分,你会无声地混合来自不同模型的嵌入,并得到悄然出错的检索结果。

语义漂移使TTL失去意义。 2022年提交到开发者文档系统的查询中,"Python"这个词有一种分布,而现在则有另一种分布。产品名称改变了含义。监管术语不断演化。六个月前正确表示文档的缓存嵌入,现在可能映射到嵌入空间的错误区域——不是因为文档改变了,而是因为围绕它的语言改变了。

文档更新不会在缓存栈中传播。 一家法律科技公司曾深受其苦:律师更新了一份合同,再次搜索时,却通过RAG检索缓存获取到了旧文档的缓存版本。检索缓存不知道源文档已经更改。传统缓存失效会很简单——当文档更改时你知道要使哪个键失效。但检索缓存是按查询嵌入为键的,而不是文档ID,所以没有干净的失效路径。

为每个缓存层匹配一致性模型

并非每个缓存层都需要相同的一致性保证,试图在所有地方应用强一致性会损害你的延迟,而不会有效地提高正确性。

强一致性——每次读取都反映最新写入——适合于错误会有明确下游后果的基于事实的缓存:财务数据、合规关键的政策文档、身份验证决策。它很少适合语义缓存,因为协调开销会消除你引入缓存所获得的大部分延迟优势。

最终一致性适用于语义缓存和大多数RAG检索缓存,因为用户可以容忍短暂的略微陈旧响应,且更新频率相对于查询频率很低。风险是无界的陈旧——最终一致性不告诉你"最终"有多终。你需要为其设定边界。

有界陈旧性是大多数生产RAG系统的正确模型。保证是:"这个缓存条目不超过X秒。"你通过文档存储的变更数据捕获流来实现——当文档更新时,你发出一个事件,触发引用该文档的检索缓存条目的失效,同时有最大延迟界限。这给你提供最终一致性的性能,以及一个你可以推理并向用户传达的最坏情况陈旧窗口。

会话一致性——用户总是能立即看到自己的写入——自然适用于提示缓存。用户的对话应该始终保持自我一致;在用户注意到缓存未命中之前,应该为其会话预热提示缓存。

四种真正有效的设计模式

带命名空间纪元的版本化嵌入。 将嵌入模型版本直接嵌入缓存键中:key = hash(query, embedding_model_v3)。当你升级嵌入模型时,旧缓存键会通过TTL自然冷却,无需显式批量删除。在迁移窗口期间,你同时运行两个版本,逐渐预热新缓存,同时旧缓存自然消退。这也防止了最严重的故障模式——在向量索引中混合不兼容的嵌入。

用于检索缓存失效的文档指纹。 内容寻址缓存是文档更新问题的解决方案。在检索缓存键中包含源文档内容的哈希值。当文档更改时,其指纹改变,所有引用它的缓存条目自然会未命中——无需显式失效扫描。使浏览器资产缓存正确工作的同样方法在RAG中同样有效:你计算hash(document_content)并将其作为缓存标识的一部分。

每层设有明确新鲜度要求的分层架构。 在语义缓存中维护静态层和动态层。静态层包含经过离线审核的精选问答对,你确信答案不会频繁变化——这些可以使用高相似度阈值和长TTL提供服务。动态层包含具有积极驱逐的新鲜内容。查询首先命中静态层;未命中会落到动态层;动态层未命中则进行完整的LLM推理。成本节省会复利叠加:每一层捕获不同类别的查询,每一层的新鲜度属性是显式且可审计的。

增量索引更新而非完全重新索引。 对大型文档语料库进行完整重新索引代价高昂且缓慢——一个基准测试显示12,000个文件需要22分钟和大量API成本。增量索引跟踪文档血统:当文档更改时,只重新计算其嵌入;当文档删除时,删除其嵌入;当文档未更改时,重用缓存的嵌入。同一基准测试显示增量更新在45秒内完成。工程投入在于构建变更检测层——CDC流、文档变更日志或版本跟踪的文档存储。

惊群效应对语义缓存更糟糕

传统缓存踩踏发生在许多条目同时过期时,压垮了源服务。语义缓存踩踏更微妙也更糟糕。

当你使语义缓存失效时——比如,在产品政策更新后——你不是在使单个键失效。你是在使涉及嵌入空间特定区域的查询簇失效。如果不扫描整个缓存,你不知道哪些查询。当这些查询开始未命中时,它们同时命中LLM,延迟峰值随变更内容的语义覆盖面扩大。

给TTL添加随机抖动可以减少同时过期的问题,但会引入语义不一致性:相似查询可能获得不同年龄的响应,意味着在抖动窗口期间,一些用户看到旧策略而另一些看到新策略。对于大多数内容,这是可以接受的。对于合规关键或法律内容,则不可接受。

解决方案是精确界定你的失效范围:当文档更改时,发出一个事件,只使检索了该特定文档的缓存查询失效。这需要跟踪每个缓存查询是哪些文档贡献了响应。这是更多的记录工作,但它将O(缓存大小)的失效转换为O(涉及该文档的查询)。

这对你的下一个架构决策意味着什么

如果你在生产中缓存了LLM响应,但没有分别思考每一层,你几乎肯定在犯以下错误之一:

  • 将TTL作为语义缓存上的唯一失效机制,意味着你要么提供陈旧响应太长时间,要么在条目正确过期时为不必要的LLM调用付费。
  • 在不对缓存键进行版本化的情况下升级嵌入模型,意味着随着不兼容嵌入共存,你的检索质量在悄然降级。
  • 不跟踪哪些文档贡献了哪些缓存响应,意味着当文档更改时你无法精确失效。
  • 对所有缓存层应用相同的一致性模型,意味着你要么过度工程化语义缓存,要么工程化不足基于事实的缓存。

心智模型转变是这样的:传统缓存失效问题是"这个数据什么时候陈旧?"AI缓存失效问题是"这个响应什么时候出错?"——这些是从根本上不同的问题。数据陈旧性有清晰的时间戳。响应错误性是模型、源文档、查询分布和用户上下文的函数。

构建可靠AI缓存系统的团队,正是那些停止将语义缓存视为Redis TTL问题,而开始将其视为每层都有明确一致性契约的分布式系统的团队。这些契约并不复杂——但你必须在第一次生产事故迫使这个问题之前定义它们。

Let's stay in touch and Follow me for more thoughts and updates