乐于分享
好东西不私藏

App Agent (3)——为什么Token少了33%,账单却涨了76%

App Agent (3)——为什么Token少了33%,账单却涨了76%

一、状态驱动是 Agent Loop 的基础

Agent Loop 本质上是一个持续观察环境并采取行动的过程。

每一次操作都可能改变页面状态:页面跳转、弹窗出现、列表刷新、按钮状态变化,甚至只是一个 Toast 提示。

因此 Agent 无法像传统函数调用一样预先规划完整路径,而必须在每次操作后重新观察当前页面,再决定下一步动作。

Observe → Act → Observe

无论是基于截图的视觉方案,还是基于 ViewTree、Accessibility 的结构化方案,本质区别只是观察方式不同,控制闭环是一致的。

因此,操作后的页面状态必须返回给模型。它是 Agent 能够持续推进任务的前提。

二、问题开始出现在状态的累积

状态返回本身没有问题。

问题在于,多轮 Agent Loop 会不断产生新的页面状态,而这些状态会持续堆积在上下文中。

以一个简单的点餐任务为例:

首页→ 搜索页→ 商品列表→ 商品详情→ 购物车

当 Agent 已经进入购物车页面时,决策真正依赖的通常只有当前页面。

但上下文中仍然保留着此前所有页面快照。

首页快照搜索页快照商品列表快照商品详情快照购物车快照

这些历史页面记录了任务轨迹,却很少参与后续决策。

统计线上任务后发现,一个 8~10 轮的典型流程中,历史页面状态约占每轮 Input Token 的 25%。随着任务长度增长,这部分比例还会持续上升。

于是一个看起来非常合理的优化思路出现了:

保留最新状态,压缩历史状态。

从 Token 视角看,这几乎是一个没有副作用的优化。

而后面的故事,也正是从这里开始的。

三、第一次优化:压缩历史状态

既然 Agent 的决策只依赖当前页面,那么历史页面是否还有必要保留完整内容?

基于这个判断,我实现了一个 Context Compactor Middleware。

原则很简单:

  • 最新页面状态保留全文;
  • 历史页面状态压缩为摘要;
  • 保留关键跳转信息,删除可从后续状态重新推导的页面细节。

例如,一条完整页面状态:

Clicked at (143, 559). New page state:Environment: content_feedScreen: SearchResult## Page Structure- Items: 12- Scrollable: 1 region## Visible Entities- 星巴克- 瑞幸...## Available Actions- click(星巴克)...

压缩后变成:

[compacted]click: Clicked at (143,559)Screen: SearchResult

在我的 App Agent 场景中,这种压缩几乎没有观察到行为退化。

因为页面结构、实体列表、候选动作等信息,已经包含在后续页面状态中;对于历史页面而言,它们更多只是执行轨迹,而非当前决策依据。

统计结果也印证了这一判断:

指标
优化前
优化后
单任务累计 Input
165k
110k
降幅
33%

无论从上下文长度还是 Token 消耗看,这都是一次相当成功的优化。

直到我看到账单。

四、KV Cache 改变了优化目标

最初我使用的是一个非常朴素的成本模型:

Token 越少 成本越低

这个模型在 KV Cache 出现之前基本成立。

但 KV Cache 普及之后,成本模型已经发生变化:

成本 ≈ 未命中的 Token

而不是:

成本 ≈ 总 Token

问题恰恰出在这里。

Compactor 真正做的并不只是压缩历史,而是在每一轮调用前改写历史消息。

例如:

Tool₁ → Tool₁'Tool₂ → Tool₂'Tool₃ → Tool₃'

而主流 Provider 的 KV Cache(OpenAI、Anthropic、DeepSeek等)都依赖相同原则:

Prompt 前缀必须逐 Token 保持一致。

一旦前缀中的任意位置发生变化,从该位置开始的缓存全部失效。

于是 Compactor 每运行一轮,都会制造新的 Cache Miss:

