跳到正文

构建 Claude Code 的经验教训:提示缓存就是一切

在 Claude Code 中优化提示缓存的最佳实践,包括如何最有效地构建提示、使用工具以及叠加压缩。

本文目录文章开头

工程领域常说“缓存主宰我周围的一切”,同样的规则也适用于智能体。

像 Claude Code 这样的长时运行智能体产品之所以可行,得益于 提示词缓存,它让我们能够复用前几轮往返的计算结果,显著降低延迟和成本。

在 Claude Code,我们围绕提示词缓存构建了整个运行框架。较高的提示词缓存命中率能降低成本,并帮助我们为订阅计划制定更宽松的速率限制,因此我们会对提示词缓存命中率运行告警,命中率过低时会宣布 SEV。

以下是我们大规模优化提示词缓存时学到的(往往有违直觉的)经验教训。

为缓存设计你的提示词结构

System Prompt Layout, top to bottom: base system instructions and tools (globally cached), CLAUDE.md and memory (cached per project), session state such as env, MCP and output style (cached per session), and messages, which grow each turn.
图 AClaude Code 的系统提示词经过组织,使稳定部分保持缓存,只有对话本身逐轮增长。

提示词缓存通过前缀匹配工作——API 会缓存从请求开头到每个 cache_control 断点的所有内容。这意味着你放入内容的顺序至关重要,你希望尽可能多的请求共享一个前缀。

提示词的结构不仅影响缓存命中,也影响输出质量——提示词工程基础是这门学科的另一半。

实现这一点的最佳方式是静态内容在前,动态内容在后。对 Claude Code 来说,结构如下:

  1. 静态系统提示词 & 工具(全局缓存)
  2. CLAUDE.md(在项目内缓存)
  3. 会话上下文(在会话内缓存)
  4. 对话消息

这样我们就能最大化共享缓存命中的会话数量。

但这种方法可能出人意料地脆弱。我们曾因各种原因破坏过这种顺序,包括:在静态系统提示词中放入精确到秒的时间戳、以非确定性方式打乱工具顺序定义,以及更新工具的参数(例如 Agent 工具可以调用哪些智能体)。

用消息进行更新

有时你放入提示词的信息会过时,例如时间变化或用户修改了文件。更新提示词可能很诱人,但这会导致缓存未命中,最终可能让用户付出相当高的代价。

考虑是否可以在智能体的下一轮通过消息传递这些信息。在 Claude Code 中,我们在下一条用户消息或工具结果中添加 <system-reminder> 标签,向模型提供更新后的信息,这有助于保留缓存。

不要在会话中途更换模型

提示词缓存是模型特有的,这使得提示词缓存的计算相当反直觉。

例如,如果你与 Opus 的对话已进行到 10 万 token,而你想问一个相当容易回答的问题,切换到 Haiku 实际上比让 Opus 回答更昂贵,因为我们需要为 Haiku 重建提示词缓存。

如果需要切换模型,最佳方式是使用子智能体;延续上面的例子,你可以部署一个子智能体,让 Opus 准备一条“交接”消息,将它需要完成的任务传递给另一个模型。我们经常在 Claude Code 的 Explore 智能体中这样做,它们使用 Haiku。

绝不要在会话中途添加或移除工具

在对话中途更换工具集是人们破坏提示缓存最常见的方法之一。这似乎很直观——你只应提供模型当前所需的工具。但由于工具是缓存前缀的一部分,添加或删除工具会使整个对话的缓存失效。

利用计划模式围绕缓存进行设计

Plan Mode 是围绕缓存约束进行功能设计的一个绝佳例子。直观的做法是:当用户进入计划模式时,将工具集替换为仅包含只读工具,但这会破坏缓存。

相反,我们始终在请求中保留所有工具,并将 EnterPlanMode 和 ExitPlanMode 本身作为工具。当用户开启计划模式时,智能体会收到一条系统消息,说明它处于计划模式以及相应的指令:探索代码库、不要编辑文件,并在计划完成时调用 ExitPlanMode。工具定义从不改变。

这还有一个额外的好处:由于 EnterPlanMode 是模型可以自行调用的工具,它可以在检测到难题时自主进入计划模式,而不会破坏任何缓存。

使用工具搜索来延迟而非移除

