从零开始的 Agent 开发:上下文,压缩和预算
0x00 前言
在多轮 tool loop 中,一种常见且有利于 Prompt Cache 的做法,是让 provider-visible history 尽量保持 append-only。已经发送过的消息、tool call 和 tool result 不再改写,只在尾部追加新内容。我曾经有一种设想,对上下文进行更细粒度的动态管理,分块处理,进行标记,长内容压缩/摘要,试图节省 Token 消耗以最终减少调用花费。但在进行实验后发现,这样做确实能够带来小规模的 Token 节省,但是从全局看,花费反而升高。
0x01 失败
在探索中,我构建了两套并行的上下文策略,一套为 Dynamic Context,即我天马行空的设想,以下简称 dc 策略罢。另一套为工程实践 Append-Only 的策略,简称ac。在验证设想的过程中,为减少花费,dc 策略是实际请求中所使用的,而 ac 策略为基于同一 trace 的确定性反事实重放。
以执行一个对项目的探索任务作为测试(没做太多控制变量的严谨测试或者 benchmark,俺又不是搞科研嘞),使用模型为 ChatGPT 5.6 Terra,推理强度为 xhigh。
模型价格数据为:
- 普通输入:$2.50 / M
- 缓存读取:$0.25 / M
- 缓存写入:$3.125 / M
- 输出:$15 / M
逐请求结果:
| 请求 | 输入 | 输出 | DC缓存 | DC命中率 | AC缓存 | AC命中率 | DC总费用 | AC总费用 |
|---|---|---|---|---|---|---|---|---|
| 4 | 25094 | 181 | 9728 | 38.77% | 9728 | 38.77% | $0.043562 | $0.043562 |
| 5 | 31198 | 145 | 9728 | 31.18% | 25094 | 80.43% | $0.058282 | $0.023709 |
| 6 | 39672 | 232 | 9728 | 24.52% | 31198 | 78.64% | $0.080772 | $0.032465 |
| 7 | 44895 | 189 | 9728 | 21.67% | 39672 | 88.37% | $0.093185 | $0.025811 |
| 8 | 51177 | 1966 | 9728 | 19.01% | 44895 | 87.72% | $0.135545 | $0.056419 |
从请求数据可以看到,自第一个请求以后的数据差异越来越大。
Uncached input 减少:
排除第一个共同基线请求,看后续四次 warm requests:
| 指标 | DC | AC |
|---|---|---|
| 输入 tokens | 166,942 | 166,942 |
| 缓存命中率 | 23.31% | 84.38% |
| 输入费用 | $0.329803 | $0.100422 |
| 总费用 | $0.367783 | $0.138402 |
Warm phase 总费用下降 62.37%,Warm phase 输入费用下降 69.55%。对比最后一次请求,单次输入费用下降 74.61%,单次总费用下降 58.38%。输出成本较高,因此总费用降幅小于输入费用降幅。
那么是为什么导致 DC 后续的缓存无法命中了呢?这跟 DC 的设计缺陷有关。只有前约 9k 左右的 Prompt 是没有被改变的,能够很好的命中缓存,但是自 9k 往下的新增 block 因为频繁变更导致根本无法命中缓存。
0x02 期望
优化的最终目标是降低总 Token 花费,而不只是单单降低 Token 的总量,或是仅降低上下文占用。
压缩成本
这里以 表示普通输入, 表示缓存读取, 表示缓存写入, 表示输出。
相关比例为:
- 普通输入 = 10 × cache read
- cache write = 12.5 × cache read
- output = 60 × cache read
- output = 6 × 普通输入
对第 次请求:
- :总输入
- :cached input
- :uncached input
- :显式 cache write
- :输出
请求成本:
如果把普通输入 token 作为统一单位,可以写成:
生成 1K output 的 compaction summary,费用等价于 6K 普通输入或 60K cached input。那么生成 10K summary 的输出费用为 $0.15,等价于读取 600K cached tokens。
所以 LLM compaction 本身非常昂贵。
在当缓存命中达到 90% 至 95% 时,以前文数据核算,相对当前,90% 缓存命中总成本下降约 67.9%,95% 缓存命中总成本下降约 73.2%。
当缓存到达 95% 后,输出成本已经占:
也就是说,缓存足够高以后,继续优化输入的边际收益会下降,输出长度会成为新的主要成本。
盈亏条件
考虑保留缓存历史与压缩的 break-even:
定义:
- :准备压缩的、已经缓存的历史
- :压缩后的 summary
- :由于前缀被改写,后面一起失效的 collateral suffix
- :未来使用该上下文的请求总数,包含压缩后的第一次冷请求
- :生成 summary 的模型成本
不压缩时:
未来 次继续读取缓存:
压缩时:
第一次请求冷启动,后续 次缓存读取:
压缩有利需要:
整理后:
含义:
- :生成摘要的直接费用
- :冷启动和连带缓存失效代价
- :每次未来请求真正节省的 cache read 成本
如果暂时忽略 summary 生成成本和 collateral:
压缩有利需要:
因为:
所以:
也就是说,如果会重置缓存,压缩后必须小于原长度的 10%,即一次压掉 90% 以上,才能在下一次请求立即回本。
如果压缩发生在上下文中间,那么被修改位置之后即便内容不变,也无法继续命中 prefix cache。
设受影响后缀为 。
普通冷启动的额外代价:
对于 51K → 10K 的场景,并连带让后面 20K 失效的话,大约需要34 次未来复用才回本;如果还使用显式 cache write,则约需要 37 次。
因此,越靠前修改历史,经济代价越大。
数学期望
考虑有终止概率时的期望:
假设每轮后终止概率为 ,未来请求数量为几何分布。
如果允许未来请求数为 0:
设压缩回本需要 次未来复用,则经济上值得压缩需要:
等价于:
例如 51K 压到 10K,需要约 30 次复用:
只有当每一轮结束任务的概率低于约 3.2%,即预计任务还会持续约 30 轮时,才值得单纯为了费用执行这次 LLM compaction,而日常中的普通 turn 通常不满足,只在长线任务执行,上下文将满时才有意义。
根本目标是费用,真正的优化目标应该为:
而最小化 raw context 并非一个好的选择。
0x03 总结
大家都这么干一定有道理
有时候的天马行空并不能经得起实践的检验。