文章以「缓存失效」经典难题切入,解释了大模型 KV Cache、Prompt Caching 的工程代价与安全风险,对理解 LLM 推理层开销有实质帮助。
那个笑话从来就不只是个笑话
计算机科学有一条古老的箴言:
"计算机科学中只有两件困难的事:缓存失效和命名。"
这句话通常被归功于 Netscape 工程师 Phil Karlton。但其归属格外有意思,因为似乎没有任何同时代的书面证据能证明他究竟何时何地说过这句话。Karlton 的儿子 David Karlton 曾表示他父亲确实用过这个说法,而 Martin Fowler 则记录说 Tim Bray 在 1996–97 年左右听到过这句话,后来帮助它在互联网上广泛传播。
然后有人把这个笑话升级了。
"计算机科学中只有三个困难的问题:缓存失效、命名,以及 off-by-one 错误。"
第三句后来被归到 Leon Bambrick 名下,而非 Karlton。Martin Fowler 对这句话历史的考证明确将原始的两句版本与后来的 off-by-one 变体区分开来。
这句话之所以流传了几十年,并非因为程序员喜欢复读老笑话。
它流传下来是因为缓存制造了一种非常特殊的谎言。
缓存存储的是一个曾经正确的答案。
然后世界变了。
突然,你的系统在自信地返回昨天的真相。
这听起来像是一个 Web 开发问题。
但请注意,在现代 AI 系统中,我们正在缓存:
AI 并没有解决缓存失效问题。
它只是让后果变得大得多。
最基础层面上,缓存很简单。
假设计算一个昂贵的操作需要 100 毫秒。
你计算一次。
下一次,不再花 100 毫秒重新计算,而是返回保存的值。
这就是全部思路。
WITHOUT CACHE
Request
|
v
Compute expensive thing
|
v
Result
WITH CACHE
Request
|
v
Is result already cached?
| \
YES NO
| |
v v
Return result Compute it
|
v
Store result
|
v
Return result
性能提升可能是巨大的。
这就是为什么缓存在各处都存在。
操作系统页面缓存。
包管理器缓存。
问题隐藏在一个看似无害的问题里:
缓存结果何时不再有效?
这个问题就是喜剧结束的地方。
想象一个网站显示:
User: Mahan
Plan: Pro
Tokens: 14,281
应用程序从数据库读取这些数据。
为了让网站更快,它缓存了结果。
Mahan 升级了账户。
Plan: Enterprise
Plan: Pro
现在你有了两个现实。
数据库代表当前状态。
缓存代表旧状态。
两台机器都没有故障。
数据库是好的。
缓存也正是按你的指令在做。
这就是令人恐惧的地方。
Bug 就在架构里。
Redis 文档以同样的方式描述了经典的 consistency 问题:当真相来源变了但缓存副本没变时,缓存就变得不一致了。不同的缓存策略之所以存在,正是因为没有单一的通用解决方案。
这就是缓存失效之所以出名的原因。
"我能复用这个旧答案吗?"
"我能证明这个旧答案仍然是安全可复用的吗?"
这是两个非常不同的问题。
第二个问题才是系统工程开始变得诡异的地方。
假设存在这样一个函数:
def get_user():
return database.query("SELECT * FROM users")
@cache
def get_user():
return database.query("SELECT * FROM users")
现在另一个函数更新用户:
def update_user(name):
database.execute(...)
数据库变了。
但你缓存的 get_user() 结果没有变。
所以系统需要某种关系:
update_user()
|
+---- invalidates ----> get_user() cache
但真实的程序不包含一个整洁的依赖关系。
Database
|
+--> User API
| |
| +--> Dashboard
|
+--> Search index
|
+--> Recommendation engine
|
+--> Analytics
|
+--> Mobile API
|
+--> Background jobs
现在改变一条数据。
有多少缓存可能受影响?
这才是真正的问题。
失效是伪装成家务活的依赖追踪。
系统越大,这些依赖关系就越不明显。
现在把用户数据库替换成语言模型。
假设应用程序反复发送:
You are a senior software engineer.
Project:
[large repository]
Architecture:
[long documentation]
Tool definitions:
[dozens of tools]
Rules:
[system instructions]
Conversation history:
[thousands of tokens]
Now answer this question:
...
没有缓存,系统可能需要反复处理相同的大前缀。
有了缓存,系统可以复用已经完成的工作。
这正是现代 AI 提供商正在优化的 workloads 类型。
OpenAI 于 2024 年 10 月引入了自动 prompt caching,解释说重复的输入 tokens 可以被复用以降低延迟和减少输入 token 成本。初始公告描述了支持模型上缓存输入 tokens 享受 50% 折扣。
Anthropic 同样将 prompt caching 描述为一种复用频繁重复上下文的机制,对于重复的长上下文工作负载,潜在延迟降低超过 2 倍,成本降低高达 90%。
Google 的 Gemini API 现在也支持 context caching,包括在较新的 Gemini 模型上的自动缓存。Google 当前的文档明确告诉开发者将大量常见内容放在 prompt 开头,并在相近时间内发送相似的前缀,以提高 cache-hit 概率。
注意到刚刚发生了什么。
一个在现代 LLMs 主流化之前就存在的计算机科学笑话,悄然成为了 AI 推理经济学的一部分。
让我们简化一个 LLM 请求。
SYSTEM
+ giant documentation
+ tool definitions
+ conversation history
+ user question
模型处理上下文。
请求的开头通常被称为 prefill 阶段。
然后模型在 decode 期间逐个生成 tokens。
一个简化的心智模型:
INPUT TOKENS
|
v
PREFILL
|
v
KV CACHE
|
v
DECODE
|
+--> token
+--> token
+--> token
+--> token
KV cache 存储中间注意力信息,这样模型就不需要为每个生成的 token 从头重新计算整个历史。
这是高效 transformer 推理背后最重要的优化之一。
现在我们的老朋友又出现了。
我们有计算代价高昂的数据。
现在我们需要决定这些存储的数据何时可以安全地复用。
KV cache 这个术语听起来几乎无害。
在自回归推理期间,模型逐个 token 生成输出,同时维护表示先前上下文的 key/value 张量。
这些张量消耗 GPU 内存。
而且它们是动态的。
它们需要在并发请求之间被调度。
它们在争夺极其宝贵的内存带宽。
vLLM 背后的原始 PagedAttention 论文将 KV cache 描述为一个主要的内存管理挑战。作者明确指出碎片化和冗余复制是现有系统浪费 GPU 内存的主要原因,然后引入 PagedAttention 来像虚拟内存中的页面一样管理 KV cache 块。他们的评估报告称,在相似延迟条件下,相比对比的 serving 系统实现了 2–4 倍的吞吐量提升。
这个概念联系很美妙。
操作系统用以下方式解决内存管理:
logical memory
|
v
pages
|
v
physical memory
LLM 推理引擎可以用:
logical sequence
|
v
KV blocks
|
v
GPU memory
这不仅仅是比喻。
vLLM 的文档将自动前缀缓存描述为缓存 KV-cache 块,并在新请求共享相同前缀时复用它们。它目前的实现使用哈希块和缓存管理层来查找可复用的前缀。
这意味着现代 AI serving 独立地重新发现了大量旧的系统思想。
我们没有逃离旧的计算机科学。
我们把它一起带了过来。
这正是 AI 营销语言可能产生误导的地方。
"Prompt caching 将成本降低 90%。"
然后在心里把它转换成:
"我的 AI 应用程序现在将便宜 90%。"
缓存只有在工作负载产生有用的 cache hits 时才有帮助。
Request 1:
[system][docs][tools][question A]
Request 2:
[system][docs][tools][question B]
潜在地非常可缓存。
请求 1:
[system][user context A][question A]
请求 2:
[system][user context B][question B]
前缀出现了分歧。
经济学逻辑也随之改变。
系统可能仍然缓存了开头部分,但有用的可复用部分很大程度上取决于请求结构。
Google 的文档将这种操作现实明确化:开发者可以通过将公共内容前置、以及在时间上让相似前缀保持紧密来提高隐式缓存命中率。
所以性能问题不再仅仅是:
"我们有没有缓存?"
"我们是否将工作负载设计成能让缓存真正发挥作用的样子?"
这是一个难得多的问题。
这就是现代 AI 系统变得异常复杂的地方。
想象一个 AI 编码智能体。
System prompt + developer rules + tool definitions + repository files + retrieved documentation + conversation history + current task
现在改变其中一件事。
仓库中的单个文件被修改了。
什么会失效?
也许是仓库上下文。
也许是一次检索结果。
也许是某次工具输出。
也许是一段缓存的前缀。
也许是一条生成的摘要。
也许是一个衍生的索引。
又或者,以上所有内容中的某几部分。
这就是噩梦。
缓存不理解你的业务语义。
所以工程师必须创建一种映射:
语义 | v 缓存键 | v 缓存产物
如果键错了,缓存可能完全高效却完全不正确。
cache["user:123"] = ...
现在想象缓存一个模型上下文:
model system prompt tool definitions conversation documents permissions tenant locale version configuration
突然之间,这个键可能实际上需要代表:
K = (model version)
忘掉一个重要输入。
你就创造了一个过期缓存 bug。
而现在这个 bug 可能不会让服务崩溃。
它可能只是产生了一个错误的答案。
传统缓存通常处理相对清晰的对象。
URL -> HTTP 响应
数据库查询 -> 行
LLM 系统则不同。
结果可能依赖于大量上下文。
Prompt + Model version + System instruction + Tools + Retrieved data + Conversation state + Sampling/configuration
是什么真正决定了之前的结果是否仍然有效?
没有 universal 的答案。
你必须自己定义一个。
这个定义就是你的缓存的语义契约。
这就是为什么缓存失效往往更多是关于理解依赖关系,而不是删除。
这个区别非常重要。
**1. Prompt / 上下文缓存**
系统复用与重复输入上下文相关的处理。
相同的上下文 | v 复用之前的工作
这是公共 API 越来越多公开暴露的内容——prompt 缓存或上下文缓存。
OpenAI、Anthropic 和 Google 现在都提供了缓存机制,旨在降低重复输入的处理成本和/或延迟。
**2. 生成过程中的 KV 缓存**
这是模型运行时的层面。
与其每次都对之前的 token 重新计算注意力状态,服务系统会保留相关的 K/V 张量。
例如,vLLM 将 KV 数据视为块,可以被分配、共享、哈希、驱逐和复用。
但它们并不完全相同。
把它们混为一谈会让关于"AI 缓存"的讨论变得令人困惑。
这就是故事真正变得奇怪的地方。
cache = performance optimization
但共享缓存可以创建侧通道。
假设两个客户使用相同的推理基础设施。
客户 A 的私有 prompt 导致某些前缀块被缓存。
客户 B 发送一个请求,带有一个猜测的前缀。
如果 B 的请求根据该前缀是否已被缓存而产生明显不同的延迟,缓存本身可能泄露信息。
vLLM 的安全文档现在明确讨论了这种威胁,并引用了 CVE-2025-46570,描述了 TTFT(Time To First Token)的差异如何揭示猜测的 prompt 前缀是否与缓存数据匹配。文档将缓存加盐(cache salting)描述为一种缓解措施,使不同的安全主体不会盲目地共享相同的前缀缓存命名空间。
这是一个了不起的演进:
1990s: "缓存失效是困难的。"
2020s: "缓存失效影响成本。"
2026: "缓存设计可以成为机密边界。"
却没有改变其基本目的。
基础设施工程中有一个诱惑:
缓存越多 = 性能越好。
缓存会消耗资源。
失效流量也是如此。
在 LLM 推理中,KV 缓存可能变得足够大,以至于内存容量和带宽直接影响性能。
最近的研究继续描述 LLM 推理严重受内存移动约束。例如,一篇 2026 年的 AAAI 论文专门研究了异步 KV-cache 预取以减少 HBM 内存瓶颈。
另一条最近的研究路线将 KV 缓存作为随着上下文长度扩展而日益重要的瓶颈来研究,因为缓存占用随序列长度增长,给内存容量和带宽都带来了压力。
这创造了一个有趣的反转。
缓存的存在是为了节省计算。
但缓存本身消耗内存和数据移动。
所以最终你会问:
我是否通过消耗带宽来节省计算?
缓存是否刚刚成为我在试图消除原有瓶颈时创造的瓶颈?
这就是计算机科学的精髓。
理解现代 LLM 前缀缓存最直观的方式之一是把它想成一个函数:
f(prefix)
如果你已经计算过:
f("A very long shared prefix...")
为什么要再计算一次?
现代推理引擎可以对前缀块做类似的事情。
vLLM 的自动前缀缓存文档给出了这样的例子:反复查询同一份长文档,或继续一个多轮对话。在这些工作负载中,系统可以复用之前处理过的前缀,而不是为每个请求重新计算该部分。
这是一个穿着非常新外套的非常古老的把戏。
底层思想没有。
还记得那个扩展版本吗?
缓存失效 命名 差一错误
差一那部分看起来像是被钉在另一个笑话上的笑话。
但在将序列划分为缓存块的系统中,边界很重要。
Token: [0 1 2 3][4 5 6 7][8 9 10 11]
如果你的系统的索引、块统计、token 偏移量或所有权逻辑错了一个单位,你可能会得到:
错误的块 错误的查找 错误的复用 错误的驱逐 错误的注意力状态
因此,现代缓存管理器必须关注那些与标题相比感觉低级得可笑的细节:
"我们构建了一个前沿 AI 系统。"
在那个标题下,你可能仍然会发现:
哈希表 LRU 队列 块索引 引用计数 内存分配器 偏移量 驱逐策略
人类发明了万亿参数模型,然后立刻不得不调试一个链表。
我爱这个行业。
这是我以为最容易被忽视的部分。
人们谈论 AI 推理经济学时,仿佛唯一重要的变量是:
FLOPs
但真正的推理受到一个丰富得多的系统的约束:
模型大小 + GPU 内存 + HBM 带宽 + 互联带宽 + 批处理 + 序列长度 + KV 缓存内存 + 调度器效率 + 缓存命中率 + 请求形状 + 延迟要求
缓存可以显著减少重复工作。
但它无法神奇地消除底层数据。
如果请求不共享前缀,它也可能无法提供帮助。
而且它可能引入内存压力。
可能引入安全边界。
并产生生命周期问题。
"缓存失效正在阻止 AI 推理变便宜"这个说法过于简单。
更准确的说法是:
缓存是使推理变得更便宜的机制之一,但随着 AI 工作负载变得更加重复、更长上下文、更多租户,缓存效率本身成为了一等系统约束。
这个区别很重要。
问题不是缓存失效了。
问题是 AI 的成本结构越来越依赖于正确地使用缓存。
想象一个 AI 应用处理 100 万个请求。
8,000 个常见输入 token + 500 个唯一输入 token
现在想象这 8,000 个常见前缀可以被复用。
没有有效缓存的情况下:
8,500 个 token × 1,000,000 次请求
在强大的前缀缓存下:
8,000 个 token 处理一次 + 500 个唯一 token × 1,000,000
确切的经济效益取决于模型、API 定价、缓存策略、工作负载形状和服务架构。
但概念上的差异是巨大的。
你不再需要为反复理解相同内容而付费。
付费一次,然后复用。
这就是为什么各公司会暴露显式或自动的缓存机制。
OpenAI 为缓存提示词的输入 token 价格做广告优惠。Anthropic 宣传在合适的工作负载上可节省高达 90% 的成本。Google 同样记录了缓存命中带来的成本节约,并提供隐式和显式两种上下文缓存机制。
所以那句老笑话已经悄悄转移到了损益表上。
一次缓存未命中现在可以直接对应一笔推理成本。
缓存本应让系统更便宜。
然而现代 AI 系统有时需要越来越复杂的基础设施来使缓存发挥作用。
hashing + block management + memory pools + eviction policies + prefix matching + scheduler integration + tenant isolation + cache salting + metrics + observability + versioning
所以系统通过增加复杂性来节省计算。
这种权衡通常是值得的。
但它引出了一条更广泛的原则:
缓存变得越有价值,缓存出错就越危险。
一个无人关心的缓存可以很笨。
一个节省了数百万美元的缓存就成了基础设施。
而基础设施需要不变量。
传统的分布式系统一直在与一致性作斗争。
Source of truth | +--> Cache A | +--> Cache B | +--> Replica | +--> Search index
现在 AI 应用通常还要加上:
Source | +--> Retrieval index | +--> Embedding store | +--> Prompt cache | +--> KV cache | +--> Tool state | +--> Agent memory
一个现代 AI 智能体可能实际上在多层记忆状态上运作。
这是一个重要的概念转变。
AI 系统不再仅仅是:
prompt -> model -> answer
它越来越像:
+-------------------+
| Source Data |
+---------+---------+
|
+------------+------------+
| | |
v v v
Retrieval Context Agent
Cache Cache Memory
| | |
+------------+------------+
|
v
Model
|
v
KV Cache
|
v
Output
当源数据发生变化时会发生什么?
这个问题不再是学术性的了。
"哪些事实发生了变化,哪些派生产物依赖于这些事实?"
Document v17 | +--> embedding v17 | +--> retrieval index v17 | +--> prompt context v17 | +--> generated summary v17
当文档变成 v18 时,你实际上面对的不是"缓存删除"问题。
而是一个依赖图问题。
document v17 X | +---- invalid | +--> embedding v17 +--> retrieval result +--> summary
document v18 | +--> new embedding +--> new retrieval result +--> new summary
这就是为什么复杂的系统越来越多地使用版本控制、内容哈希、不可变产物、时间戳或显式缓存命名空间,而不是试图手动追踪每一个过期对象。
这是系统工程中最奇怪也最有用的教训之一。
你可以尝试精确判断一个缓存条目何时过期。
或者你可以设计一个系统,使过期自然地被约束。
一个常见策略是 TTL:
cache entry | +--> expires after 5 minutes
另一个是版本化键:
document:123:v17 document:123:v18
另一个是内容寻址:
hash(contents) -> artifact
还有不可变数据。
还有写穿透或写回。
还有一个就是简单地对太难安全失效的数据拒绝缓存。
缓存不是宗教。
是最好的设计决策。
现代推理框架背后的工程是整篇文章的一个完美例证。
vLLM 的 PagedAttention 将 KV 缓存更多地视为内存页面,而不是一个巨大的连续数组。其后续的前缀缓存系统通过哈希 KV 缓存块来扩展这一思路,使相同的前缀能被识别和重用。
这是一个美丽的演进:
Virtual memory ↓ PagedAttention ↓ KV block management ↓ Prefix caching ↓ Cross-request reuse
AI 革命一直在产生看起来非常像经典操作系统研究的系统。
因为在某种程度上,GPU 仍然是一台机器,具有:
finite memory finite bandwidth finite time finite buses finite queues
再多的 AI 炒作也改变不了这一点。
在 AI 工程中存在一种将优化视为竞赛的倾向:
better model better GPU more FLOPs longer context
但在底下还有另一场竞赛在进行:
better memory management better scheduling better batching better cache locality better reuse better data movement
一些最大的收益不是来自让模型在数学上更聪明。
而是来自不做重复的工作。
在规模上,"不重复计算同样的东西"是一种经济策略。
这是哲学的部分。
缓存不是真理。
缓存是真理的副本。
而副本制造了分歧。
AI 有趣的地方在于,现代模型已经用表示而非原始世界来工作了。
retrieved data + summaries + embeddings + prompt caches + KV caches + agent memory
现在我们在表示之上构建了一层又一层的表示。
每一层都可能过期。
每一层都需要来源追踪。
每一层都需要失效规则。
你离真相的源头越远,就越难知道你手里的东西是否仍然可信。
这正是缓存失效长期以来一直很困难的原因。
缓存失效和命名。
2026 年的版本可能是:
缓存失效、命名,以及说服一个 400 行的 AI 智能体,它昨天缓存的东西已经不再正确了。
这个笑话还在继续。