同样的原则也适用于我们的工具搜索工具。Claude Code 可以加载数十个 MCP 工具,在每个请求中包含所有工具会很昂贵,但在对话中途移除它们会破坏缓存。

我们的解决方案是:defer_loading。我们不移除工具,而是发送轻量级存根(仅包含工具名称,带有 defer_loading: true),模型可以在需要时通过工具搜索“发现”它们。完整的工具架构仅在模型选择它们时才加载。这使缓存前缀保持稳定,因为相同的存根始终以相同的顺序出现。

你也可以通过我们的 API 使用工具搜索工具来简化这一过程。

不破坏缓存的压缩

How compaction works with prompt caching. Before: system and tools plus many turns, with the context window nearly full. A forked compaction call reuses the same prefix for a cache hit at one tenth the price, appends “Summarize this conversation”, and returns a summary of about 20k tokens. After: system and tools, a compact_boundary, the conversation summary, re-attached files and context, and room for new conversation.
图 B当上下文窗口填满时,Claude Code 会分叉一个已缓存的调用来总结对话,然后用总结代替原始消息继续进行。

Compaction(压缩)是在上下文窗口用尽时发生的事情。我们总结到目前为止的对话,并用该总结继续新的会话。

压缩与提示缓存的交互方式很容易出错。要压缩对话,你必须将完整对话发送给模型,以便它撰写摘要。最简单的方法是进行一次单独的 API 调用,使用自己的系统提示(类似“总结这个”)且不附带工具,但这正是成本陷阱所在。提示缓存仅在请求的前缀从头开始逐字节匹配已缓存内容时才适用。你的主要对话在一个系统提示和工具集下被缓存;总结调用使用不同的系统提示且没有工具,因此前缀在第一个标记处就分道扬镳,缓存完全不适用。你最终要为发送的整个对话支付完整的、未缓存的输入费率——而且对话越长(即你最初越需要压缩),那一次调用就越昂贵。

解决方案:缓存安全的分叉

当我们执行压缩时,我们使用与父对话完全相同的系统提示、用户上下文、系统上下文和工具定义。我们将父对话的消息前置,然后将压缩提示作为新的用户消息附加在末尾。

从 API 的角度来看,该请求与父请求的上一个请求几乎完全相同——相同的前缀、相同的工具、相同的历史记录——因此会复用缓存前缀。唯一的新 token 是压缩提示本身。

但这确实意味着我们需要保存一个“压缩缓冲区”,以便在上下文窗口中有足够的空间来包含压缩消息和摘要输出 token。

压缩很棘手,但幸运的是,你不需要亲自吸取这些教训——基于我们在 Claude Code 中的经验,我们直接在 API 中构建了 compaction,这样你就可以在自己的应用中应用这些模式。

经验教训

以下是我们在构建 agent 时发现的用于优化提示缓存的一些模式:

  1. 提示缓存是前缀匹配。前缀中任何位置的任何更改都会使其之后的所有内容失效。围绕这一约束设计你的整个系统。把顺序搞对,大部分缓存就能自动生效。
  2. 使用消息而不是修改系统提示。你可能会想编辑系统提示来执行诸如进入计划模式、更改日期等操作,但实际上在对话过程中将这些内容插入消息中会更好。
  3. 不要在对话中途更改工具或模型。使用工具来建模状态转换(如计划模式),而不是更改工具集。延迟加载工具,而不是移除工具。
  4. 像监控正常运行时间一样监控你的缓存命中率。我们会对缓存中断发出警报,并将其视为事故。几个百分点的缓存未命中率会显著影响成本和延迟。
  5. 分叉操作需要共享父请求的前缀。如果你需要运行一个旁路计算(压缩、摘要、技能执行),请使用相同的缓存安全参数,以便你能命中父请求的前缀。

Claude Code 从第一天起就围绕提示缓存构建;为了在构建 agent 时获得最佳效果,我们也建议你这样做。

立即开始使用 Claude Code。

本文由 Claude Code 团队的技术成员 Thariq Shihipar 撰写。

阅读显示设置

调整正文排版

当前浏览器记住选择,应用于文章正文。手机上正文宽度以屏幕可用空间为限。

图表完整性提示

普通文章遇到图表保存问题时显示提示。