这段时间,一个叫 Reasonix 的终端编程 Agent 很火。

在公众号搜索词条,前三名的文章标题都指出了它最显著的优势——通过高缓存命中降低成本

在这里插入图片描述

提高缓存命中率是 AI 发挥低成本试错价值的核心,Claude Code 团队甚至把缓存命中率低于阈值定义为 SEV(严重事故),和生产事故同级。因为一次缓存失效,意味着这轮对话的所有 token 都要重新计算。系统提示词要重新解析,工具定义要重新加载,历史消息要重新扫描。模型还没开始"思考",成本已经出去了。

正所谓"前事不忘,后事之师",本文整理了 Agent 工程中关于模型缓存的核心经验,分享 Claude Code 与 Reasonix 等代表性产品在开发过程中踩坑踩出来的教训。

希望对各位开发者有所帮助。


教训一:提示词缓存是前缀匹配,布局顺序决定一切

提示词缓存通过前缀匹配实现——API 会从请求开头缓存至每个断点之前的所有内容。这意味着元素的排列顺序至关重要:你希望尽可能多的请求共享同一前缀。

最佳实践是:静态内容在前,动态内容在后。 在 Claude Code 中,这一结构如下:

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

通过这种方式,最大化不同会话之间的缓存命中概率。

但这一方法出奇地脆弱。Claude Code 曾因多种原因破坏过这一顺序,包括:在静态系统提示词中加入详细的时间戳、非确定性地打乱工具定义顺序、以及更新工具参数(例如 Agent 工具可调用的子智能体列表)。

举个例子:你在系统提示词第一层加了个 当前时间:2026-06-01 12:00:00,看起来很合理——模型需要知道时间。但每次发送请求,这个时间戳都会变化,第一层缓存每次都失效,连带后面所有缓存全部作废。正确做法是把时间信息放在对话消息里,通过 <system-reminder> 传给模型。

在这里插入图片描述


教训二:不要在会话中途更换模型

提示词缓存具有模型唯一性(model-specific),这使得缓存的数学逻辑相当不直观。

例如,如果你已与 Opus 进行了 100k tokens 的对话,现在想询问一个相对简单的问题,此时切换到 Haiku 实际上反而更昂贵——因为需要为 Haiku 重建整个提示词缓存。

如果必须更换模型,最佳方式是使用子智能体(subagents)。延续上述例子,你可以部署一个子智能体,让 Opus 准备一条"交接"消息,说明需要完成的任务,然后由另一个模型接手。Claude Code 的 Explore 智能体就是这样做的——它们使用 Haiku 模型,但不破坏父会话的缓存。


教训三:不要在会话中途增删工具

在对话中途变更工具集是人们破坏提示词缓存最常见的方式之一。这看似直观——只给模型当前需要的工具即可。但由于工具属于缓存前缀的一部分,增删任一工具都会使整个会话的缓存失效。

Plan Mode:围绕缓存约束设计功能

Plan Mode 是围绕缓存约束设计功能的典型案例。直觉上的做法可能是:用户进入计划模式时,将工具集替换为只读工具,但这会破坏缓存。

正确的方案是:在请求中始终保留所有工具,并将 EnterPlanModeExitPlanMode 本身作为工具。当用户开启计划模式时,智能体会收到一条系统消息,说明当前处于计划模式及其指令:探索代码库、不编辑文件、完成计划后调用 ExitPlanMode。工具定义永远不会改变,且由于 EnterPlanMode 是模型可以自主调用的工具,当它检测到难题时,可以自动进入计划模式,无需任何缓存破坏。

Tool Search:延迟加载而非移除

同样的原则也适用于工具搜索(Tool Search)功能。Claude Code 可能加载数十个 MCP 工具,在每次请求中包含全部工具代价高昂,但在对话中途移除它们又会破坏缓存。

正确的方案是:延迟加载。不移除工具,而是发送工具的简单说明(仅包含工具名称和使用场景),模型可以通过工具搜索在需要时"发现"它们。完整的工具描述仅在模型选中后才加载。这保持了缓存前缀的稳定性,因为相同的占位符始终以相同顺序存在。


