Agent X-Ray
RuntimeNotesAbout
Notes/AI 前沿/大厂技术博客档案/第45章

构建 Claude Code 的经验:提示缓存 (Prompt Caching) 决定一切

6 分钟 · 更新于 2026-09-01 · 原文

要点速览

  • 对长生命周期代理产品而言,提示缓存 (Prompt Caching) 直接决定成本、延迟和订阅方案可提供的额度,必须像可用性一样被严密监控。
  • 提示缓存是前缀匹配机制,因此系统提示、工具定义、项目上下文、会话上下文和消息的排列顺序必须围绕共享前缀来设计。
  • 中途切换模型、增删工具,或直接修改系统提示,都会导致缓存命中率大幅下降;更好的做法是用消息、状态工具和延迟加载来表达变化。
  • Plan mode、Tool Search 和 compaction 这些能力都需要从“缓存安全”角度来设计,否则用户成本会被隐性放大。
  • 凡是需要 fork 出侧路计算的操作,都应该尽量复用父会话的前缀,这样才能安全继承已有缓存。

原文链接 原文:English Original 作者:Thariq (@trq212),Anthropic Claude Code 开发者

构建 Claude Code 的经验:提示缓存 (Prompt Caching) 决定一切

工程领域里常有人说一句话:“Cache Rules Everything Around Me”,而对代理来说,这条规则同样成立。

像 Claude Code 这样可长时间运行的代理式产品,之所以能在工程上可行,很大程度上依赖提示缓存。它让我们能够复用前几轮交互里的计算结果,从而显著降低延迟和成本。什么是提示缓存,它是如何工作的,又该怎样在技术上实现?可以看看 @RLanceMartin 关于提示缓存的文章,以及我们最新的自动缓存发布说明。

在 Claude Code 中,我们整个运行框架几乎都是围绕提示缓存搭建的。高提示缓存命中率既能降低成本,也能让我们的订阅计划提供更慷慨的速率限制,所以我们会对提示缓存命中率设置告警,一旦太低就会按 SEV 级别处理。

下面这些,就是我们在大规模优化提示缓存过程中学到的经验,其中不少都和直觉不太一样。

为缓存来布局你的提示

提示缓存通过前缀匹配 (prefix matching) 工作。API 会从请求开头开始缓存,一直到每个 cache_control 断点为止。这意味着你怎样排列内容极其重要。你要尽可能让更多请求共享同一个前缀。

最佳实践是:静态内容放前面,动态内容放后面。对 Claude Code 来说,大致是这样:

  1. 静态系统提示和工具定义(全局缓存)
  2. Claude.MD(在项目范围内缓存)
  3. 会话上下文(在会话范围内缓存)
  4. 对话消息

这样我们就能最大化不同会话之间共享缓存命中的概率。

但这件事其实脆弱得出乎意料。我们过去就因为很多原因破坏过这种顺序,例如:把一个非常细的时间戳塞进静态系统提示里;以非确定性方式打乱工具定义顺序;修改工具参数(例如 AgentTool 可调用哪些代理)等等。

用消息承载更新

有时你放进提示里的信息会过期,比如时间变化了,或者用户修改了某个文件。你可能会本能地想直接更新提示,但那会导致缓存未命中,对用户来说成本可能相当高。

你应该考虑,能不能把这些更新改为在下一轮通过消息传入。比如在 Claude Code 里,我们会在下一条用户消息或工具结果中加入一个 <system-reminder> 标签,把更新后的信息告诉模型(例如“现在已经是周三了”),这样可以尽量保住缓存。

不要在会话中途切换模型

提示缓存是模型级隔离的,这会让提示缓存的成本计算变得相当反直觉。

如果你已经在和 Opus 的一段对话中积累了 100k token,这时想问一个其实很简单的问题,切换到 Haiku 反而可能比继续让 Opus 回答更贵,因为我们必须为 Haiku 重新构建一遍提示缓存。

如果你确实需要切换模型,最佳做法是借助子代理 (subagents)。也就是说,由 Opus 先为另一个模型准备一条“交接”消息,再把任务转过去。我们在 Claude Code 中经常这么做,比如 Explore agents 就使用 Haiku。

永远不要在会话中途增删工具

在对话进行过程中改变工具集合,是人们最常见的提示缓存破坏方式之一。直觉上你会觉得,只把当前真正需要的工具提供给模型才合理。但因为工具本身属于被缓存的前缀,任何增删都会让整段对话的缓存失效。

Plan Mode:围绕缓存来做设计