Round 3[Sys, Hum, AI₁, Tool₁', AI₂, Tool₂, AI₃, Tool₃]                   ↑Round 4[Sys, Hum, AI₁, Tool₁', AI₂, Tool₂', AI₃, Tool₃, AI₄, Tool₄]                               ↑

历史消息不断被改写,前缀不断被击穿。

最终得到一个极其反直觉的结果:

Raw Input
Cache Hit
实际计费
启用 Compactor
62k
52%
30k
保持原样
95k
82%
17k

Input Token 减少了 33%。

实际账单却上涨了 76%。

原因也很简单:

Compactor 省掉的那部分 Token,本来已经被 KV Cache 以极低成本服务;而历史改写导致的大量 Cache Miss,反而让原本接近免费的前缀重新参与计费。

从这个案例开始,我意识到一件事:

KV Cache 出现后,Agent 的优化目标已经发生变化。

很多看起来正确的 Context Engineering,在新的成本模型下可能恰好是反优化。

五、KV Cache 改变了 Agent 的优化目标

这次踩坑最大的收获,不是发现了一个错误优化,而是发现 Agent 的成本模型已经发生变化。

过去做 Context Engineering,目标很简单:

Context Engineering = 减少上下文

因为在没有 Cache 的世界里:

成本 ≈ Token 数量

Token 越少,成本越低。

但 KV Cache 普及之后,情况发生了变化。

对于多轮 Agent Loop 而言,成本不再主要取决于发送了多少 Token,而取决于有多少 Token 没有命中缓存。

因此优化目标从:

minimize(tokens)

变成了:

minimize(cost)subject to:maximize(cache_hit)

这里有一个容易混淆的点:

Cache 命中率并不等于前缀稳定性。

前缀稳定性是约束条件,Cache 命中率是可观测结果。

我们真正维护的是:

Prompt Prefix Stability

最终看到的是:

Cache Hit Rate

而这两个目标经常与「减少 Token」发生冲突。

例如:

压缩历史改写历史消息前缀变化Cache Miss

虽然发送的 Token 更少,但计费反而更高。

反过来:

保留历史前缀稳定Cache Hit

虽然发送的 Token 更多,但真正计费的部分更少。

因此在 KV Cache 存在的前提下:

保持前缀稳定,往往比减少 Token 更重要。

六、什么样的优化才是安全的

理解 KV Cache 之后,很多优化策略会重新排序。

首先是源头压缩,而不是事后压缩。

ViewTree    ↓GUI Context    ↓Tool Result

页面信息在生成 Tool Result 时就完成语义提炼。

页面类型、结构指标、关键元素识别、可执行动作生成等确定性计算全部放在 Runtime 侧完成,LLM 只负责推理。

这样生成出来的状态天然更小,同时不会触碰任何历史消息。

第二是裁尾,而不是改写。

如果确实需要控制上下文长度,只能删除最旧的完整消息, 删除只会缩短上下文尾部。

改写则会破坏前缀稳定性,并导致后续 Cache 全部失效。

第三是监控 Cache,而不是只监控 Token。

命中率高时,保持前缀稳定就是最大的成本杠杆。

只有在 Cache 几乎不存在的场景下(例如部分本地模型、小模型或无 Cache Provider),历史压缩这类策略才重新具备价值。

最终我移除了 Compactor,采用:

源头压缩+保持前缀稳定+监控 Cache

KV Cache 存在时,最好的优化不是改写上下文,而是让上下文从产生的第一刻起就足够小。

稳态运行后:

指标
数值
Raw Input
95k
Cache Hit
93%
实际计费
17k

结果比那次“成功优化”更便宜,也更稳定。

七、小结

这次踩坑最大的收获,并不是发现了一次失败的优化,而是重新理解了 Agent Runtime 的成本模型。

过去做 Context Engineering,关注的Token 越少越好。

因为成本几乎与输入长度线性相关。

而在 KV Cache 普及之后,情况发生了变化。

对于多轮 Agent Loop 而言,真正昂贵的往往已经不是上下文本身,而是失去缓存命中的上下文。

于是优化目标也随之改变:

让上下文更短        ↓让前缀更稳定

之前优化本身没有问题,问题在于它优化的是 Token,而 Provider 实际计费的却是 Cache Miss。

历史状态是否保留、页面信息如何表达、业务知识如何加载,看似是不同问题,本质上都在回答同一个问题:

哪些信息必须存在于上下文中,以及应该以什么代价存在。

因此,Context Engineering 的重点已经不再是单纯压缩上下文,而是在保持前缀稳定的前提下,让每一份状态都尽可能小、尽可能准确、尽可能接近事实。

理解模型如何推理很重要。

理解模型如何计费,同样重要。