为什么你的 Agent 这么贵:Prompt Cache 命中率为 0 的排查记录¶
前段时间我把 DotCraft 接到工作流里跑任务,遇到了一个非常直观的问题:一个任务明明只跑了几分钟,账单却接近 50 美刀。
更离谱的是,我去后台看了一眼:cache hit rate = 0。
这意味着 Agent 每一轮工具调用,都在把同一份 System Prompt、工具 Schema、历史消息重新付费。对于普通 ChatBot 来说,这可能只是贵一点;但对于一个会连续调用几十次工具的 Agent 来说,这就是一台非常稳定的烧钱机器。
后来我在 DotCraft 里补了一套 Trace,把 System Prompt、Tools Schema、History Prefix 都打上 Hash,一轮一轮地查,才发现真正破坏 Prompt Cache 的往往不是模型,而是 Agent Harness 里几个很常见、也很容易被忽视的设计习惯。
这篇文章主要记录一下这次排查过程,也顺便聊聊一个我最近越来越强烈的判断:
Agent 的核心不是会不会 Function Call,而是 Harness 如何控制上下文、成本、权限、状态和可观测性。
如果你正在做 Coding Agent、MCP 客户端、企业内部 Agent,或者只是觉得“为什么我的 AI 应用这么贵”,这篇应该会比较有参考价值。
先说结论¶
Prompt Cache 优化不是玄学,也不是单纯换个便宜模型就能解决的问题。对于 Agent 来说,真正要控制的是输入前缀的稳定性。
我这次主要踩到了几个坑:
- Plan 模式切换时改了 System Prompt 和工具列表,导致前缀直接变化。
- 时间戳、当前模式、用户实时状态被塞进 System Prompt,导致每一轮都像新请求。
- AGENTS.md、MEMORY.md、Skill 索引实时刷新,导致系统提示词持续抖动。
- MCP 工具启用后动态刷新,导致 Tools Schema 变化。
- 上下文压缩、记忆整理、分支会话如果设计不好,也会把原本可以复用的前缀缓存打碎。
对应的解决思路也很简单粗暴:
- 能固定在 System Prompt 里的东西就固定,不要每轮变。
- 动态信息尽量追加到 User Message 后面,而不是塞进系统提示词。
- 工具列表不要随便变,模式限制交给 Policy 层兜底。
- 低频变更内容做快照,不要每轮实时刷新。
- 对 System Prompt、Tools Schema、History Prefix 做 Hash 和 Trace,没有观测就没有优化。
下面是 DotCraft 里应用侧统计的 Usage 视图。Prompt Cache 这类问题如果只靠体感,很容易变成“感觉好像便宜了”;但对 Agent Harness 来说,最后一定要落到可观测数据上。
起因:一个几分钟烧掉 50 美刀的任务¶
笔者从二月份 Claw 类 Agent 助手兴起后,开始设计开发 DotCraft——一个以 .NET 技术栈、C# 为主要语言的 AI Agent。
它最初只是我自己的 Claw 助手和群机器人,后来随着不断迭代,逐渐吸收了很多主流 Coding Agent 的功能,也开始成为我游戏开发工作里的主力工具。因为拓展性比较强,工作中的一些 Agentic 业务也逐步基于这套 Agent Harness 来做。
最初把 DotCraft 接到工作开发场景时,用的是公司的 Claude Opus 4.7 API。模型本身很强,效果也确实不错,但跑了几次之后账单就有点不对劲了。
一个任务只跑了几分钟,接近 50 美刀。
虽然没有之前流传的“某游戏公司一夜烧掉价值几百万的 Token”那么夸张,但也足够让我停下来重新审视整个 Harness 的设计。于是我去后台看缓存命中情况,结果发现 cache hit rate 竟然是 0。
这就很难绷了。
大部分大模型厂商都会把输入 Token 按“缓存命中”和“未命中”分开计费。按我当时接触到的计费规则,缓存命中的部分通常会便宜一大截;具体比例每家厂商、每个模型、每种协议都不一样,最终还是要以实时文档为准。
但核心结论很清楚:对于 Agent 来说,缓存打不中就是贵。
因为 Agent 早就不是 ChatGPT 3.5 时代那种一问一答的聊天机器人了。现在的 Agent 往往会连续调用几十次工具,甚至上百次工具。每一次工具调用之后,客户端都要把历史消息、工具结果、系统提示词、工具 Schema 重新发给模型。
如果这些重复前缀能被缓存命中,成本会下降很多;如果每轮都打不中,那就是每一轮都在重新付费。
所以这次排查的目标也很明确:不是先去换便宜模型,而是先搞清楚 DotCraft 为什么完全吃不到 Prompt Cache。
为什么 Agent 对 Prompt Cache 特别敏感¶
首先要说明一下 Agent 的基本循环。
Agent 本质上就是 LLM + Function Call 组成的循环:用户发消息,LLM 输出消息;如果 LLM 调用了工具,客户端就执行工具,再把历史消息和最新工具结果发回 LLM,如此往复。
这里容易被忽略的一点是:我们平时说的“一轮对话”,和 Agent 内部真正发给模型的“一轮请求”,不是一回事。
用户可能只是问了一句话,但 Agent 内部可能经历了:
- 模型思考并调用搜索工具。
- 客户端返回搜索结果。
- 模型读取文件。
- 客户端返回文件内容。
- 模型修改文件。
- 客户端返回修改结果。
- 模型继续验证。
- 客户端返回验证结果。
用户看起来只说了一句话,底层可能已经跑了很多轮。
而每一轮请求里,除了上一轮的输出和历史消息之外,通常还会带上两块非常大的东西:
- System Prompt:包括应用内硬编码的规则,也包括用户可编辑的项目上下文,例如 CLAUDE.md、AGENTS.md 等。
- Tools Schema:包括所有内置工具和 MCP 工具的 JSON Schema。
于是问题就来了:如果 System Prompt 或 Tools Schema 每一轮都在变,那么输入前缀就稳定不下来。
Prompt Cache 的核心并不复杂,至少从应用开发者视角看,可以先粗暴理解成一句话:
前缀越稳定,越容易命中;前缀越抖,越容易烧钱。
缓存到底是怎么碎掉的¶
那是不是只要保证每轮输入前缀一致,就一定能命中缓存?
当然不是。真正的缓存命中逻辑在大模型后端实现,不同厂商作用到推理端 KV Cache 的方式也不完全一样。应用开发者不需要关心所有底层细节,但至少要理解协议层面的几个约束。
以 OpenAI Chat Completions 这种比较典型的隐式缓存为例,开发者不需要手动指定缓存断点,后端会对输入消息数组做前缀匹配。只要后续请求不修改数组前面的内容,前缀在大多数情况下就有机会复用。
但这只是理想情况。实际落地时还有两个很常见的干扰因素。
第一个是路由问题。第 N 轮请求的缓存可能写到了 A 集群,但第 N+1 轮被路由到了 B 集群,自然就读不到前面的缓存。
第二个是时效问题。缓存本质上占用推理资源,尤其是 GPU 显存,各家厂商都会设置 TTL。超过时间之后,缓存自然会失效。
所以厂商会提供一些自己的协议能力来解决这些问题。比如 Codex 使用 OpenAI Responses 协议时,除了隐式缓存之外,还会通过一些请求参数和关联信息维持路由黏性;Anthropic 则支持更显式的 cache control 和更长 TTL,但对应会带来额外的缓存写入成本。
这些是厂商层面的能力。下游应用开发者真正需要控制的,是自己的请求前缀到底稳不稳定。
我先给 DotCraft 补了一套 Trace¶
一开始看到 cache hit rate = 0 的时候,我其实也不确定问题出在哪里。
可能是厂商路由问题,可能是 TTL 问题,也可能是 DotCraft 自己把前缀弄碎了。只看账单和命中率是没法定位的,所以第一步不是优化,而是补观测。
DotCraft 里我主要做了两件事。
第一,对每次发给 LLM 的 Request,把 System Prompt 和 Tools Schema 分别计算 Hash。只要某一轮 Hash 变了,就能立刻知道是系统提示词变了,还是工具列表变了。
第二,根据不同协议的返回包统计缓存写入和读取情况。Anthropic 这类协议会给相关字段,OpenAI 某些协议则没有那么直接,所以需要应用侧做更多归因。
这里还有一个很多人容易误解的点:缓存命中不代表命中率一定是 100%。
命中率和当轮输入里“可复用前缀”的占比有关。比如上一轮只写入了 100 Token 的缓存,而上一轮输出了 900 Token,那么当前轮理论最大命中率也只有 10%。再比如用户每次都开新会话,只问一个问题就走,那么永远处于冷启动,理论命中率就是 0%。
所以如果一个项目只说“缓存命中率稳定 98%、99%”,但不说会话长度、冷启动比例、上下文压缩策略、输入输出占比,那这个数字其实没有太多参考价值。
相比之下,DotCraft 这边主打一个不躲、不藏、不绕、不逃,稳稳地把应用侧数据都摆出来。哈哈。
下面是 DotCraft 的 Dashboard Trace。可以看到用户发送 User Message 后,模式信息和时间戳被追加在它后面,而不是塞进最前面的 System Prompt。
真正破坏缓存的几个设计¶
有了 Trace 之后,问题就很好查了。DotCraft 老版本里确实有不少会破坏缓存的设计,其中有些在 OpenClaw 的早期版本里也能看到,这也是当时很多使用者对它的核心“评价”——贵。
1. Plan 模式切换时改 System Prompt 和工具列表¶
这是最典型的问题。
很多 Agent 都会有 Plan / Act 之类的模式。Plan 模式下只允许规划,不允许改文件;Act 模式下才允许执行修改。
最直接的实现方式是:进入 Plan 模式时,改 System Prompt,移除文件写入工具;进入 Act 模式时,再把 System Prompt 和工具列表换回来。
这听起来很合理,但从 Prompt Cache 角度看就是灾难。因为 System Prompt 一变,前缀变了;Tools Schema 一变,前缀也变了。
更好的做法是:在 System Prompt 里固定写好各种模式的行为准则,然后在当前 User Message 后面追加一段当前模式信息,告诉 Agent 现在处于 Plan 还是 Act。工具层面不要频繁改 Schema,而是由 Policy 层兜底。如果 Plan 模式下 Agent 误用了文件写入工具,客户端直接返回错误和引导信息。
这样行为上仍然能限制 Agent,但前缀不会每轮乱跳。
2. 把时间戳塞进 System Prompt¶
第二个坑更隐蔽:把用户实时信息放进 System Prompt,比如当前时间、当前工作目录状态、当前模式、临时环境信息等。
这些信息看上去像“系统上下文”,但它们的问题是变化太频繁。一旦放在 System Prompt 里,就会导致系统提示词每轮变化,缓存自然很难命中。
我的策略是:实时信息追加到 User Message 后面。
这类信息本来就是“本轮请求的动态上下文”,不应该污染稳定前缀。
3. AGENTS.md、MEMORY.md、Skill 索引实时刷新¶
很多 Coding Agent 会读取项目里的 AGENTS.md、CLAUDE.md、MEMORY.md,或者维护一套 Skill 索引。这些内容确实很重要,但不能因此每一轮都重新刷新进 System Prompt。
尤其是 MEMORY.md 这种低频变更内容,如果每轮实时读取并拼进系统提示词,等于每轮都在赌它不变。一旦有后台任务更新了记忆,或者索引顺序变化,缓存就会被打碎。
DotCraft 后来对这类内容做了分页和快照管理。低频变更内容保留快照,只在上下文压缩之后、TTL 失效之后,或者明确需要刷新时才更新。
换句话说,Agent 需要记忆,但记忆不能成为每轮请求里的随机扰动源。
4. MCP 工具实时刷新¶
MCP 很好用,但 MCP 工具多了之后,Tools Schema 会变得非常大。
如果 MCP 工具启用后还会动态刷新,或者每轮都重新枚举工具,那么 Tools Schema 的顺序、描述、参数都可能发生变化。哪怕只是顺序变了,对前缀缓存来说也可能是一次破坏。
DotCraft 里为 MCP 工具增加了 Deferred Loading,目前主要支持 OpenAI Responses 协议。核心思路是:基础工具稳定放在前缀里,增量工具通过延迟加载进入上下文,尽量不要破坏已有前缀缓存。
这也是我现在对 MCP 的一个看法:MCP 不是越多越好。对于 Agent Harness 来说,工具数量、工具加载时机、工具权限、工具 Schema 稳定性,都应该被当成工程问题来设计。
更高级的缓存命中策略¶
前面这些只能算 Agent Harness 的基础优化。如果继续往 Claude Code、Codex 这类更成熟的 Harness 看,会发现 Prompt Cache 还有一些更高级的玩法。
显式缓存控制¶
Anthropic 协议支持显式 cache control,可以提供多个 Cache Point 断点,让应用侧更灵活地做分段缓存。
它最直接的好处是“缓存回退”。
假设历史会话发生了变化,比如上下文压缩之后,完整历史已经不一样了;但如果前面的“系统提示词 + 工具 Schema”有一个稳定断点,那么后续请求仍然可以复用这个断点之前的缓存。
对于长上下文 Agent 来说,这个能力非常关键。因为 Agent 不只是要缓存“完整历史”,更重要的是缓存那些昂贵、稳定、重复出现的基础前缀。
上下文压缩如何复用前缀¶
上下文压缩也是 Agent Harness 里非常重要的一环。
Codex 的上下文压缩走的是一个特殊的 Compact API,具体实现比较黑盒。笔者推测它应该也用了类似显式缓存断点的设计,尽量复用“系统提示词 + 工具 Schema”这部分稳定前缀。
另一种思路是 Claude Code 那种 Fork 分支会话:从主会话复制系统提示词、工具 Schema 和历史消息,再追加一条 User Message,让 LLM 生成 Summary。这样分支会话也能复用主会话前缀缓存。
DotCraft 采用的是类似 Claude Code 的方案,并且把这套策略用在了记忆整理功能上:主会话继续工作,后台 Fork 一个分支会话完成 MEMORY.md 和 HISTORY.md 的总结与写入。
对于无法复用前缀的冷启动场景,例如应用重启之后,DotCraft 会退回 Legacy 方案:直接修改系统提示词、移除工具 Schema、过滤工具输出,用更低成本的方式生成上下文 Summary。
这里没有银弹,只有取舍。
缓存预热¶
还有一种策略是缓存预热。
前面提到,用户发 Request 时会有一个冷启动窗口。这段时间里缓存还没有写好,无法立刻读取。预热的思路就是:应用先行发送“系统提示词 + 工具 Schema”来写入这部分前缀缓存,等用户真正发请求时就能直接命中。
这在大规模并行 Agent 场景里会更有价值。比如多个会话共享同一套 System Prompt 和工具 Schema,就可以复用同一份预热后的前缀缓存。
当然,预热本身也有成本。它适合高频、稳定、可复用的场景,不适合用户随便开一个一次性会话就无脑预热。
中途插入系统提示词¶
写这篇文章时恰逢 Claude Opus 4.8 发布。抛开那些颇为唬人的 Benchmark 不谈,这次更让我感兴趣的是:它支持把系统提示词随时追加到历史会话之后。
这个能力对 Agent Harness 很有价值。
还是拿 Plan 模式切换来说,过去如果想强约束 Agent 行为,很多实现会选择修改最初的 System Prompt。但这样会破坏稳定前缀。现在如果协议支持中途追加系统提示词,就可以在切换模式之后直接追加一段新的系统级约束,而不是把所有模式规则都 hardcode 在最初的系统提示词里。
这会让 Harness 的行为控制更灵活,也更省 Token。
不过这类能力往往和厂商后训练、协议设计、模型行为有关,不是简单照搬 API 字段就能完全复刻的。所以我更倾向于把它看成厂商专属优化,而不是通用方案。
回到 Agent Harness:省钱只是表象¶
这次 Prompt Cache 排查之后,我对 Agent 开发的一个感受更强烈了:
Agent 的价值不在“会不会调用工具”,而在 Harness 能不能把工具调用变成可控、可观测、可迭代的工程系统。
Function Call 本身并不复杂。真正麻烦的是这些问题:
- 上下文怎么组织,才能既有信息量,又不无限膨胀?
- System Prompt 哪些内容应该稳定,哪些内容应该动态?
- 工具 Schema 怎么加载,才能既好用,又不污染前缀?
- 模式切换、权限控制、错误恢复,应该由 Prompt 管,还是由 Policy 管?
- 记忆系统什么时候读,什么时候写,什么时候压缩?
- 出了问题之后,能不能通过 Trace 快速定位,而不是靠猜?
Prompt Cache 只是其中一个切面。它看起来是成本优化,但背后其实是 Agent Harness 的架构问题。
游戏开发里我们很熟悉这种事。性能优化从来不是最后加一个开关就能解决的,它会反过来影响渲染管线、资源组织、场景结构、工具链和工作流。Agent 也是一样:成本、上下文和工具系统如果一开始不设计好,后面就会变成到处漏水。
最后给一份检查清单¶
如果你也在做 Agent,可以按这个清单过一遍:
- System Prompt 是否每一轮都完全一致?
- 当前时间、当前模式、用户状态是否被错误地塞进 System Prompt?
- Tools Schema 是否会因为模式切换、MCP 刷新、工具顺序变化而抖动?
- AGENTS.md、MEMORY.md、Skill 索引是否每轮实时刷新?
- 是否对 System Prompt 和 Tools Schema 分别打 Hash?
- 是否区分了缓存写入、缓存读取、冷启动窗口和 TTL 失效?
- 上下文压缩后,是否还能复用“系统提示词 + 工具 Schema”的稳定前缀?
- Plan / Act 这类模式限制,是通过修改工具列表实现,还是通过 Policy 层兜底?
- MCP 工具是否支持按需加载,而不是一次性把所有工具塞进上下文?
- 是否有 Dashboard 或 Trace 能解释每一轮为什么贵?
如果这些问题都答不上来,那 Prompt Cache 命中率低大概率不是厂商的问题,而是 Harness 还没有长出自己的控制面。
结语¶
Agent Harness 里的很多东西,本质上都是工程上的优化手段。当然,中间也少不了一些有趣的奇技淫巧,哈哈。
我越来越觉得,现代 Agent 的通用形态会逐渐沉淀下来:上下文管理、工具加载、权限控制、记忆系统、Trace、缓存策略、压缩策略,最后都会变成 Harness 的基础设施。
所以现在造一个 Agent,理论上确实可以“照葫芦画瓢”;但真正有价值的不是把 Function Call Loop 跑起来,而是让它在具体业务里稳定、便宜、可控地创造价值。
笔者在设计 DotCraft 时,也更看重拓展性和架构设计。与其跟风堆功能,不如在真实业务里持续打磨这些基础能力。
最后欢迎小伙伴们 Star、Fork 和 Contribute ~









