Agent harness 为何显著影响 prompt 缓存与能耗效率
Why harnesses matter for energy (and token) efficiency
Neuralwatt 联合创始人兼 CEO Chad Gibson 基于 2026 年 9-10 月自家云平台流量和 lab 测试数据,比较了 OpenCode、Pi、omp、Kilo Code 与 deepseek-harness 等 agent harness 在 prompt 缓存命中率上的差异。
基于自家云上 2026 年 9-10 月的流量和 lab 数据,对比主流 agent harness 的缓存命中率,并给出可逐项修复的失效成因。
作者:Chad Gibson,联合创始人兼首席执行官
主要发现
- 不同客户端的缓存方式不同。我们观察到各客户端之间有 1.6% 到 11.1% 的输入未被缓存。"缓存"指的是输入令牌中由提示词缓存直接提供而无需重新计算的比例。在缓存命中率为 92% 时,每 100 个令牌中有 8 个未被缓存;而在 97% 时仅有 3 个:预填充工作量约为前者的 2.7 倍。
- 缓存未命中大约会让一次请求的能耗翻倍。这里的未命中指的是提示词几乎或完全没有被缓存的请求(低于 10%)。在 GLM-5.3 上,约为 2.5 倍。
- 深度会话让未命中的代价更高。重读意味着从零开始重新计算提示词的这一部分。超过 128k 令牌后,若一轮对话需要重读其提示词的过半内容,其预填充开销大约相当于该深度下 600 轮普通对话。
- 这些问题大多是可以解决的。我们实验室测试中的每一次缓存中断,都源自客户端修改了它已经发送过的内容,迫使从第一个差异点开始重新读取。
在 Neuralwatt,我们可以看到调用模型的智能体客户端如何影响其能耗。我查看了我们云端一个月的流量,发现客户端似乎决定了我们 GPU 工作量中相当大的一部分。我们通常从模型(活跃参数数量、其注意力机制)或硬件(H200、B200、B300)的角度讨论推理效率。而客户端则值得更仔细的关注。
我们几乎所有的流量都是智能体调用,输入远多于输出。一个典型的编程智能体一轮对话会发送 10 万个以上令牌的上下文,只返回几百个令牌作为回复。大部分输入是上一轮提示词的副本,只在末尾追加一点新内容。如果前面的文本没有变化,服务器已经持有其键值(KV)缓存,只需处理新增部分。这就是提示词缓存,也是我们最大的效率来源。从缓存中提供一个令牌远比为它重新计算要便宜。
缓存要求完全一致的前缀。当客户端修改了它已经发送的内容时,就会发生缓存中断。随后我们必须从第一个差异点开始重新计算全部内容。开头的改动——哪怕只是一个时间戳——都意味着要重新计算几乎整个提示词;而末尾的改动代价则很小。客户端控制着提示词的构建方式。
相同模型,相同价格,不同的缓存命中率
每个请求都会告诉我们是由哪个客户端发出的。在 2026 年 9 月 8 日至 10 月 7 日的 30 天里,我们观察到数百个不同的客户端,包括 OpenCode、Oh My Pi(omp)和 Pi 等开源编程智能体。其中还包括编辑器集成、OpenAI SDK 以及自定义应用。仅 OpenCode 一家就被我们大约四分之一的客户使用。
对于使用最广泛的客户端,以下是我们无法从缓存中提供的输入令牌比例:

这里的每个客户端都缓存了其绝大多数输入。这些小差异仍然重要:缓存命中率为 97% 的客户端每 100 个令牌只给我们发送 3 个未缓存令牌;而命中率为 92% 的则发送 8 个。对于相同的对话,后者的预填充工作量是前者的 2.7 倍。
命中率也取决于工作负载,而不仅仅是工具链。较长的会话得分更高,因为每轮新增的文字只占整个提示词的较小比例,而 deepseek-harness 的领先优势约有半数来自运行超长会话的用户。更干净的衡量工具链方式是看有多少轮次会破坏缓存:deepseek-harness 大约是每 100 轮中 1 轮,而 OpenCode 和 Pi 大约是每 100 轮中 3 轮。我们在流量中同时看到 OpenCode 1.x 和 2.x,所以这里的指标把两者合并在一起。2.x 单独看效果更好(未缓存率为 2.2% 对比 3.0%),这主要归因于模型组合。在同一模型上,它们的缓存效果基本相当。我们在实验中修复了 2.x 中发现的绝大多数 1.x 缓存破坏问题。这种差异在生产环境中没有体现,我们尚不清楚原因。
一次未命中在能耗上的代价
我们按实际测量的能耗计费,因此可以直接看到成本。在 2026 年 10 月 1 日至 7 日这一周,我们对比了同一模型、相同提示词大小(约 50k tokens)以及相近输出长度下的请求。以下是每个请求相对于完全缓存请求所多消耗的能耗:

