LLM KV cache 什么时候失效

先把最容易被误解的一点说清楚:KV cache “失效”,大多数时候不是整块缓存清零,而是从某个位置开始不能再复用。

Decoder Transformer 在位置 i 的 K/V,本质上依赖三样东西:模型,位置,还有从第 0 个 token 到第 i 个 token 的完整前缀。所以规律其实只有一句——

找到两次请求第一个不同的 token,从那个 token 开始,后面的旧 KV 基本都不能复用,前面的公共 prefix 仍然可以复用。

OpenAI、Anthropic、vLLM 的 prefix caching 都是这条规律的实现:OpenAI 要求 exact prefix match,vLLM 按 prefix 加 block 内的 tokens 哈希 KV block。

举两个对比的例子。第一个:

Request A:
[SYSTEM 5000 tokens]
[user: hello]
[assistant: ...]
[user: A]

Request B:
[SYSTEM 5000 tokens]
[user: hello]
[assistant: ...]
[user: B]

前面那 5000+ tokens 都能接着复用,只需要重新 prefill 最后不同的部分。但如果改成这样:

Request A:
[current_time=12:01]
[SYSTEM 5000 tokens]
...

Request B:
[current_time=12:02]
[SYSTEM 5000 tokens]
...

第一个 token 附近就不同了,后面那 5000 tokens 的巨大缓存价值也一起没了。很多 Agent 框架“明明在缓存却永远命中不了”,根源往往就是这个:动态内容被放到了最前面。

把情况归成四类

我把能想到和查到的都整理了一遍,发现其实就四类:

1. Token prefix 变了
   → system / history / RAG / memory / tools / image / schema / dynamic state

2. 计算 KV 的模型变了
   → model / fine-tune / LoRA / model revision

3. Cache identity / routing 变了
   → cache key / tenant / salt / worker / region

4. Cache 本身不存在了
   → TTL / eviction / restart / autoscaling / memory pressure

对 Agent 架构设计而言,最值得盯的是第一类。后三类要么是部署运维问题(第四类),要么是换模型这种本来就知道会失效的(第二类)。真正隐蔽、容易在代码里顺手埋下的坑,基本都落在第一类。

所以后面重点说第一类。我按对命中率的影响,把 Agent 里最容易踩的五个排成这样:

动态 tool list → 动态 system prompt → RAG/Memory 插得太靠前 → 重写 history → compaction。

动态 tool list

很多 Agent 会每轮重新算一遍可用工具:

Turn 1:  read, write, bash, git
Turn 2:  read, write, bash
Turn 3:  read, write, bash, git, browser

逻辑上很漂亮,但 KV cache locality 非常差。tools 动辄几千甚至几万 tokens,每轮一变,后面整段 history 都受影响。

这些操作基本都会让缓存分叉(❌ = 失效):

Tool 操作 Cache
增加 / 删除 tool
tool 排序变化
name / description 改变
JSON Schema 改变
parameter / enum / required 改变
MCP 每轮返回不同 tool 集合

OpenAI 把 tool definitions、descriptions、parameter schemas 和 tool ordering 都列为 cacheable prefix,建议保持完全一致。这也是我觉得 Agent framework 很容易犯的一个设计错误——每轮 calculateAvailableTools(currentState) 看起来优雅,实际上等于持续打自己的缓存。更好的是 tools 保持固定一套,另用 allowed_tools = read, write, bash 来控制,OpenAI 现在也推荐这么干。

动态 system prompt

很多 framework 会每轮偷偷塞动态信息。date、time、精确到秒的 timestamp、UUID / request id、trace id、session id、cwd、git branch / SHA、当前打开文件、OS、container id……如果它们出现在 prompt 最前面:

BAD

[current time]
[current cwd]
[random request id]

[stable 30K agent system prompt]
[stable tools]
[user request]

第一个 token 附近就变了,后面 30K 的稳定内容全部作废。

OpenAI 文档专门拿 timestamp 举过例:如果放在稳定 prompt 与 breakpoint 之间,每次都会产生新的 prefix。它建议把 request id、timestamp 这类动态内容放到 reusable prefix 后面,纯日志信息则放 metadata。所以动态信息要么不放,要么放最后:

GOOD

[system prompt]
[policy]
[tools]
------ CACHE POINT ------
[current time]
[dynamic environment]
[user request]

RAG / Memory 插得太靠前

