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 的缓存策略下能拿到多少命中率」。这个数字直接影响你的有效成本。

好,今天就聊到这。

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个