一个几乎未缓存的请求,其能耗大约是同等大小已缓存请求的两倍。在 GLM-5.3 上更接近两倍半。预填还要与生成竞争同一批 GPU,因此浪费的预填会拖慢该服务器上其他所有人的响应。
会话深处缓存未命中代价高昂
我们在 2026 年 10 月 6 日至 13 日这一周,按轮次跟踪了三个最繁忙模型上的各个会话。我们查看了所有在距离前一条请求 5 分钟内落到同一服务器、且缓存本应处于热状态的轮次。大多数缓存破坏规模很小:只是提示词末尾的几千个 tokens 被重新计算。
在上下文超过 128k tokens 后,每 1000 轮中约有 3 轮会从头重读一半以上的提示词。这些极少数的轮次却占到了该深度下全部重读 tokens 量的 75%。每轮平均重读约 230k tokens。在该深度下,一次普通轮次平均仅新增约 370 个 tokens,因此其中任何一次的成本就相当于大约 600 次普通轮次的预填量。
延迟也相当明显。以下是一次普通轮次与一次必须重读大部分提示词的轮次的首 token 中位时间对比:

比起缓存破坏的频率,我更关注它在会话中发生的位置。早些时候对提示词开头做一次修改,代价很小。但若在一次长会话的第 200 轮这样做,你就会看到一次显著的、可感知的停顿,以及高昂的能耗成本。长智能体会话占据了我们的绝大部分 tokens 消耗,所以这一问题会不断累积。
我们从服务端并不总能区分是工具链修改了提示词,还是缓存在内存压力下被驱逐。这些数字把两种情况都包含在内。我们在下面展示的实验表明,工具链造成了我们能够复现的那些对提示词开头部分的修改;而驱逐可能解释了其他部分的原因。
是什么破坏了缓存
我从两个地方查找原因:实验室——可以看到每一个 token;以及生产环境——只能看到请求元数据。
在实验室
我们通过一个录制代理对四个流行的开源代理(OpenCode、Pi、omp 和 Kilo Code)针对 DeepSeek V4.1 Flash 和 GLM-5.3 进行了测试。我们使用的是截至 10 月 6 日的最新发布版本,均采用默认设置:OpenCode 1.18.34、Pi 1.0.4、omp 18.6.3 和 Kilo Code 7.8.3。随后我们又在 2.x 系列的 OpenCode 2.0.29 上重复了该会话。每个代理都在同一个小型 Python 项目上按同一脚本化会话依次进行:先 plan,再 build,切换推理力度再切回,派生一个 sub-agent,保持空闲,被重启后恢复会话,连接和断开一台 MCP 服务器,添加一项 skill,并持续运行足够长的时间以触发压缩。我们把每一条请求与紧邻的前一条进行比较,既对比 JSON,也对比服务端实际渲染出的 token。
在同一次会话中跟在之前某条之后的 539 条请求里,88% 是纯粹的追加:上一条提示词恰好是新提示词的前缀。另外那 65 条则破坏了缓存。每一次中断都来自框架修改了它此前已经发送过的内容;没有一次来自我们的对话模板。在全部四个代理中,空闲时间都没有引起缓存中断。重启框架再恢复会话、或使用 sub-agent,也没有引起中断。
有两种情况,即便用户没有任何操作,缓存也会被打破:
- OpenCode 的 plan 模式提示。一旦你用过 plan 模式,OpenCode 就会把一条提示("Your operational mode has changed from plan to build")附加到每一条新的用户消息上。在下一轮中,它又会从那条消息里移除掉这条提示。于是每一轮都要把整个上一轮重新读取一遍,包括其中的工具调用和结果。在我们短小的会话里,每一轮因此重读了 330 到 2,340 个 token。这一开销会随轮次规模的增大而增长。OpenCode 2.x 把这条提示保留在它自己的一条消息里,因此不再触发重读。
- omp 对重读文件的剪枝。当某个文件被第二次读取时,omp 会用
[Superseded by a newer read of this file]替换掉历史记录中较早的那一份。在我们的运行中,那一处编辑落在提示词 62% 的位置,导致 15,520 个 token 中有 5,846 个被迫重读。omp 还会剪掉 "uneventful" 的工具结果,并且在空闲 90 分钟后,它会在任意深度做剪枝。把compaction.supersedeReads: false和compaction.dropUseless: false设上即可保住缓存。
Pi 和 Kilo Code 没有出现此类中断。
其余原因则取决于用户的行为。其代价大小取决于提示词的改动落在何处:
- 切换推理力度。两套模板都把力度级别写在提示词的最开头几个 token 里:GLM-5.3 上是
Reasoning Effort: Max,DeepSeek V4.1 上则是 1 到 100 之间的一个数字。两个模型上首个差异都出现在第 8 个 token,因此切换一次就会重读一切内容。只有那一个值发生了变化;它的长度以及其后所有内容都保持不变。 - 连接或断开一台 MCP 服务器。OpenCode 1.x 和 Kilo Code 会按字母顺序把新工具插入到工具列表的中间。OpenCode 2.x 不动工具列表,只是追加一条备注,因此连接和断开都是纯粹的追加。Pi 会增加一项工具并修改它的系统提示。omp 则是在系统消息末尾追加一段。在 GLM-5.3 上,工具列表是先于系统提示渲染出来的,因此改动会落到很靠前的位置:对 OpenCode 1.x 而言大约在第 1,300 个 token 处。
- 添加一项 skill。四者都在系统提示中列出 skill。首个差异在 DeepSeek V4.1 上落在第 461 到第 2,948 个 token 之间;在 GLM-5.3 上则出现得更晚,因为该模型把工具放在了前面。
- 日期变更。 OpenCode 1.x 和 Kilo Code 会把今天的日期放进系统提示中,大约在第 2,000 个 token 处。OpenCode 2.x 保留会话的原始日期,并在日期变化时追加一条说明。omp 把它放在第一条用户消息里。Pi 不发送日期。对于会覆写日期的客户端而言,跨过当地午夜意味着几乎所有内容都要重新读取一次。
- 压缩。 每个 harness 的做法都不一样。Pi 会逐字保留最后约 ~20k 个 token,但重写开头附近的摘要。提示几乎不缩减,其中大部分无论如何仍会被重新读取。在同样的 40k token 窗口下,OpenCode 2.x 压缩了三次,总共重新读取了 67,850 个 token。1.x 压缩了一次,重新读取了 29,776 个。
切回到会话早先用过的一个设置比预期的更便宜。服务器仍保留着当时那个版本的提示。在我们的运行中,把 effort 切回会命中 10.6k 到 27.5k 的缓存 token。一个开关每次在新状态首次使用时产生一次完整重读,而不是每次切换都算一次。
在生产环境中
我们不记录也不保留客户提示词或回复的文本。本节的所有内容都来自请求元数据。上文的 token 级分析来自我们自己的脚本化测试会话,而非客户流量。该元数据记录了每次请求所要求的推理 effort 以及所发送的工具数量。我们用它针对 2026 年 10 月 6 日至 13 日在 DeepSeek V4.1 Flash、GLM-5.3 和 GLM-5.3 Flash 上的真实会话核查其中两个原因。我们采用了同一服务器上五分钟以内、至少 8k token 的连续轮次。为了避免少数重度账号左右结果,我们在每组中剔除了重新读取 token 数最多的三个客户。

变更工具列表或 effort 等级使缓存失效的可能性提高到 20 倍以上,平均代价约为前一个提示的 40%。工具列表的变更只出现在约 0.5% 的轮次上,却在这些模型上占全部重新读取 token 数的 19%。这部分占比相当集中:三个账号就贡献了大约三分之二。逐轮效果在其余用户身上依然成立。
其余四分之三的重新读取 token 出现在 effort 和工具数量均未变化的轮次上。这个「我们看不出来」的桶里装着那些从服务器侧无法观察到的原因:OpenCode 1.x 中的提醒、omp 的裁剪、技能、日期、压缩与缓存淘汰。接下来,我希望我们能记录下每个提示与上一次的第一个差异点。这样就能在不阅读任何提示文本的情况下把这个桶拆开。
deepseek-harness 如何保持缓存完好
在我们的实验室采集中,deepseek-harness 的每一条后续请求都是对前一条的纯追加,即便进程被重启、会话被恢复也是如此。读了它的源码后,我发现了几项值得参考的做法:
- 请求由仅追加的会话日志重建。重试与恢复会产生字节级完全一致的提示。
- 系统提示固定且精简。其中不含日期、git 状态与操作系统。它只写一次,之后不再改动。
- 状态变更放在历史之后。当审批策略或可用技能发生变化时,harness 会追加一条简短快照,说明它替代了更早的内容。它不改动系统提示和更早的消息。
- 工具列表在各模式下始终保持排序并完全一致。通过叙述性的指令来约束各模式,而不靠增删工具。
- 压缩发生得很晚。摘要调用会复用已有的前缀,因此即使是压缩也基本是命中缓存。那些把对话记录重新序列化为单条新消息的摘要器则是冷启动。在我们见过的最大此类负载中,这些调用的命中率只有 22%。
- 每处变更都会被审查对缓存的影响。仓库中的每个包都带有一小段"KV 缓存效应"章节。
你可以做的事
如果你构建或配置一个 harness:
- 每一轮都让提示的开头保持完全一致。把新消息和工具返回的结果加在对话末尾。对设置的更改也在那里描述,不要改动前面的文本。
- 不要把日期、git 状态或任何会变化的内容放进系统提示。
- 保持工具列表稳定且已排序。把新工具加在末尾。
- 把压缩延后,并让摘要调用复用对话的完全一致的前缀。
- 发送一个稳定的会话 id。在 Neuralwatt 上,
X-NW-Conversation-ID让我们能把会话保留在已经持有其缓存的服务器上,即使跨压缩也能保持。
如果你使用 harness:
- 为每个会话选定一个推理力度,而不是在会话中途切换。
- 对于 omp,考虑
compaction.supersedeReads: false。 - 在会话变长之前就确定好力度、MCP 服务器和技能等设置。缓存断裂伤害最大的正是会话变长之后。
在你自己的会话中查看
本文中的一切都来自我们对每个请求记录的数据,你也可以看到你自己工作的同样数据。Neuralwatt 仪表盘中的 Session Insights 会逐轮显示每个 agent 会话:每个提示中有多少来自缓存,有多少必须冷启动计算,首 token 时间,以及测得的能耗和成本。它永远不会显示提示或补全的文本。