memory 和 RAG 每轮检索结果都不同。问题在它们被放在哪。如果 memory 放 system prompt 很前面:

SYSTEM
MEMORY        ← 每轮在动
AGENT.md
TOOLS         ← 巨大
HISTORY       ← 巨大

一次记忆变化,后面全 miss。RAG 也一样,检索文档变了、top-k 变了、排序变了、chunk boundary 变了,都从第一个文档开始分叉。

但注意:tool call 本身不是缓存杀手。每次请求只是往尾部 append tool result:

SYSTEM             ← reuse
TOOLS              ← reuse
USER               ← reuse
ASSISTANT tool_call ← reuse
USER tool_result   ← new

这对 KV cache 很友好。真正的问题是每轮重新拼整段 prompt,或者重写之前的 history。

重写 conversation history

这个最容易理解,也最容易犯。只要修改、删除、重排旧 message,从第一个变化处开始,后面全废。触发点包括:改一个字或标点(token 序列变了)、改换行空格(tokenizer 输出不同)、改 role(user → system)、删除/插入/修改历史、regenerate 某个 assistant answer、conversation fork(fork 点以前共享,之后各走各的)、few-shot examples 改变或换顺序。

OpenAI 建议多轮 Agent append message,而不要修改/删除/重排旧 history。所以多轮最理想是只追加:

STATIC PREFIX
U1
A1
U2
A2
U3
A3

       append only

还有个隐蔽的坑:tool result 一旦变成历史,就别再改它。改成 {"foo":1,"latency":183} 之后,从它往后都不能复用旧 KV。observability 特别容易犯:result + duration + timestamp + trace id + random metadata,重放时一变,历史就不稳。

compaction / summarization

长对话一开始很漂亮:

S U1 A1 U2 A2 ... U50 A50
██████████████████████████   ← 全部可复用

context 太长后做 summarization:

S
SUMMARY(U1...A40)
U41
A41
...

于是:

S            ← 仍可能复用
SUMMARY       ← new
后面所有东西    ← 相对旧 prefix 全变了

compaction 本质上是一个 trade-off:省 context tokens,换丢 accumulated KV cache。OpenAI 官方也明确提醒 truncation / summarization / compaction 虽然减少 token 数,但可能 reset 可复用的 prefix。长生命周期 Agent 里这个账得自己算——省下的话术 tokens,未必抵得过丢掉的缓存价值。

几个容易漏掉的“隐藏”失效源

什么一般不会破坏 cache

temperature、top_p、top_k、max_output_tokens、stop 这些纯 decode 参数,从 Transformer 数学上讲只作用于后面的 token selection,不改变已经 prefill 完的 prompt K/V。所以同样 prompt、不同 temperature,理论上 prompt KV 可以相同。

但有个限定:如果 provider 把某个参数转成 hidden prompt / configuration 并纳入 cache key,那就可能失效——Anthropic 的 thinking / effort / speed 就是现实例子。另外,temperature 虽然不破坏当前 prefix,但会生成不同的 assistant tokens,下一轮再把这些输出塞回 conversation,两条会话就自然 fork 了。

风险从高到低,我自己的看法

最危险:
dynamic data

SYSTEM

TOOLS

MEMORY

HISTORY

比较危险:
SYSTEM
dynamic tool definitions

MEMORY

HISTORY

还可以:
SYSTEM
TOOLS
dynamic memory

HISTORY

理想的情况下,把会动的东西都放到稳定内容之后:

┌─────────────────────────────┐
│ Stable system instructions  │
│ AGENTS.md / policy          │
│ Few-shot                    │
│ Stable tool definitions     │
│ Stable shared context       │
├────── CACHE BOUNDARY ───────┤
│ Dynamic memory              │
│ Dynamic RAG                 │
│ Current environment         │
│ Timestamp                   │
│ Current user message        │
└─────────────────────────────┘

说穿了就一句话,也是 OpenAI 官方现在的最佳实践:static content first, variable content last。动态内容越靠后,前面那棵大树越稳定,命中率越好看。

如果你在设计 Codex / Claude Code 那种长生命周期 coding agent,我会建议把 KV-cache friendliness 当成 context engineering 的一等架构约束,而不是 inference 层的小优化——50K~200K 的 context 里,公共 prefix 稳不稳,TTFT 和输入成本的差距是很悬殊的。OpenAI、Anthropic、vLLM/SGLang 现在的设计方向也都在往 prefix reuse 上使劲。


参考: