Prompt Caching 不是优化项,而是 Agent 系统设计的起点

最近读到一篇很有启发的文章:Lessons from Building Claude Code: Prompt Caching Is Everything。它讨论的不是一个局部技巧,而是一个很容易被忽略的系统级事实:

对于长会话、长上下文、频繁往返的 Agent 产品来说,Prompt Caching 往往不是“锦上添花”的优化,而是决定成本、延迟和产品形态的基础设施。

如果一句话概括这篇文章的核心观点,那就是:

Prompt cache 命中率,会反过来塑造你的 prompt 结构、工具设计、模型切换策略,甚至影响功能应该怎样实现。


这篇文章主要讲了什么

文章围绕 Claude Code 的工程经验,分享了一个核心结论:Prompt caching 的本质是前缀复用(prefix match)。也就是说,只有当请求前面那一大段内容保持稳定时,缓存才能命中。一旦前缀中某个位置发生变化,后续内容的缓存收益就会被破坏。

这直接带来一个非常反直觉的设计原则:

你不能把缓存当成底层能力接一下就完事了,而应该从系统设计阶段就围绕“如何保持前缀稳定”来做架构。
agent prompt架构如下:
在这里插入图片描述

文章具体展开了几个关键经验:

  1. Prompt 的内容顺序极其重要,应该把稳定内容放前面,把动态内容放后面。
  2. 不要轻易改 system prompt,很多变化更适合通过消息注入。
  3. 不要在会话中途切换模型,否则缓存会失效。
  4. 不要在会话中途增加或删除工具,否则会破坏缓存前缀。
  5. 像压缩上下文、做总结、启动子任务这类“分叉操作”,也应该尽量复用原会话的前缀。

这些经验看起来像实现细节,但其实都在说明一件事:

Agent 的很多功能设计,不应该只看“语义上对不对”,还要看“缓存上稳不稳”。


为什么 Prompt Caching 这么重要

传统对话产品里,一次请求的 prompt 相对较短,即使有一些重复,浪费也未必特别夸张。但 Agent 类产品不一样。

Agent 往往会有这些特点:

  • system prompt 很长
  • 工具定义很多
  • 会话历史很长
  • 每一步都要多轮调用模型
  • 一个任务可能持续几十轮甚至上百轮

在这种情况下,如果每一轮都把大量重复 token 重新算一遍,成本和延迟都会迅速膨胀。缓存命中得越好,单次交互越便宜,响应越快,产品能给用户的额度和体验也越“慷慨”。

Claude Code 团队甚至会监控 prompt cache hit rate,并在命中率明显下降时当成事故处理。这说明在真实产品里,缓存命中率已经不是可有可无的指标,而是直接影响业务表现的核心指标。


第一条经验:Prompt 结构要为缓存而排布

文章里最重要的工程原则之一,就是:

静态内容放前面,动态内容放后面。

Claude Code 的大致组织方式是:

  1. 全局稳定的 system prompt 和 tools
  2. 项目级上下文,例如 Claude.md
  3. 会话级上下文
  4. 当前对话消息

这样安排的目的,是让尽可能多的请求共享同一段前缀。

这背后的启发非常强:我们平时设计 prompt,往往先考虑“模型好不好理解”;但做 Agent 时,还要增加一个维度,叫“不同请求之间能不能最大化共享前缀”。

文章还提到一些很容易忽略的缓存杀手,例如:

  • 在静态 system prompt 里放精确时间戳
  • 工具定义顺序不稳定
  • 工具参数在不同轮次中悄悄变化

这些改动看上去很小,但因为它们出现在前缀区域,会让缓存命中率断崖式下降。


第二条经验:能用消息更新,就不要改 System Prompt

这是文章里一个特别实用的建议。

很多时候,系统里的信息会变化,比如:

  • 当前时间变了
  • 用户修改了文件
  • 系统状态切换了

直觉上,我们很容易去更新 system prompt。但问题是,一旦你改了 prompt 前面的稳定部分,就等于主动放弃了缓存。

Claude Code 的做法是:尽量把这些变化,通过下一轮消息补进去,而不是直接修改原本稳定的提示词。例如,用类似系统提醒的消息告诉模型“现在是星期三”或者“某个文件刚刚被修改了”。

这背后的思路非常值得借鉴:

让 system prompt 承担“长期稳定的规则”,让 message 承担“临时变化的状态”。

这不仅更利于缓存,也能让提示词职责更清晰。


第三条经验:不要在会话中途切模型

很多人会觉得,复杂问题用大模型,简单问题切到便宜小模型,更省钱。

但文章指出,在长上下文场景里,这件事常常恰恰相反。

因为 prompt cache 是和具体模型绑定的。如果一个会话已经在某个模型上积累了大量缓存,这时候切到另一个模型,意味着你要重新构建整段上下文,对成本的打击可能比继续用原模型更大。

也就是说:

从单次调用看,小模型更便宜;但从整段会话看,切模型可能更贵。

这是一种很典型的“局部最优不等于全局最优”。

文章给出的更好方案是:如果确实需要换模型,就通过子代理或子任务来做,把主会话保持在原模型上,让另一个模型去处理局部任务。

这个建议非常像操作系统里的进程隔离思路:不是把主上下文打碎重来,而是把局部工作外包出去。


第四条经验:不要在会话中途增删工具

