上下文预算与 compaction
上下文是最贵、最先撞墙的资源。这一节把账算清楚:谁在吃、第几轮撞墙、压缩要压什么。
学完这节你能做到
- 算出一个 agent 会话的上下文构成,并指出最先撑爆窗口的那一项
- 预测在给定窗口下大约第几轮触发 compaction
- 设计一个不破坏 prompt 缓存的压缩策略
上下文是 agent 里最贵、也最先撞墙的资源。它同时决定三件事:能不能跑完、跑得准不准、花多少钱。 这一节把账算清楚。
五个吃客
一次请求的输入由这几块组成,每一轮都要完整重发:
| 吃客 | 特点 | 典型量级 |
|---|---|---|
| 系统提示词 | 固定不变,缓存友好 | 1–5k |
| AGENTS.md / 项目指令 | 固定,但容易越写越长 | 0.5–3k |
| 工具声明 | 固定,随工具数线性增长 | 每个 100–400 |
| 对话历史 | 线性累积 | 每轮 0.3–1k |
| 工具输出 | 波动最大,一次就能吃掉半个窗口 | 每轮 1k–50k |
前三项是固定开销:不干活也占着,但内容稳定,prompt 缓存能把它的钱压下来。 后两项是增量,一路往上涨到撞墙。
200k 窗口不等于能装 200k 输入。要给这一轮的输出留位置 —— 留 8k 输出,可用输入预算就是 192k。算账时别忘了减掉。
自己算一遍
改几个参数,看两件事:不压缩的话第几轮撞墙,以及窗口到底被谁吃掉了。 先按默认值看一遍,再点「工具堆到 40 个」那个预设 —— 注意固定开销那一行的变化。
- 系统提示词2.6k
- AGENTS.md / 项目指令900
- 工具声明2.3k
- 对话历史9.8k
- ▸ 工具输出77k
工具输出是最大的吃客,也是最容易压的一项。给工具的返回值加长度上限、加分页、加摘要 —— 一次 grep 全仓就能吃掉几万 token,而模型真正需要的往往只有几行。
- 固定开销
- 5.8k(每轮重发,可被缓存)
- 40 轮总花费
- $6.38(含缓存命中)
- 若完全不命中缓存
- $13.9
- 缓存省下
- $7.57 · 54%
- 单轮均价
- $0.160
几个值得自己试出来的结论:
- 把「每轮工具输出」从 2200 调到 20000,撞墙轮次立刻掉到个位数 —— 工具输出是唯一值得优先优化的那一项。
- 工具从 12 个加到 40 个、每个 320 token,固定开销直接吃掉可用预算的一大块, 而这部分每一轮都在付。
- 把「压缩后保留历史」调到 60%,压缩次数明显变多 —— 压得浅等于没压。
compaction:省窗口是要花钱的
接近上限时,harness 会把早期历史摘要掉,换出空间。Pi 默认就做这件事,也允许用扩展替换策略。
代价常被算漏:压缩改变了 prompt 前缀,缓存全部失效。那一轮的输入按全价重算。 计算器里的「若完全不命中缓存」和「缓存省下」两行就是在说这笔账 —— 压缩越频繁,缓存的价值被冲刷得越厉害。
摘要不可能无损。被压掉的往往是「第三轮那个报错的具体行号」这类细节 —— 而它恰恰是后面排障要用的。所以压缩不是终点,是最后手段。
比压缩更该先做的四件事
- 给工具输出设上限并分页 —— 一次 grep 返回三十行加一句「还有 812 条,缩小范围」, 比返回全部有用得多。
- 按需读取,别预加载 —— 别把「可能有用」的文件提前塞进上下文,让 agent 自己去读。
- 外置状态 —— 长任务的进展写进文件(TODO.md、笔记),需要时再读回来。 文件是无限大的上下文。
- 工具分组注册 —— 四十个工具不必同时在场。这也是 skill 存在的理由: 用得上才装载。
把最稳定的放最前面:系统提示 → 项目指令 → 工具声明 → 历史。 任何在前面插入内容的行为(比如每轮往系统提示里注入当前时间)都会让整个缓存前缀失效 —— 一个动态时间戳就能让缓存命中率归零。
什么时候该换更大的窗口
不是「越大越好」。窗口变大,单轮输入的钱也线性上涨,而且长上下文里的信息检索质量会下降 (中段内容更容易被忽略)。判断顺序是:
- 先看计算器里排第一的吃客 —— 能压的先压。
- 压不动了,看压缩频率。40 轮压 4 次以上,说明预算结构本身有问题。
- 结构没问题、任务本身就需要长上下文(大仓库重构、长文档处理),再换大窗口。
一个 agent 在第 8 轮就报「上下文超限」。用计算器一算,占比最高的是工具输出(每轮平均 18k)。最该先做什么?
这一节的结论
- 窗口是输入输出共享的,算账先扣掉输出预留。
- 固定开销(提示词 + 指令 + 工具声明)每轮都付,缓存能省钱但省不了窗口。
- 工具输出是最大变量,也是最容易压的一项 —— 优化从这里开始。
- compaction 是最后手段:它用一次全价重算换空间,还会隐蔽地丢细节。
- 先压输出、按需读取、外置状态、分组注册;这四条都做完了再考虑换大窗口。