Claude Code 构建经验:为什么 Prompt Caching 是 Agent 的地基

基于 Claude Code 团队关于 prompt caching 的工程经验,拆解长任务 Agent 为什么必须围绕缓存来设计:静态前缀、工具集合、模型切换、compaction 和缓存命中率监控。

8 min read Part of Claude Code · Ch. 1

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 还没来得及变聪明,就先被成本和延迟拖垮了。


出处与延伸阅读


这篇文章会讲什么

如果你只是在做一次性问答,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 的设计更绕一点,却更稳:

  • 始终保留完整工具集合
  • EnterPlanModeExitPlanMode 也设计成工具
  • 进入 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,都可以从下面几条开始:

  1. 把 prompt 分成稳定前缀和动态消息。
  2. 工具定义固定排序,避免非确定性生成。
  3. 状态切换优先用消息和工具建模,不要频繁改 system prompt。
  4. 不要在长会话中随意切模型。
  5. 工具太多时做延迟披露,而不是中途删工具。
  6. compaction、总结、审查这些旁路任务尽量共享父会话前缀。
  7. 监控 cache hit rate,并把异常和具体 release 关联起来。

这些事情看起来都很底层,但它们会决定一个 Agent 能不能从 demo 走到生产。


小结

Prompt caching 最容易被误解成“降成本技巧”。

更准确地说,它是长任务 Agent 的运行地基。

一个 Agent 如果需要持续读项目、调用工具、执行多轮任务、做 compaction、派发 subagent,它就不能每一轮都像第一次请求一样重新理解世界。

所以真正的工程问题不是“能不能缓存”,而是:

你的系统设计,是否允许缓存持续命中?

能做到这一点,Agent 才有机会又快、又稳、又便宜。