这是我自己在 2026 年 9 月 26 日的一次会话:在 DeepSeek V4.1 Flash 上跑的一个编码 agent 的 35 轮,上下文从 29k token 增长到 257k token。它 95% 的提示 token 来自缓存,那些小的红色帽状标记是每轮新增的文本。缓存断裂表现为高高的红条,所以如果你在会话中途切换推理力度或接入一个 MCP 服务器,你可以看到它付出了多少代价。你可以用它来比较你自己负载下的 harness 或设置,并检查某项变更是否真的让缓存保持温热。
我们这一侧正在做的事
我们会把每个会话路由回持有该缓存的服务器。在我们最忙的模型上,这已经能让每个会话 88–96% 的 token 留在其归属服务器上。当会话不得不迁移时,我们共享的 KV 缓存池让新服务器可以直接加载前缀,而不是重新计算。
我们最近测算了在现有基础上,更智能的前缀感知路由器还能额外挽回多少:命中率再提升 0.1 到 0.2 个百分点。剩余的浪费大部分来自提示的构建方式。
接下来,我们计划把修复和推荐的默认值上游推送到客户使用最多的开源 harness。我们也想研究在提示开头附近的小幅修改(例如力度行或日期)之后,让服务器复用已缓存工作的方法。这对我们无法自行修改的客户端会有所帮助。
为什么这值得衡量
缓存断裂不会以错误的形式出现。你仍然会得到答案,只是更晚,并且为之消耗了更多能量。对每个请求能耗的测量让我们看清了这些浪费累加起来有多少。其中大部分最终可以通过一项设置或对 harness 构建提示方式的小改动来修复。
AI 依靠能源运行。我们能看到的越多,就能避免越多的浪费,无论对象是单个开发者的会话,还是行业的下一个数据中心。
方法说明。我们通过每个请求 User-Agent 头中的名称来识别客户端。SDK 和运行时标签(如 Node.js 应用、OpenAI SDK 应用)涵盖许多不同的应用程序。我们仅展示被大量客户使用的客户端,绝不识别具体客户。服务器以固定大小的块来缓存提示,因此即使是纯粹的追加操作,也可能在上一次对话末尾留下几个需要重新读取的标记。我们将其计入未缓存的输入,在图 1 的编码智能体中增加了不到 0.1 个百分点。缓存率是 2026 年 9 月 8 日至 10 月 7 日期间所有模型从缓存中提供的提示标记所占的比例。能源以分配到每个请求的 GPU 能源来衡量;缓存与未缓存的比较保持提示大小和输出长度不变,覆盖 2026 年 10 月 1 日至 7 日。深度会话和原因分析涵盖 2026 年 10 月 6 日至 13 日,针对 DeepSeek V4.1 Flash、GLM-5.3 和 GLM-5.3 Flash,追踪同一服务器上五分钟内每个会话的连续对话轮次。
Chad Gibson 是 Neuralwatt 的联合创始人兼 CEO。他曾在微软任职超过 20 年,之后转向早期初创公司投资,并于 2024 年联合创立了 Neuralwatt。
来源:Hacker News · neuralwatt.com