Plan mode 就是一个围绕缓存约束设计功能的典型例子。最直观的做法似乎是:当用户进入 plan mode,就把工具集合切换成只读工具。但这样会直接打破缓存。

所以我们的做法是:始终把所有工具都保留在请求里,并把 EnterPlanMode 和 ExitPlanMode 本身也设计成工具。当用户切换到 plan mode 时,代理会收到一条系统消息,说明当前处于 plan mode,以及应该遵循的规则,比如探索代码库、不要编辑文件、在计划完成后调用 ExitPlanMode。工具定义本身从不变化。

这还有一个额外好处:因为 EnterPlanMode 是模型自己可以调用的工具,所以当它识别到一个棘手问题时,就可以自主进入 plan mode,而不会打破缓存。

Tool Search:延后加载,不要移除

同样的原则也适用于我们的 tool search 功能。Claude Code 可能会一次加载数十个 MCP 工具,如果每次请求都把它们全部带上,成本会很高。但如果在会话中途把它们删掉,又会破坏缓存。

我们的解决办法是 defer_loading。我们不移除工具,而是发送轻量级 stub,也就是只给出工具名并设置 defer_loading: true,模型在需要时再通过 ToolSearch 工具去“发现”它们。只有当模型选中某个工具时,完整 schema 才会真正加载进来。这样一来,被缓存的前缀始终保持稳定:同样的 stub 总是按同样顺序存在。

好在你可以直接通过我们的 API 使用 tool search tool,这能显著简化实现。

Forking Context:Compaction

当你耗尽上下文窗口时,就会发生 compaction。我们会先把当前对话总结一下,然后带着这份摘要继续开启一个新会话。

出人意料的是,compaction 和提示缓存之间有很多容易让人误判的边界情况。

特别是,在做 compaction 时,我们需要把完整对话重新发给模型以生成摘要。如果这是一个单独的 API 调用,而且使用了不同的系统提示、也不带工具(这属于最简单的实现方式),那么它和主对话的缓存前缀根本对不上。结果就是:那些输入 token 全都要按全价重新计算,用户成本会明显飙升。

解决方案:缓存安全的 Forking

当我们执行 compaction 时,会复用和父对话完全相同的系统提示、用户上下文、系统上下文以及工具定义。我们先拼接上父对话已有的消息,再把 compaction 提示作为一条新的用户消息附在最后。

从 API 的视角看,这个请求几乎和父对话的上一轮请求一模一样:同样的前缀、同样的工具、同样的历史。因此,父对话的缓存前缀可以被直接复用。真正新增的 token,只有 compaction 提示本身。

不过,这也意味着我们需要预留一个“compaction buffer”,这样上下文窗口里才能始终留出足够空间,容纳 compaction 消息和摘要输出 token。

Compaction 确实很棘手。但幸运的是,你不必自己把这些坑全部踩一遍。基于我们从 Claude Code 中得到的经验,我们已经把 compaction 直接内建进 API 里了,所以你也可以把这些模式直接用在自己的应用中。

经验总结

  • 提示缓存本质上是前缀匹配。 前缀中任何位置的变动,都会让它后面的缓存全部失效。你应该围绕这个约束来设计整个系统。只要顺序设计对了,大部分缓存收益都会自然出现。
  • 优先用消息,而不是改系统提示。 你也许会想通过改系统提示来表达进入 plan mode、日期变化等状态,但更好的办法通常是在对话过程中把这些信息插入消息中。
  • 不要在对话中途改工具或模型。 像 plan mode 这样的状态切换,应通过工具来建模,而不是改变工具集合。工具加载也应采用延迟加载,而不是直接移除。
  • 像监控服务可用性一样监控缓存命中率。 我们会对缓存破坏发告警,并把它视为事故。哪怕只是几个百分点的缓存未命中,也可能大幅影响成本和延迟。
  • 所有 fork 操作都应共享父前缀。 如果你要运行旁路计算,例如 compaction、摘要生成、Skill 执行等,应尽量使用完全一致、缓存安全的参数,这样才能命中父会话已有的前缀缓存。

Claude Code 从第一天开始就是围绕提示缓存来构建的。如果你也在做代理产品,最好同样如此。


  • 原文:English Original
本章目录
为缓存来布局你的提示用消息承载更新不要在会话中途切换模型永远不要在会话中途增删工具Plan Mode:围绕缓存来做设计Tool Search:延后加载,不要移除Forking Context:Compaction经验总结Related Documents
苏ICP备2025204887号-2