- 发布
AI 阅读打卡 Day 1
- 作者

- 名称
- 徐志毅
原文: https://x.com/trq212/status/2024574133011673516
老话常说:“Cache Rules Everything Around Me”, 这句话同样适用Agents。
像Claude Code这种长时间运行的agentic产品, 它能运行是因为提示缓存(prompt caching),这项技术可以允许我们重复使用的之前交互得出的计算结果,从而可以显著降低延迟和消费成本。
什么是提示缓存(prompt caching), 它是如何运行的,以及你如何技术上实现的可以看这篇文章@RLanceMartin's piece on prompt caching and our new auto-caching launch.
在Claude Code中, 我们整个系统的设计都围绕着提示缓存这一核心展开。
一个高效的提示缓存命中率不仅可以减少成本,而且还可以为我们的订阅计划设定更宽松的速率限制标准, 所以我们对提示缓存命中率设置了监控告警,如果太低了,会宣布进入故障事件(SEV)处理流程。
这些便是我们在大规模优化提示缓存过程中所领悟到的(往往不太直观的)经验教训。

制定缓存提示信息
提示缓存通过前缀匹配来工作 - API会缓存从请求开头到每一个cache_control块之间的请求内容。
这意味着你处理事情的顺序至关重要,你要让尽可能多的请求享受同一个前缀。
实现这个最好的方式就是 静态文件首先,动态内容最后。Claude Code就类似如下:
- 静态的系统提示 & 工具 (全局缓存)
- Claude的markdown (缓存在一个项目中)
- Session上下文 (缓存在session)
- 对话消息
这个方式我们可以最大化的共享更多的seesion缓存命中的数量。
但是这也变得出乎意料的脆弱! 我们曾因以下原因打破过顺序:在静态系统提示词中放入详细的时间戳,不确定的目的打乱工具的顺序,更新工具的参数(比如什么agents agent工具可以调用)等等
用消息来传递更新内容
很多时候你放入提示词的信息会过时,例如用户改变一个文件或者时间发生变化。
你可能要更新提示词,但是这会导致缓存未命中,导致用户产生更贵的成本
更好的做法是: 在下一轮对话,传递这些更新。
在CC里,如果有消息更新(比如今天是星期三), 我们会在下一个用户消息或者工具结果中添加一个system-reminder标签,这样既告诉了模型新情况,又保住了前面的缓存。
不要在对话中切换模型
提示词缓存对模型来说是唯一的,这就导致了一个反直觉的成本计算
如果你已经有100K个token在opus聊天中,这时候,你想问一个很简单回答的问题,你可能觉得切换便宜的Haiku模型省钱。但是错了,这反而会会更贵,因为你需要为Haiku重新建立更多提示词缓存。
如果你需要切换模型,最好的方案是用子agenet(子代理), 让Opus准备一个交接的信息给其他模型,处理需要的任务。 我们就经常用这种方式调用 Haiku去探索CC的功能。
永远不要中途添加或者删除工具
在会话中途切换工具集,是大多数人破话提示词缓存的常见操作之一
直觉上,你可以觉得给模型提供他需要的工具。但是,因为工具定义是缓存前缀的一部分, 添加或者移除工具,都会导致整个会话的缓存失败。
计划模式 - 围绕着缓存
计划模式是围绕着缓存约束来涉及功能的一个最好的例子。直接的做法是: 当用户进入计划模式,把工具集替换掉,只保留只读功能。但这会破坏缓存。
替换的是,我们保留了请求中所有的工具,同时把 EnterPlanMode(进入计划模式)和 ExitPlanMode(退出计划模式)本身也做成工具。
当用户进入计划模式, agent会获得一个系统信息,告诉它,进入计划模式,你的任务是探索代码库,不要修改文件,计划完成后调用 ExitPlanMode。工具的定义根本没有改变。
这还有个额外的好处,因为EnterPlanMode 也是一个工具,可以自己调用自己,当他遇到困难的问题,他可以自动进去计划模式,而且全程不会破坏缓存。
工具搜索 - 推迟取代删除
同样的原则也适用于我们的工具搜索功能。CC可以加载外部的MCP工具,如果每个请求都包括他们的话,成本很高。但是中途删除他们,又会破坏缓存。
我们解决方案: 延迟加载。 取代删除工具,我们发送轻量的占位符,就是工具的名字。 可以通过ToolSearch来“发现”它们。只有当模型选中某个工具时,才会加载完整的工具结构。
相同的占位符都是一样的,顺序没变,缓存前缀就会保持住了。
幸运的是,您可以通过我们的 API 使用【工具搜索】https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool 工具来简化这一过程。
处理上下文压缩

当你在运行上下文快满的时候, 我们需要进行压缩。就是把之前的对话总结一下,带着这个进行一个新的会话。
惊喜的是,压缩过程也隐藏着很多破坏缓存的陷阱。
特别是, 当我们需要压缩时,我们需要吧整个对话发给模型,让它生成一个总结。 如果这是与不同系统相关的单独 API 调用,且没有工具辅助(这是最简单的实现方式),那么这个请求的前缀和原对话完全不匹配。你就必须为这些输入token,支付全价,用户成本会瞬间飙升。
解决方案 - 缓存安全分支
当我们进行压缩时,我们用到了父级会话的完全相同的系统提示词,用户上下文,系统上下文 和工具定义。
你先把父对话的历史消息放在前面,然后把“请生成总结”这条指令,作为一条新的用户消息追加在最后。
在API请求看来,这请求和父级最后请求接近一模一样,相同的前缀,相同的工具,相同的历史。于是,缓存前缀被成功复用。你需要支付的新 token,仅仅是最后那句“请总结”的指令而已。
这意味着,你要留一个压缩缓冲,为了有足够额度空间去给上下文窗口,放得下最后这条压缩指令和总结输出的tokens。
压缩是棘手的但是是幸运的,你不需要自学课程,我们已经能集成到了 API 中,因此您可以在自己的应用程序中应用这些模式。
总结
- Prompt 缓存看的是前缀匹配 前面任何一点改变,都会让后面的缓存失效。你的系统设计必须围绕这个约束展开。只要顺序排对了,很多时候缓存会自动生效。
- 用信息替代改变系统提示词 你可能会想要修改系统提示信息,为了诸如进入计划模式、更改日期等操作。但实际上,将这些内容直接插入对话中的消息中会更好。
- 对话中途不要更改工具或模型 使用工具来模拟状态转换(例如计划模式)而非改变工具集。推迟工具的加载而非移除工具。
- 像监控服务器宕机一样监控缓存 我们会为缓存断裂报警,并把它们当成事故来处理。哪怕缓存命中率只掉了几个百分点,成本和延迟也会大受影响。
- 分支操作需要共享父级的前缀 如果您需要执行辅助计算(如压缩、汇总、技能执行),请使用相同的缓存安全参数,以便在父级的前缀上获得缓存命中。。
Claude Code 从第一天起就是围绕 Prompt 缓存构建的。如果你也在开发 Agent,建议你也这么做。