在工程领域常有人说“缓存主宰一切”(Cache Rules Everything Around Me),这一法则同样适用于 Agent。
像 Claude Code 这类长时间运行的 Agent 产品,之所以可行,靠的是提示缓存,它允许我们复用之前轮次的计算结果,显著降低延迟和成本。
什么是提示缓存?它是如何工作的?技术上又该如何实现?更多内容请阅读 @RLanceMartin 关于提示缓存及我们新推出的自动缓存功能的文章。
在 Claude Code 中,我们整个架构都围绕提示缓存来构建。高提示缓存命中率能降低成本,并帮助我们为订阅计划提供更慷慨的速率限制,因此我们会针对提示缓存命中率设置告警,如果命中率过低,甚至会发起严重事故等级(SEV)响应。
以下是我们从大规模优化提示缓存中学到的(往往不太直观的)经验。
按缓存规则布局你的提示词

提示缓存基于前缀匹配工作——API 会缓存从请求开始到每个 cache_control 断点之间的所有内容。这意味着内容的顺序至关重要,你希望尽可能多的请求共享相同的前缀。
最好的做法是:静态内容在前,动态内容在后。对于 Claude Code,具体结构如下:
- 静态系统提示 & 工具(全局缓存)
- Claude.MD(项目内缓存)
- 会话上下文(会话内缓存)
- 对话消息
这样,我们就能最大化会话之间的缓存命中。
但这可能出奇地脆弱!我们之前破坏过这种排序的例子包括:在静态系统提示中放置了详细的时间戳、非确定性地打乱工具顺序定义、更新工具参数(比如 AgentTool 可调用的 Agent 列表)等等。
使用消息进行更新
有时,你放入提示词的信息可能会过时,比如时间或用户修改了文件。你可能会想更新提示词,但这会导致缓存未命中,最终给用户带来更高的成本。
考虑是否可以在下一轮对话中通过消息传递这些信息。在 Claude Code 中,我们会在下一次用户消息或工具结果中添加一个 <system-reminder> 标签,携带更新后的信息(例如“现在是周三”),这样有助于保持缓存。
不要在会话中途切换模型
提示缓存是模型特有的,这使得提示缓存的成本计算非常不直观。
如果你与 Opus 的对话已经达到 10 万 token,然后想提一个相当简单的问题,此时切换到 Haiku 反而会比让 Opus 回答更贵,因为我们需要为 Haiku 重建提示缓存。
如果需要切换模型,最好的方式是通过子 Agent 来实现:让 Opus 准备好一条“交接”消息,将需要完成的任务传递给另一个模型。我们在 Claude Code 的 Explore Agent 中经常这样做,它们使用的就是 Haiku。
不要在会话中途添加或移除工具
在对话过程中更改工具集是人们破坏提示缓存最常见的方式之一。直觉上,你只应该给模型提供它当前需要的工具。但由于工具是缓存前缀的一部分,添加或移除工具会使整个对话的缓存失效。
计划模式——围绕缓存设计
计划模式就是一个围绕缓存约束设计功能的绝佳例子。直观的做法是:当用户进入计划模式时,将工具集替换为只包含只读工具。但这样做会破坏缓存。
相反,我们始终保持所有工具在请求中,并将 EnterPlanMode 和 ExitPlanMode 本身也作为工具。当用户切换计划模式时,Agent 会收到一条系统消息,说明当前处于计划模式以及执行指令——探索代码库、不要编辑文件、计划完成后调用 ExitPlanMode。工具定义始终不变。
这还有一个额外好处:因为 EnterPlanMode 是模型可以自行调用的工具,所以当模型检测到难题时,它可以自主进入计划模式,而不会破坏缓存。
工具搜索——延迟加载而非移除
同样的原则也适用于我们的工具搜索功能。Claude Code 可能加载了数十个 MCP 工具,在每次请求中都包含所有这些工具会非常昂贵。但在对话中途移除它们会破坏缓存。
我们的解决方案:defer_loading(延迟加载)。我们不移除工具,而是发送轻量级的存根——只包含工具名称,并设置 defer_loading: true——当模型需要时,可以通过 ToolSearch 工具来“发现”它们。完整的工具架构只有在模型选中时才会加载。这样保持了缓存前缀的稳定:相同的存根始终以相同的顺序存在。
幸运的是,你可以通过我们的 API 使用工具搜索工具来简化这一过程。
分叉上下文——压缩

压缩发生在上下文窗口用尽时。我们会总结当前对话,并用该总结继续一个新的会话。
令人惊讶的是,压缩在提示缓存方面存在许多不直观的边缘情况。
特别是,当我们进行压缩时,需要将整个对话发送给模型以生成摘要。如果这是一个独立的 API 调用,使用了不同的系统提示且没有工具(这是简单的实现方式),那么与主对话的缓存前缀完全不匹配。你需要为所有这些输入 token 支付全价,大幅增加了用户的成本。
解决方案——缓存安全的分叉
当我们执行压缩时,我们使用完全相同的系统提示、用户上下文、系统上下文和工具定义,与父对话保持一致。我们先附加父对话的消息,然后在末尾追加压缩提示作为新的用户消息。
从 API 的角度来看,这个请求与父对话的上一个请求几乎相同——相同的前缀、相同的工具、相同的历史——因此缓存的 prefix 被重用。唯一的新的 token 就是压缩提示本身。
但这意味着我们需要保存一个“压缩缓冲区”,以便在上下文窗口中有足够的空间容纳压缩消息和摘要输出 token。
压缩虽然棘手,但幸运的是,你无需亲自摸索这些经验——基于我们在 Claude Code 中的学习成果,我们直接将压缩功能构建到了 API 中,因此你可以在自己的应用中应用这些模式。
经验教训
- 提示缓存是前缀匹配。 前缀中的任何改动都会使其之后的所有内容失效。围绕这一约束设计你的整个系统。顺序正确,大部分缓存工作就能自动完成。
- 使用消息而非更改系统提示。 你可能会想通过编辑系统提示来进入计划模式、更改日期等,但更好的做法是在对话过程中将这些信息插入到消息中。
- 不要在对话中途更改工具或模型。 使用工具来建模状态转换(例如计划模式),而不是更改工具集。采用延迟加载而非移除工具。
- 像监控正常运行时间一样监控缓存命中率。 我们会对缓存破坏设置告警,并将其视为事故处理。缓存未命中率哪怕只有几个百分点,也会显著影响成本和延迟。
- 分叉操作需要共享父对话的前缀。 如果需要运行辅助计算(压缩、摘要、技能执行),请使用相同的缓存安全参数,以便在父对话前缀上获得缓存命中。
Claude Code 从第一天起就围绕提示缓存构建,如果你正在构建 Agent,你也应该这样做。





