Prompt Caching 是 Agent 的地基
flowchart LR
A["Agent 长任务"] --> B["大上下文"]
B --> C["高延迟 / 高成本"]
C --> D["Prompt Caching"]
D --> E["稳定前缀"]
D --> F["工具不乱变"]
D --> G["缓存安全的分叉"]
做长时间运行的 Agent,很多人第一反应是研究模型能力、工具调用、规划算法。但 Claude Code 的经验更朴素,也更扎心:如果 prompt cache 命中率不稳,Agent 还没来得及变聪明,就先被成本和延迟拖垮了。
出处与延伸阅读:
- 原文:Lessons from building Claude Code: Prompt caching is everything(Claude Blog,2026-04-30)
- 本文基于用户提供的本地译文《Claude Code 构建经验:Prompt Caching 就是一切》整理和改写。
- 相关文档:How Claude Code uses prompt caching
这篇文章会讲什么
如果你只是在做一次性问答,prompt caching 可能只是一个成本优化选项。
但如果你在做 Claude Code 这类 coding agent,情况完全不同。Agent 每一轮都要带上 system prompt、工具定义、项目上下文、历史消息、工具结果、当前请求。对话越长,请求越重。没有缓存,长任务会越来越慢,也越来越贵。
这篇文章不讨论 prompt caching 的 API 细节,而是把它放回 Agent 产品设计里看:
- 为什么缓存会影响功能设计
- prompt 应该按什么顺序组织
- 为什么不要在会话中途切模型、切工具
- Plan Mode、Tool Search、Compaction 为什么都要围绕缓存重做
- 一个团队应该怎样把 cache hit rate 当成生产指标
先说结论
Claude Code 的这组经验里,最值得记住的不是某个技巧,而是一条工程原则:
缓存不是优化项,而是架构约束。
一旦你接受这一点,很多看起来“不够直觉”的设计就变得合理了。
比如:
- 不要把动态时间戳塞进 system prompt
- 不要因为进入 plan mode 就换一套只读工具
- 不要为了省一点模型单价,在长会话中途切到另一个模型
- 不要在 compaction 时另起一个完全不同的 summarizer prompt
- 不要只在账单异常时才看 cache hit rate
这些建议表面上都和“缓存”有关,底层其实是在说同一件事:
Agent 的上下文必须稳定增长,而不是反复重写。
Prompt Caching 的关键不是缓存,而是前缀
先看定义:这里的 prompt caching,本质上是前缀匹配。请求开头到 cache breakpoint 之间的内容,如果和之前请求完全一致,就可以复用已经计算过的部分。
这意味着顺序非常重要。
一个适合缓存的 Agent 请求,通常应该长这样:
稳定 system prompt
↓
稳定工具定义
↓
项目级上下文,比如 CLAUDE.md
↓
会话级上下文
↓
不断追加的对话消息和工具结果
把静态内容放前面,动态内容放后面,这是最基本的规则。
但真正难的地方在于:很多东西看起来“只是很小的变化”,实际上会把后面的缓存全部打断。
比如:
| 变化 | 看起来 | 实际后果 |
|---|---|---|
| system prompt 里加入精确时间 | 很合理,模型需要知道现在几点 | 每轮时间变了,前缀就变了 |
| 工具定义顺序不稳定 | JSON 数组顺序似乎无所谓 | 前缀字节级不同,缓存断裂 |
| 动态修改工具参数 | 当前任务只开放几个 agent | 工具 schema 变了,整段重算 |
| 中途改 CLAUDE.md | 项目规则更新了 | 项目级前缀可能失效 |
缓存系统不关心“语义上是不是差不多”。它只关心前缀是否匹配。
所以做 Agent harness 时,不能只把 prompt 当成一段提示词看,而要把它当成一份会影响成本、延迟和产品能力的工程结构。
动态信息尽量走消息,不要改 system prompt
Agent 运行过程中,确实有很多信息会变。
比如:
- 当前日期变化了
- 用户修改了某个文件
- 当前进入了 plan mode
- 某个工具结果需要提醒模型注意
- 工作区状态发生了变化
最直觉的做法,是把这些信息写进 system prompt。问题是,只要 system prompt 变了,稳定前缀就不稳定了。
Claude Code 的做法更像“追加事件日志”:把变化通过下一轮用户消息或工具结果传进去,例如用类似 <system-reminder> 的结构提醒模型。
这背后的取舍很清楚:
- system prompt 负责长期规则
- message 负责短期状态
- 工具结果负责事实回传
不要把三者混在一起。
一个可复用的判断方式是:
如果这条信息只对当前几轮有用,就不要改 prompt 前缀。
不要在会话中途切模型
这条经验有点反直觉。
假设你已经和 Opus 跑了一个 100k token 的长会话,现在用户问了一个很简单的问题。直觉上,切到便宜模型回答是不是更省?
很多时候不是。
因为 prompt cache 是按模型隔离的。你切到另一个模型,就要为它重新建立整段上下文缓存。模型单价省下来的钱,可能还不够覆盖这次 cache miss。
更稳的做法是:主会话不要乱切模型。如果确实需要便宜模型处理局部任务,可以让主模型生成一条清楚的交接消息,再交给 subagent 做。
这和人类团队很像:
- 主 agent 保持完整上下文
- 子 agent 只拿必要任务和边界
- 子 agent 回传结果,而不是接管主线记忆
这样做的收益不只是省钱,也能避免主会话心智模型被切碎。
不要在会话中途增删工具
工具集合也是缓存前缀的一部分。
这件事会直接影响功能设计。
比如 plan mode。最自然的实现方式可能是:普通模式给模型读写工具,进入 plan mode 后只给只读工具。听起来很干净,但缓存会被打断,因为工具定义变了。
Claude Code 的设计更绕一点,却更稳:
- 始终保留完整工具集合
- 把
EnterPlanMode和ExitPlanMode也设计成工具 - 进入 plan mode 时,通过消息告诉模型当前规则:探索代码、不要编辑、完成计划后退出
这样工具定义不变,状态通过消息变化,缓存前缀仍然稳定。
这个设计也有一个额外好处:模型可以在发现任务复杂时,自己选择进入 plan mode。这比外部 UI 强行切模式更自然。
同样的思路也适用于 tool search。
如果 Agent 能接几十个 MCP 工具,每次都塞完整 schema 很贵;但中途删工具又破坏缓存。折中办法是保留轻量 stub,让模型需要时再发现完整工具定义。
所以这里真正的原则是:
工具可以延迟披露,但不要随机消失。
Compaction 也要缓存安全
长会话一定会遇到上下文窗口不够的问题。Compaction 的基本做法是:把前面的对话压缩成摘要,然后继续新会话。
这听起来简单,但很容易写出昂贵实现。
最糟糕的做法是另起一个 summarizer 请求,使用完全不同的 system prompt,也不带原来的工具定义,只把历史消息扔进去让它总结。
这样做的问题是:这个 summarizer 请求和主会话前缀完全不一样,缓存几乎帮不上忙。你会为整段历史重新付费,而这段历史往往正是最贵的部分。
更好的方式是 cache-safe forking:
沿用父会话的 system prompt
沿用父会话的工具定义
沿用父会话的上下文
放入父会话历史
在末尾追加 compaction 指令
从缓存视角看,这次请求就像父会话的自然延续。真正新增的,只是最后那条 compaction 指令。
这件事给所有 Agent 系统一个提醒:任何“旁路任务”都不应该随便另开一套上下文。
包括:
- 摘要
- 审查
- 子任务执行
- skill 执行
- 长任务中途分叉
只要它需要读取父会话的大量历史,就应该尽量共享父前缀。
Cache Hit Rate 应该像可用性一样被监控
很多团队会监控延迟、错误率、token 成本,但很少把 prompt cache hit rate 当成一等指标。
Claude Code 的经验是:这件事值得按事故处理。
原因很简单。Agent 的成本结构和传统 API 不一样。一次 cache miss 可能不是多花几百 token,而是让几十万 token 的上下文重新计算。命中率下降几个百分点,最后可能体现在:
- 响应变慢
- 用户额度消耗异常
- 订阅套餐成本失控
- 长任务更容易被打断
- 团队开始被迫收紧 rate limit
这不是纯后端优化,而是产品体验问题。
一个比较实用的监控表可以这样设计:
| 指标 | 关注点 |
|---|---|
| cache hit rate | 是否有系统性缓存断裂 |
| uncached input tokens | 真实新增成本 |
| per-turn latency | 用户是否感到变慢 |
| model switch rate | 是否有不必要的模型切换 |
| tool schema churn | 工具定义是否频繁变化 |
| compaction cost | 摘要是否吃掉大量预算 |
如果这些指标不透明,Agent 团队很容易在功能迭代里不小心把缓存打碎,然后只看到“最近怎么贵了很多”。
对普通 Agent 开发者的启发
你不一定在做 Claude Code 这么复杂的产品,但这些经验依然有用。
如果你在做客服 Agent、数据分析 Agent、代码 Agent、运营 Agent,都可以从下面几条开始:
- 把 prompt 分成稳定前缀和动态消息。
- 工具定义固定排序,避免非确定性生成。
- 状态切换优先用消息和工具建模,不要频繁改 system prompt。
- 不要在长会话中随意切模型。
- 工具太多时做延迟披露,而不是中途删工具。
- compaction、总结、审查这些旁路任务尽量共享父会话前缀。
- 监控 cache hit rate,并把异常和具体 release 关联起来。
这些事情看起来都很底层,但它们会决定一个 Agent 能不能从 demo 走到生产。
小结
Prompt caching 最容易被误解成“降成本技巧”。
更准确地说,它是长任务 Agent 的运行地基。
一个 Agent 如果需要持续读项目、调用工具、执行多轮任务、做 compaction、派发 subagent,它就不能每一轮都像第一次请求一样重新理解世界。
所以真正的工程问题不是“能不能缓存”,而是:
你的系统设计,是否允许缓存持续命中?
能做到这一点,Agent 才有机会又快、又稳、又便宜。