构建 Claude Code 的经验:Prompt Caching 就是一切
素材源自Anthropic 的博客:Prompt Caching 是构建 Claude Code 的一切,或者可以看这篇https://zhuanlan.zhihu.com/p/2034290221907490796
构建 Claude Code 的经验:Prompt Caching 就是一切
一、为什么 prompt caching 对 Agent 是"一切"
工程界有句老话 “cache rules everything around me”(缓存统治一切),作者认为对 Agent 来说更是如此。
像 Claude Code 这种长时间运行的 agentic 产品(一次会话可能跑几十、上百轮工具调用),之所以在经济上可行,完全建立在 prompt caching 之上。它做了两件事:
- 复用之前请求的计算结果(本质是缓存 KV cache,跳过 transformer 重新算一遍前缀);
- 大幅降低延迟和成本——Anthropic 官方的数据是延迟最多降低 85%、成本最多降低 90%,cache read token 价格是普通 input token 的 10%。
作者透露了一个团队内部的工程文化细节:Claude Code 团队会对 prompt cache 命中率设报警,一旦命中率掉下来,会按 SEV(线上事故)处理。换句话说,他们把"缓存命中率"当成产品 SLA 的一部分。这是因为命中率每掉几个百分点,用户成本和延迟都会有明显的劣化,而订阅制下,成本上去意味着他们必须收紧给用户的速率限制(rate limit)——所以缓存命中率直接决定了产品的"慷慨程度"。
二、prompt 的排版本身就是一种架构
这是全文最核心的一条原则。
prompt caching 的本质是前缀匹配(prefix match)。API 缓存的是"从请求最开头到 cache_control 断点为止"的所有内容,而且是逐字节匹配。这意味着:
前缀中任何一处的改动,都会让其后所有内容的缓存失效。
由此自然推出一个排版策略:静态内容在前,动态内容在后。Claude Code 实际的 prompt 结构是这样分层的(从前到后):
- 静态系统提示词 + 工具定义(全局缓存,所有会话共享)
- CLAUDE.md(项目级缓存,同一项目下的会话共享)
- Session 上下文(单会话内缓存)
- 对话消息(每轮都在增长,不缓存或最后一刻缓存)
为什么这样排?因为越靠前的内容越稳定,改动越少,被复用的频率就越高;越靠后的越"动态",每次都会变。这样能最大化跨 session、跨用户、跨请求的缓存复用率。
作者特别坦率地承认,他们自己也踩过坑,曾经几次无意中破坏了这个排序,例如:
- 在静态系统提示词里塞了一个精确到秒的时间戳(每次请求都不一样,等于把整个前缀的缓存废了);
- 工具定义的顺序不稳定(用了非确定性的遍历,导致同样的工具集每次序列化出来的顺序不一样);
- 改了 Agent 工具能调用哪些子 agent 的参数定义。
这些坑听起来都很小,但每一个都能让整条会话的 cache 直接归零。
三、用"消息"而不是"改 prompt"来更新信息
很多人本能的做法是:信息过期了,就改系统提示词。比如时间变了改时间、用户改了文件就更新文件内容、进入某种模式就加一段说明。
但只要你动了系统提示词,缓存就废了。
Claude Code 的做法是:把这些"更新"塞进下一条 user message 或 tool result 里,用一个 <system-reminder> 标签包起来。这样:
- 系统提示词不变,前缀缓存继续命中;
- 模型仍然能在上下文里看到最新信息;
- 代价只是多了几十个 token 的"新增 user message"。
这个技巧在 agent 开发里非常通用——任何"动态"信息都应该尽量从消息流里流进来,而不是反向去修改系统级 prompt。
四、不要在会话中途切换模型
这一条反直觉得最厉害。
很多人会想:“这个问题简单,从 Opus 切到 Haiku 省钱”。但作者算了一笔账:
假设你已经和 Opus 聊了 10 万 token,现在想用 Haiku 回答一个简单问题——切到 Haiku 反而更贵。
为什么?因为 prompt cache 是按模型隔离的。Opus 的 KV cache 不能给 Haiku 用,所以切过去意味着 Haiku 必须把这 10 万 token 从头重算一遍(全价 cache write),反而比让 Opus 直接读缓存回答更贵。
那确实需要切模型怎么办?作者给的方案是用子 agent(subagent)做"交接":让 Opus 在它自己的会话里准备一份"hand-off 消息",然后单独起一个针对另一个模型的新会话,只把这份压缩好的交接信息传过去。
Claude Code 内部的 Explore agent 就是这么用的——主对话跑 Opus,Explore agent 跑 Haiku,各自维护各自的缓存。
五、永远不要在会话中途增减工具
这是"最常见的破坏缓存的方式"。
直觉做法:模型现在不需要的工具就别给它,免得分心。但工具定义是缓存前缀的一部分,加一个或删一个,整条会话的缓存就全废了。
文章举了两个非常巧妙的设计案例,展示如何"在不动工具集的前提下实现动态行为"。
Plan Mode(规划模式)的设计
直觉做法:进入 Plan Mode 时,把工具集替换成只读工具。代价:缓存炸了。
Claude Code 的做法:工具集永远不变,而是把 EnterPlanMode 和 ExitPlanMode 本身做成两个工具。
当用户切换到 Plan Mode 时:
- 工具列表一个不少;
- 给模型发一条 system message,告诉它"现在处于 Plan Mode,只能探索代码、不能改文件,做完 plan 就调用 ExitPlanMode";
- 工具定义本身完全不变 → 缓存稳如老狗。
附带好处:因为 EnterPlanMode 本身就是个工具,模型可以在检测到难题时自主进入 Plan Mode——而且全程不破坏缓存。这是一个"约束反过来催生更优雅设计"的经典例子。
工具搜索(tool search)+延迟加载(defer_loading)
Claude Code 可能加载了几十个 MCP 工具,每次都把完整 schema 全塞进 prompt 太贵。但中途删除又会破坏缓存。
解决方案:发送轻量的 stub(只有工具名 + defer_loading: true),完整 schema 在模型通过 tool search 选中它时才加载。
这样:
- 每次请求里,这些 stub 都在同一位置、同样顺序,前缀就是稳定的;
- 实际"使用"工具时才付出完整 schema 的成本;
- 你可以通过 Anthropic API 的 tool search tool 直接用这套机制。
六、压缩(Compaction)如何不破坏缓存——“cache-safe forking”
这是技术含量最高的一节。
Compaction 是什么
当上下文窗口快满了,需要把前面的对话总结成一段摘要,然后用摘要替代原始消息,继续后面的会话。
陷阱在哪
朴素做法是另开一个 API 请求,用一个"请总结这段对话"的系统提示词,不带任何工具,把整段对话扔进去让模型总结。听起来很合理对吧?
但 prompt cache 是逐字节前缀匹配。你主对话用的系统提示词是 A、工具集是 T;现在总结调用用的系统提示词是 B、没有工具——从第一个 token 开始两者就分叉了,主对话的缓存一个都用不上。结果:你需要为整段(可能十几万 token)的对话付全价 input token,而且对话越长、越需要压缩,这次调用就越贵。这是个非常隐蔽的成本陷阱。
解决方案:cache-safe forking(缓存安全的分叉)
做 compaction 时,完全复用父对话的所有参数:
- 一样的系统提示词;
- 一样的 user context、system context;
- 一样的工具定义;
- 一样的历史消息;
- 只在末尾追加一条新的 user message:“请总结上面的对话”。
从 API 的角度看,这次请求和父对话最后一次请求几乎完全相同——只是末尾多了几十个 token。前缀缓存照样命中,你只为最后那一小段新内容付费。
代价:你得预留一个"compaction buffer"——上下文窗口里要留出空间放总结指令和总结的输出 token,不能真的撑到 100% 再压缩。
好消息是,这套"cache-safe compaction"已经被 Anthropic 内置到 API 里了,普通开发者不用自己重新发明这套机制。
七、总结:作者给 Agent 开发者的 5 条原则
文章最后浓缩出 5 条心法:
- Prompt caching 是前缀匹配。前缀里任何一处改动都会让后面全部失效。把整个系统围绕这个约束来设计,顺序对了,大部分缓存就是"免费"的。
- 用消息更新,而不是改 prompt。日期变化、模式切换、状态更新——全部走消息流,别去碰系统提示词。
- 不要中途换模型或工具。把状态变化(比如 Plan Mode)建模成"工具调用",而不是"工具集变更"。需要"隐藏"工具时用 defer_loading 而不是删除。
- 像监控 uptime 一样监控缓存命中率。Claude Code 团队对缓存命中率掉点会发报警、当事故处理。几个百分点的命中率变化,对成本和延迟的影响是巨大的。
- 分叉操作必须共享父请求的前缀。需要做"侧路计算"(压缩、总结、调用 skill 等)时,参数一定要和父对话保持一致,这样才能蹭到父对话的缓存。
八、几个值得带走的"工程哲学"思考
第一,缓存不是优化,而是架构约束。一般人把 cache 当成"事后加上去的性能优化",但 Claude Code 是反过来的——先确定 cache 的工作方式,再围绕它设计 prompt 结构、产品功能、模式切换机制。Plan Mode 那个设计就是典型:不是"想出一个功能再考虑怎么缓存",而是"缓存约束催生了一个更好的功能形态"。
第二,反直觉的取舍无处不在。"小模型答简单问题更便宜"是错的;"只给模型当前需要的工具更聪明"是错的;"过期信息就该立刻更新到 prompt"也是错的。Agent 这一层的工程直觉,和传统软件工程的直觉相当不一样,因为成本模型完全变了——计算成本主要花在"重新处理上下文"而非"生成新 token"上。
第三,Anthropic 在把内部经验产品化。文中多次提到"这些坑你不用自己踩了,我们已经把 compaction、tool search 这些机制做进 API 了"。这是 Anthropic 一以贯之的路线——先在 Claude Code 这种自家产品上把模式跑通,再下放到 API 给所有开发者用。所以如果你在做 agent,定期翻 Anthropic API 的 changelog 是有用的,经常会冒出"哦原来这个问题官方已经解决了"。
更多推荐


所有评论(0)