这一条对做 MCP、函数调用、工作流 Agent 的人尤其重要。

很多人会本能地觉得:当前轮次需要什么工具,我就给模型什么工具;不需要的工具就先移掉,这样更干净。

但从缓存角度看,这种做法代价很高。因为工具定义通常也在 prompt 前缀里,只要你增删工具,整个缓存前缀就变了。

Claude Code 的解决方式很巧妙:不要真的切换工具集,而是把“状态切换”也建模成工具或消息。

比如它们的 Plan Mode,不是进入计划模式就换一套只读工具,而是:

  • 工具仍然保持不变
  • 通过 EnterPlanMode / ExitPlanMode 这样的工具表达状态切换
  • 再用系统消息告诉模型当前处于计划模式,应该只做探索,不做修改

这样既保住了缓存,也让状态机更一致。

文章还提到另一个思路:对于大量工具,不要动态移除,而是通过延迟加载、工具搜索、轻量 stub 等方式,让“工具列表的骨架”保持稳定,只有真正用到时再补充细节。

这说明一个重要原则:

为了缓存稳定,很多动态能力应该设计成“延迟展开”,而不是“动态重写前缀”。


第五条经验:上下文压缩和总结,也要做成 Cache-Safe

文章中我觉得最有洞察力的一部分,是它谈到了 compaction,也就是上下文窗口不够用时,对历史对话做总结压缩。

直觉上,这件事很简单:另起一个请求,把历史喂给模型,让它总结一下,再带着总结继续。

但这里隐藏了一个大坑:

如果这个“总结请求”用的是另一套 system prompt、另一套工具,或者干脆没有工具,那么它和原会话的缓存前缀就完全对不上。这样一来,你会为整段历史再次付费,成本非常高。

Claude Code 的做法是:

  • 保持和父会话几乎一致的前缀
  • 复用相同的 system prompt、上下文和工具定义
  • 把“请帮我压缩总结”作为新的用户消息追加到末尾

这样从 API 视角看,它还是那条老会话的自然延续,于是前缀缓存可以继续命中。

这个经验非常有启发,因为它其实不只适用于 compaction,也适用于很多“旁路任务”:

  • 对历史做总结
  • 执行技能或子任务
  • 生成中间摘要
  • 做 fork 分支探索

凡是这类操作,只要它们共享足够多的父上下文,就应该尽量做成 cache-safe fork。


这篇文章给我们的真正启示

如果只看表面,这篇文章像是在讲一套 prompt caching 的调优技巧;但我觉得它真正传达的是更底层的方法论:

设计 Agent,不要只从能力和语义出发,还要从“可缓存性”出发。

换句话说,Prompt Caching 不是最后再加的一层性能优化,而应该成为产品设计时的“约束条件”。

它会影响:

  • prompt 怎么分层
  • 状态怎么表达
  • 工具怎么组织
  • 模型怎么协作
  • 长上下文怎么压缩
  • 功能切换怎么实现

很多看起来“更自然”的实现方式,在缓存层面可能都不是最优解。真正成熟的 Agent 系统,往往不是语义上最直观的那种,而是既满足语义,又尽量保持前缀稳定的那种。


如果我们自己做 Agent,可以直接借鉴什么

结合这篇文章,我觉得至少可以马上落地下面几条实践:

1. 把 Prompt 分成稳定层和变化层

稳定层包括:

  • 核心 system prompt
  • 固定工具定义
  • 项目级说明文档

变化层包括:

  • 当前任务状态
  • 临时提醒
  • 文件变更
  • 用户最新输入

目标是让稳定层尽量不变,把变化都压到后面。

2. 给缓存命中率做监控

如果一个 Agent 产品已经进入真实使用阶段,就应该监控:

  • cache hit rate
  • 平均输入 token 成本
  • 首 token 延迟
  • 因 prompt 变化导致的缓存失效比例

否则你很可能只看到“模型越来越贵”,却不知道问题出在系统设计上。

3. 用消息表达状态,不要频繁改工具和 Prompt

像“进入规划模式”“当前时间更新了”“文件刚被改过”这类信息,优先考虑:

  • system reminder
  • 状态消息
  • 显式状态工具

而不是直接改前缀。

4. 把复杂能力设计成子任务,而不是打断主会话

如果某一步适合小模型、专门模型或单独执行,就用子 agent、handoff、fork 的方式处理,不要轻易破坏主会话的缓存积累。

5. 对总结、压缩、分叉任务做“前缀复用”设计

任何旁路任务,只要仍然依赖父会话,就应优先复用父会话的前缀环境,而不是临时起一个完全不同的调用模板。


结语

这篇文章最有价值的地方,在于它提醒我们重新理解 Agent 工程里的“优化”。

在很多传统系统里,缓存是性能优化;但在 Agent 系统里,尤其是长上下文、多轮调用、强工具依赖的系统里,缓存其实已经上升为产品可行性的条件之一。

当一个系统的成本、延迟、额度策略都和缓存命中率强相关时,你就不能把它当成底层黑盒,而必须让它进入架构设计。

所以如果你正在做自己的 coding agent、workflow agent,或者任何长会话 AI 产品,这篇文章最值得记住的一句话可能不是“prompt caching 很重要”,而是:

把缓存当成系统设计的一部分,而不是部署完成后才想起来补上的优化。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