教训四:用消息传递代替提示词修改

提示词中的信息可能会过时,例如时间信息变化,或用户修改了文件。此时你可能倾向于直接更新提示词,但这会导致缓存失效,对用户而言可能代价高昂。

正确的方案是:通过智能体下一轮对话中的消息来传递这些信息。在 Claude Code 中,他们在下一轮用户消息或工具结果中添加 <system-reminder> 标签,向模型提供更新后的信息,从而保护缓存不被破坏。

举个例子:用户改了项目里的 Python 版本,从 3.9 升级到 3.12。你不能修改系统提示词里的版本信息——那会破坏全局缓存。正确做法是在下一轮对话中,通过工具返回结果附带一条 <system-reminder>注意:该项目的 Python 版本已更新为 3.12</system-reminder>。系统提示词没变,缓存照常命中,模型也收到了最新信息。

锁存机制:评估一次,会话期间不可逆

除此之外,Claude Code 还用锁存机制(Latching)保护缓存键的稳定性。核心思路是"评估一次,会话期间不可逆",即一旦在会话中发送过某个请求头,这个请求头在会话剩余期间继续发送,即使对应的功能已经关闭。只有 /clear/compact 命令会重置锁存状态。

在这里插入图片描述


教训五:分叉操作需要共享父前缀

压缩(Compaction)是指上下文窗口耗尽时的处理机制——总结当前对话,然后以摘要开启新会话继续运行。

压缩与提示词缓存的交互方式很容易出错。要压缩对话,必须将完整对话发送给模型以生成摘要。

方案 系统提示词 工具集 缓存命中 成本
独立 API 调用 不同的摘要提示词 ❌ 第一个 token 就分歧 全额未缓存费率
缓存安全型分叉 与父对话相同 与父对话相同 ✅ 前缀完全复用 仅新增 token 计费

最简单的方式是发起一个独立的 API 调用,使用独立的系统提示词(例如"总结以下内容")且不附加任何工具——但这正是成本陷阱所在。你的主对话在一个系统提示词和工具集下被缓存;而总结调用使用不同的系统提示词且没有工具,因此前缀在第一个 token 处就发生分歧,缓存完全无法应用。你最终需要为发送的整个对话支付全额的未缓存输入费率——而且对话越长(即越需要压缩),这一调用的成本就越高。

正确的方案是:缓存安全型分叉(Cache-safe Forking)。执行压缩时,使用与父对话完全相同的系统提示词、用户上下文、系统上下文和工具定义。将父对话的消息前置,然后在末尾追加压缩提示词作为新的用户消息。

从 API 的角度看,这个请求与父对话的上一次请求几乎完全一致——相同的前缀、相同的工具、相同的历史——因此缓存前缀被复用。唯一新增的 token 只有压缩提示词本身。

但这意味着需要预留"压缩缓冲区",确保上下文窗口有足够空间容纳压缩消息和摘要输出 token。

Reasonix 用更严格的约束解决这个问题:前缀哈希在会话开始时计算一次并固定,日志条目按追加顺序序列化、不得修改或重排,草稿区内容进入日志前必须提炼。代价是牺牲了灵活性,换来了缓存的可预测性。


写在最后:缓存不是功能选项,是架构决策

很多团队把缓存当"优化选项":先跑起来,功能实现之后再优化。这条路走不通。提示词的布局、会话状态的管理、工具集的加载策略、子智能体的前缀设计——这些决策一旦做出,改动成本极高。

缓存优化必须在架构设计阶段完成。

Reasonix 的核心观点:

“缓存稳定性不是一个你可以开启的功能,而是整个循环设计围绕的不变量。”

从碰运气到必然命中,需要的不是技巧,是约束。

你愿意为了缓存稳定性,禁止会话中途切换模型吗?愿意为了前缀稳定,放弃消息重排和压缩的自由吗?愿意为了 98% 的命中率,接受 DeepSeek-only 的锁定吗?

这些取舍没有标准答案,但都必须在架构设计阶段做出。等到代码写完再考虑缓存,就太晚了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