Appearance
缓存是什么、如何控制
为什么 Agent 离不开缓存
LLM API 是无状态的:每一轮请求都要把完整的对话历史重新发送、重新计算一遍。编程 Agent 一个任务动辄上百轮工具调用,意味着开头那几万 token 的系统提示、工具定义、文件内容要被重复处理上百次。
Prompt Caching(提示缓存) 解决的就是这个问题:服务端把已经算过的前缀的内部状态(KV cache)存下来,下一轮请求如果前缀完全一致,就直接复用,不必重算。
效果(以 Anthropic API 为例):
| 类型 | 价格(相对基础输入价) | 延迟 |
|---|---|---|
| 普通输入 token | 1× | 正常 |
| 缓存写入(首次) | 1.25×(5 分钟 TTL)/ 2×(1 小时 TTL) | 正常 |
| 缓存读取(命中) | 0.1×,一折 | 大幅降低首 token 延迟 |
一个长会话里 90% 以上的输入 token 都可以是缓存命中——这正是 Claude Code、Codex CLI 能以可接受的成本运行长任务的基础。OpenAI API 也有对应机制(automatic prompt caching,前缀 ≥1024 token 自动生效,命中打五折/部分模型更低),原理相同。
缓存的关键性质:前缀匹配
缓存只匹配完全一致的前缀。请求内容按固定顺序参与哈希:
text
tools(工具定义)→ system(系统提示)→ messages(对话历史)
────────────────────────────────────────────▶ 位置越靠前,越应该稳定规则只有一条,但推论很多:
- 前缀哪怕变了一个字节,从那个位置往后的缓存全部失效,需要全额重建;
tools排在最前——中途增删任何一个工具定义,整个缓存作废(这也是工具爆炸的隐性成本之一);- 对话是"追加式"的就天然缓存友好:旧消息不动,只在末尾加新内容,每轮只有增量需要计算。
缓存的控制:cache_control 断点
Anthropic API 的缓存是显式控制的:在内容块上标 cache_control,告诉服务端"缓存到这里为止的所有前缀":
python
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是代码审查助手。以下是项目编码规范:\n" + long_style_guide,
"cache_control": {"type": "ephemeral"}, # ← 断点:缓存 tools + 到此为止的 system
}
],
messages=[{"role": "user", "content": "审查这个 PR ..."}],
)要点:
- 一个请求最多可放 4 个断点,可分别标在工具定义末尾、系统提示末尾、对话历史末尾等层级上;
- 默认 TTL 为 5 分钟,每次命中会刷新;可用
"cache_control": {"type": "ephemeral", "ttl": "1h"}换 1 小时 TTL(写入价更高,适合低频但重复的场景); - 有最小长度门槛(多数模型 1024 token,Haiku 类为 2048/4096),太短的前缀不会被缓存;
- 响应里的
usage字段可以验证效果:cache_creation_input_tokens(本轮写入)、cache_read_input_tokens(本轮命中)。接了新系统后先看这两个数,命中为 0 说明前缀被你弄变了。
在 Claude Code / Codex 里需要手动控制吗?
不需要。Claude Code、Codex CLI 这类官方 Agent 已经自动管理缓存断点。你要做的不是"开启缓存",而是不要破坏它——这正是下一篇避免反复重建缓存的主题。
一个算术题:缓存能省多少
假设系统提示 + 工具定义 + CLAUDE.md 共 30K token,任务跑了 50 轮:
- 无缓存:30K × 50 轮 = 150 万 token 全价输入;
- 有缓存:首轮 30K × 1.25(写入)+ 后续 49 轮 × 30K × 0.1(命中)≈ 18.4 万 token 等效费用,约为前者的 12%,且每轮首 token 延迟显著下降。
上下文越大(尤其 1M 模式),这笔账越悬殊——1M 前缀重建一次的成本,足够缓存命中跑几十轮。
缓存不是万能的
- 缓存只降输入成本,输出 token 照常计费;
- TTL 过期(默认 5 分钟无访问)后需要重新写入——长时间挂起的会话回来第一轮总是慢一些、贵一些;
- 缓存按账号隔离,不同 API key/组织之间不共享;
- 它不能替代上下文管理:窗口塞满导致的质量下降,缓存救不了。