先把最容易被误解的一点说清楚: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,未必抵得过丢掉的缓存价值。
几个容易漏掉的“隐藏”失效源
- tool 顺序变了,哪怕语义完全一样也分叉。 MCP 返回的 tools 从
A, B, C变成B, A, C,prefix 就变了。OpenAI 把 tool ordering 明确列为影响 cache reuse 的因素。 - reasoning / thinking 参数会被 render 进 prompt。 这个不能当成单纯 sampling 参数。Anthropic 明确说
thinking、budget_tokens、effort会 render 到 prompt,可能使 message cache 失效,有些模型甚至让 tools/system cache 一起失效。tool_choice、web search 开关、citations、speed fast/standard 也会 invalidate system 或 messages。API 参数虽然不是显式发送的文本,但 provider 可能把它转成 hidden prompt 纳入 cache key,照样影响 KV。 - 多模态。 OpenAI 允许 image/audio 进可缓存 prefix,但要求内容、顺序、detail setting 一致。图片变了、增删、换顺序、
detail low → high都会在图片位置以后分叉。screenshot每轮重新截图,是 coding / computer Agent 特别容易 miss 的地方。vLLM 甚至把 multimodal input hash 加进 KV block hash,避免两个不同图片碰巧用相同 placeholder tokens 时错误共享。 - cache granularity:prompt 一样也可能“看起来失效”。 vLLM 只缓存完整 block(默认
block_size = 16),公共 prefix 有 103 tokens 时可能只命中 96,剩下 7 个重算。SGLang 某些 backendpage_size=64更明显:32-token 的 prompt 连一页都填不满,没有 prefix cache;65 tokens 也只能复用前 64。OpenAI 也有自己的粒度,当前 GPT-5.6+ 要求 cacheable prefix 至少 1024 tokens。 prompt_cache_key不会让两个不同 prompt 共用 KV。 它(以及 vLLM 的cache_salt)主要作用是路由到有对应 cache 的机器、或故意隔离 tenant,prefix 仍然必须真正匹配。
什么一般不会破坏 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 上使劲。
参考: