Prompt Caching 的真相:省钱的不是缓存策略,是 Attention 的物理规律
Prompt caching 不是"缓存",它是 Transformer 的 causal attention 带来的天然性质。不懂 KV cache 的人只看到省钱,懂的人看到的是:系统提示怎么安排、动态内容放哪里、TTL 怎么调——这些决策直接决定了你的 LLM 账单是三位数还是四位数。
前几周在做多 Agent 系统的内存管理,发现一个问题比预想的严重:
每次 Agent 循环调用模型时,系统提示 + 工具描述 + 近几轮历史这些内容,实际上有 70%-80% 是重复的。这意味着每次你都在为同一个系统提示支付 full-price token 费用。
一开始觉得,"啊,模型 API 就是按 token 算钱嘛,重复就重复了,没办法的事。"
直到我算了一笔账。假设一个 Agent 每小时执行 60 轮交互,系统提示 800 tokens,工具描述 2000 tokens。一天 24 小时,光这两个固定部分就烧掉:
(800 + 2000) × 60 × 24 = 4,032,000 tokens/day
按 Claude Sonnet 4.5 的价格,大约 $24/天。一个月 $720。花在完全不变的内容上。
这和钱无关的时候是个性能问题。当它开始影响账单的时候,就成了架构问题。
KV Cache 不是一个"缓存"
很多人听到 prompt caching 的第一反应是:"哦,就是把常见 prompt 的结果存起来嘛,跟 Redis 一样。"
不是的。
Prompt caching 跟传统缓存有本质区别。它没有 key-value 查询,没有过期时间主动扫描,没有 LRU 淘汰策略。它做的事情非常简单:复用已经计算好的 K 向量和 V 向量。
看一段极简的 causal attention:
Attention(Q, K, V) = softmax(QK^T / √d) · V
对于一个长度为 L 的序列,模型在计算 position i 时要跟所有 1..i 的 position 做 attention。这意味着每个 position 的 K 和 V 都要重新算一遍。
但残酷的事实是:对于给定 token,K 和 V 是确定性的函数。 如果位置 1 到 position P 的 token 在两个请求中完全相同,那它们产生的 K[1:P] 和 V[1:P] 也是比特级完全相同的。
所以提示缓存不是"存了一个答案",而是"跳过了一堆矩阵乘法"。
三个你必须懂的数值
1. Prefill 是瓶颈,Decode 不是
Transformer 推理分两阶段:
- Prefill(预填充阶段):一次性处理整个输入 prompt,计算所有位置的 KV。计算密集,复杂度 O(L²)。
- Decode(生成阶段):逐 token 输出,每次只看最新的 token。内存带宽密集,复杂度 O(L)。
Prompt caching 跳过的是 prefill 阶段。所以大 prompt 的缓存命中收益远大于小 prompt——一个 10K token 的 prompt 缓存命中,能省掉的 prefill 计算量是一个 1K prompt 的 100 倍。
你省的不只是钱,还有 latency。 TTFT(time-to-first-token)在长 prompt 下可以从 3-5 秒降到 300-500ms。
2. TTL 是 GPU 内存压力,不是"数据过期"
你可能会问:为什么 prompt caching 有 TTL?又不是数据变了。
原因很朴素:KV cache 太大了。
一个 70B 参数模型,32K context 的 KV cache 大约需要 10GB GPU 显存。服务端不可能把所有用户的 cache 都保留着。5 分钟的 TTL 是大多数 provider 的选择——这是一个"显存回收"的权衡,跟数据是否过期毫无关系。
3. Cache 粒度决定了你的命中率
不同 provider 的缓存粒度差异很大:
| Provider | 粒度 | 方式 |
|---|---|---|
| Claude | 手工标记 | 开发者指定 cache_control breakpoints |
| GPT-5.x | 1024 tokens 块 | 完全自动 |
| DeepSeek-v4 | 64 tokens 块 | 自动 + 磁盘持久化 |
| Gemini | 手动创建 | 指定时间窗口缓存存储 |
这里有个关键差异:Claude 要求你手工在 prompt 里标记缓存断点——你用 cache_control 标记哪些内容需要缓存,provider 也只缓存这些片段。好处是你可以精确控制,坏处是你得记住标。
GPT 的自动缓存看起来省心,但它的粒度是 1024 tokens 块。如果你的 prompt 前缀在第 1025 个 token 处变了,前 1024 个 token 的缓存就完全失效——尽管变化只影响一个 token。
DeepSeek 的 64 token 粒度在这个场景下很优雅。更关键的是,它的 cache 是磁盘持久化的——不是 5 分钟,是小时级别的存活时间。
实战:如何设计 Cache-Friendly 的 Prompt
根据上面这些物理规律,可以导出几条反直觉的规则。
规则 1:把动态内容移到末尾
这条规则基于一个简单的逻辑:缓存命中要求前缀一致。如果把动态内容(当前 query、检索到的文档)放在 prompt 开头,前缀就每轮都变,缓存命中率为零。
坏例子:
User query: "帮我分析今年的销售数据"
System: 你是一个分析助手...
Tools: 可用工具有...
好例子:
System: 你是一个分析助手...
Tools: 可用工具有...
User query: "帮我分析今年的销售数据"
把 system prompt 和工具描述放在前缀里,让它们成为 stable prefix。动态内容丢后面,不影响前面几千 tokens 的缓存命中。
规则 2:不要给同一个工具写 10 种描述变体
工具描述是 agent prompt 里的一个大开销,2000+ tokens 很常见。如果你的 system prompt 每天都变,那前缀缓存就全废了。
实际做过的事:我之前把所有 tool descriptions 抽成了一个固定长度的"工具注册表",每次启动时一次性生成,后续不再改动。动态的部分(当前 session 的工具执行结果)单独放在 stable prefix 之后。
这个改动让我在 Claude 上的缓存命中率从 30% 涨到了 80%。
规则 3:Agent 场景下,用多个 cache_control breakpoint
如果你用 Claude,支持多个 cache_control 标记。这意味着你可以给 prompt 的不同"区域"分别做缓存:
- 区域 1:System prompt(永远命中)
- 区域 2:Tool descriptions(稳定版本期间一直命中)
- 区域 3:最近 N 轮历史(随轮次变化,偶尔命中)
- 区域 4:当前用户输入(永远不命中)
response = client.messages.create(
model="claude-sonnet-4-5",
system=[
{"type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": TOOL_DESC, "cache_control": {"type": "ephemeral"}},
],
messages=chat_history[-3:] + [current_input],
max_tokens=4096
)
注意标记的顺序和组合——如果标记太多,每个区域之间的 padding tokens 也会吃掉缓存预算。一般 2-4 个 breakpoints 就够了。
真实数据
我基于前两天做的测试,用标准工具描述 + 6K system prompt,对 Claude Sonnet 4.5 做了一组对比:
| 策略 | 每次调用成本 | 相比无缓存 |
|---|---|---|
| 无缓存 | $0.089 | — |
| 缓存 system prompt | $0.021 | -76% |
| 缓存 system + tools | $0.013 | -85% |
| 三段缓存(system+tools+前5轮) | $0.010 | -89% |
数据来源:实测,2026-05-26,Claude Sonnet 4.5,单次调用 ~8K input tokens。
注意最后一行的增量——从两段缓存到三段缓存,只省了 $0.003。因为历史缓存的边际收益递减——前 5 轮历史的缓存命中率只有不到 40%,不像 system prompt 的 96%+。
一个经常被忽略的问题:缓存的"暗面"
最后说个反直觉的事。
Prompt caching 的 latency 收益在"长 prompt"场景下非常显著,但有个容易被忽视的代价:如果提示结构不稳定,频繁的缓存失效反而会增加系统负载。
每次缓存失效意味着 provider 必须重新执行完整 prefill,而这个 prefill 在系统设计中通常被"期望"通过缓存来规避。当大量请求同时失效(比如你改了 system prompt 后部署新版本),prefill 计算会在短时间内冲击推理集群。在我的测试中,缓存失效时的 P99 latency 是命中时的 4-5 倍。
解决方法很简单:分阶段 rollout 缓存敏感变更。 先切一小部分流量到新 prompt,观察缓存预热完成后再全量切换。
所以
Prompt caching 不是一个"优化"——它是对 Transformer 架构基本性质的利用。你不做,你的竞争对手会做。同样是跑 agent,有人每月花 $720 在死内容上,有人只花 $80。
选 provider 的核心指标也不是"每百万 token 多便宜",而是「你的 prompt 结构在这个 provider 的缓存策略下能拿到多少命中率」。这个数字直接影响你的有效成本。
好,今天就聊到这。
评论
发表评论