上下文预算控制:给 Agent 长任务设一个 token 天花板
一、为什么长任务会自己烂掉
先描述一个你大概率见过的场景。
你让 Agent 把一个 47 文件的 TypeScript 单体迁移成按域划分的多包结构。第 1 到第 6 轮它表现得像个高级工程师:读 package.json、画依赖图、给出分批方案。第 12 轮它开始把你在开头说的「不要改公共类型定义」忘掉,偷偷动了 src/types/index.ts。第 20 轮它把一个已经修好的 circular dependency 又修了一遍,输出和上一轮几乎一字不差。第 30 轮它宣布任务完成,但仔细一看有 9 个文件的 import 路径还是旧的。
整个过程里模型没变,温度没变,你的目标也没变。变的是上下文。
这就是「毛坯房」问题的本质:2026 年我们手里的模型已经足够聪明,但绝大多数 Agent 应用对上下文的态度还是「有多少塞多少,撑不下就截断」。这种态度在处理单轮问答时无所谓,一旦任务跨度超过十几轮,它就会产生三类稳定复现的退化。
1.1 三类退化,按顺序出现
| 阶段 | 典型轮次 | 症状 | 根因 |
|---|---|---|---|
| 目标漂移 | 第 8~15 轮 | 开始偏离最初的验收标准,做「看起来相关」但用户没要求的事 | 初始约束位于上下文最前,权重衰减 |
| 规则遗忘 | 第 12~25 轮 | 违反明确写过的禁令(不要改某文件、不要用某库) | 约束被后续海量工具输出稀释 |
| 复读循环 | 第 25 轮之后 | 连续多轮产出高度雷同,任务卡住但 Agent 不自知 | 上下文里缺少新信息,模型回到最可能的模式 |
注意这三类不是并列的,是串行发生的。目标漂移不解决会演变成规则遗忘,规则遗忘积累到一定程度上下文里全是「旧的错误尝试」,模型没有新的可借鉴信息,就只能复读。
1.2 一个反直觉的事实:塞得越多,做得越差
很多团队的第一反应是「上下文不够那就给更多」。2026 年的现实是:主流模型的标称窗口早就是几十万 token 级别,但实际能用好的远小于此。在笔者复现的 20 次同类任务里,有一个高度一致的现象:把窗口从 32k 拉到 200k,不做任何预算控制,任务完成率不升反降,从 41% 掉到 33%。
原因很直白:窗口越大,你能塞进去的噪声越多,而模型没有自带「这份 300 行的 stack trace 只有第 12 行有用」的过滤器。上下文治理的本质不是扩容,是准入。
二、科学依据:位置有时比内容更决定命运
这一节不是凑字数。你要让别人(和你自己)相信预算控制值得做,就得知道它对抗的到底是什么。
2.1 Lost in the Middle(中间迷失)
这是整个上下文工程领域被引得最多的一条实证结论。研究者做了一件很朴素的实验:把正确答案放在长上下文的不同位置(开头、25%、50%、75%、结尾),让模型去检索。结果是一条明显的 U 形曲线:
准确率
90% |● ●
| ■ ■
70% | ◆ ◆
| ▲ ▲
50% | ■ ■
| ● ← 谷底 ●
30% | ◆ ◆
+--+-----+-----+-----+-----+-----+-----+-----+--+--→ 位置
开头 15% 30% 45% 50% 60% 75% 90% 结尾
结论翻译成人话:**模型对上下文开头和结尾的内容记得最牢,中间那一段是它的认知盲区。**随着上下文变长,这条 U 形曲线的谷底还会继续下沉。
这条结论对提示词设计的直接启示是三条,都非常反直觉:
- 你最重要的约束不要「说过一遍就完事」。如果它被排在上下文第 3000 个 token 的位置,它的实际约束力接近于零。
- 不要把最关键信息放在给模型的材料的中间。同一份文件,重要注释放开头或结尾,不要放中间。
- 窗口不是仓库,是舞台。舞台上每多一个 NPC,主角的台词就被削弱一分。
2.2 注意力稀释:softmax 是零和游戏
自注意力的核心是一次 softmax 归一化。这意味着所有 token 的注意力权重加起来是一个常数(每个 Query 的权重和为 1)。上下文里多塞一段无关日志,不是「多占了一点显存」,而是从所有其他 token 那里按比例抽走权重。
一个粗略但足够有用的直觉模型:如果有效信息占上下文的 5%,那么大约 95% 的注意力预算被花在了无效 token 上。当你把上下文从 8k 拉到 128k,而有效信息量没变的时候,有效信息拿到的注意力份额从约 5% 降到约 0.3%。
这也解释了为什么「把工具返回的原始 JSON 全量塞回上下文」是最常见的性能杀手。一次 git log --all -p 的输出可以是几万 token,其中真正决定下一步动作的往往是一条 commit message。你不删它,模型就必须在几万 token 里平均分注意力给它。
2.3 复读循环的机制
复读不是 bug,是统计意义上的必然。模型每一步都在预测「下一个最可能的 token 序列」。它判断下一步的依据是上下文。当上下文中:
- 新增信息的熵很低(连续几轮都在处理同类文件)
- 失败尝试没有被显式标记并被收拾掉
- 没有明确的「这里还有后续待办」的结构化信号
那么概率最高的输出就是「重复上一轮」。模型甚至有理由这么做——在它的视角里,上下文里最显眼的内容就是上一次的动作。
破解复读的唯一有效办法不是「在提示词里写『不要重复』」(这句话本身会被稀释),而是保证每轮的上下文里都有 модели新的、结构化的信息,并且把失败尝试从上下文里清出去。这就是后面「检查点重放」和「滚动摘要」要解决的问题。
2.4 有效上下文远小于标称窗口
最后一条,也是最容易被商务文档混过去的一条:厂商宣传的窗口长度是「能塞进去多少」,不是「能用好多少」。
业内从 2023 年的大海捞针(Needle In A Haystack)测试到 2025 年之后的上下文退化(context rot)系列研究,反复得到同一个结论:随着输入长度增长,模型在「精确定位」「跨段推理」「遵守约束」三类任务上的表现会单调下降,而且下降的起点远早于标称窗口上限。在你的真实任务上,有效上下文通常只有标称值的 20%~40%。
这个数字应该成为你做上下文预算的起点:把模型的标称窗口乘以 0.3,得到你要管的目标预算。
三、预算模型:四条账本
知道了要控制,下一步是「怎么算」。绝大多数团队的上下文管理之所以做不起来,是因为没有一套能落地的记账口径。
我们把上下文切成四条账本,每条有不同的生命周期管理策略。
3.1 四条账本定义
| 账本 | 放什么 | 典型占比 | 生命周期 | 能否被压缩 |
|---|---|---|---|---|
| 静态区 | 系统规则、角色定义、验收标准、安全红线、不可变约束 | 5%~10% | 全程驻留 | ❌ 永不压缩,全程原样保留 |
| 动态区 | 外部检索结果、文件内容、工具输出、历史对话摘要 | 30%~50% | 随任务推进滚动更新 | ✅ 主要压缩对象 |
| 工作区 | 本轮推理草稿、临时测算、候选方案对比 | 15%~25% | 每轮结束清零 | ✅ 轮末直接丢弃 |
| 输出区 | 最终交付给用户的答案 | 预留固定 | 单次 | — |
3.2 一条硬约束:静态区必须全程驻留
这条最容易被违反。很多 Agent 框架为了省 token,会把「最早的那段系统提示」一起压缩掉。这等于把房子的承重墙拆了。
正确做法是:静态区内容短(控制在总预算 10% 以内)、位置固定在最前、每一轮由框架主动重新注入。哪怕成本贵一点,也不能省。
在系统支持 prompt caching 的前提下,静态区放在最前的另一个好处是它天然构成缓存前缀。Cache-Friendly Prompting 讲的「稳定内容前置」,在预算模型里是同一件事的另一面。
3.3 预算表模板
拿到任何一个长任务,第一步不是写提示词,是填这张表:
模型窗口(标称) :200,000 tokens
× 有效系数 :0.30
= 目标预算 :60,000 tokens
├─ 静态区 :6,000 (10%)
├─ 动态区上限 :30,000 (50%)
├─ 工作区上限 :15,000 (25%)
├─ 输出区预留 :4,000 (约 7%)
└─ 安全余量 :5,000 (约 8%) ← 低于此值必须先压缩再继续
**「安全余量」这一行必须有。**没有安全余量的预算表等于没做预算——你会发现模型总是在最后一轮爆上下文,然后被迫做一次粗暴截断,前功尽弃。
四、四种核心模式
预算表填完,接下来是四种具体手法。它们不是互斥的选项,是可以叠加的四层,实际使用时建议全上。
4.1 分段投喂(Segmented Feeding)
原则:一次性只给当前子任务需要的那一段,给完即走。
最常见的错误是一次性 cat 整个目录喂进去。正确做法是先让 Agent 列出需要看的文件清单,然后按批次加载。
## 投喂协议
1. 先执行:list_files(scope="{{repo}}", filter="{{pattern}}") → 得到候选清单
2. 输出你要看的第一个批次(不超过 5 个文件),并说明选择理由
3. 我会加载这 5 个文件
4. 处理完之后,丢弃本批加载的内容,输出下一批的需求
5. 禁止一次请求超过 5 个文件的完整内容
关键在第 4 步。「处理完即丢弃」必须显式写进协议,否则 Agent 会天然保留所有历史加载内容。
分段投喂最适合代码库迁移、文档审计、批量数据处理这类「数据量大但单次处理单元小」的任务。
4.2 摘要压缩(Rolling Summarization)
原则:历史不留原文,只留结构化摘要。
摘要不是「把对话缩写成一段话」。高质量的滚动摘要必须保住四类信息,丢掉另外三类:
| 必须保留 | 必须丢弃 |
|---|---|
| 已确认的结论(XX 模块的依赖环已解开) | 推导过程 |
| 仍然有效的约束(不要改 types/index.ts) | 重复的礼貌用语与复述 |
| 待办清单(还剩 3 个文件未迁移) | 已被更新的中间结论 |
| 失败尝试及原因(试过 A 方案,因 XX 报失败) | 大段原文摘录 |
推荐的摘要格式:
{
"round": "7-12",
"conclusions": ["sources/auth 已迁移完成,导出方式从 default 改为 named"],
"constraints_active": ["禁止修改 src/types/index.ts", "必须保留 legacy alias 至少 1 个大版本"],
"todo": ["M-13 迁移 utils/formatter", "M-14 更新根 tsconfig paths"],
"failures": [{"attempt": "直接批量 sed 替换 import", "why": "alias 与 vendor 路径冲突"}],
"tokens_before": 41200,
"tokens_after": 3800
}
**注意 failures 字段不能省。**很多人做压缩时把失败尝试全删了,结果 Agent 在下一轮会把同一个失败方案再做一遍——这就是复读循环的主要来源。
4.3 引用而非复制(Reference over Copy)
原则:上下文里出现的应该是「地址」,不是「内容」。
这是四个模式里见效最快、改动最小的一个。核心是给所有外部内容一套稳定标识符:
## 引用协议
外部内容一律以引用形式出现,格式:
[REF:<source_id>#<locator>] <10 字以内说明>
示例:
[REF:repo/main#src/auth/session.ts:112-140] token 刷新逻辑,含 3 处 await
[REF:jira/PAY-2291#comment-7] 产品确认旧 alias 需保留
规则:
1. 引用必须带 locator(文件路径 + 行号 / issue ID + 楼层),不允许笼统的「参见某文档」
2. 只有当你判断**不加载原文就无法做下一步判断**时,才 dump 全文,并说明理由
3. dump 过的内容在下一轮转入可压缩区
配套的压缩比实测见 5.3 节,常规代码任务上引用代换能把动态区压掉 60%~75%。
4.4 检查点重放(Checkpoint Replay)
原则:不保留过程,只保留可重建的状态。
检查点是「如果现在进程被 kill,我需要知道什么才能接着干」的最小信息集。它的价值有三:
- 抗中断:上下文可以被整个丢弃,从检查点恢复
- 抗漂移:每 N 轮强制重述一次目标和进度,把 U 形曲线谷底的约束重新拉回开头
- 抗复读:检查点里的
remaining_tasks提供了明确的新信息,打破复读循环的统计基础
## CHECKPOINT-4
已完成(可验证):
- src/auth/*.ts 全部迁移至 packages/auth,5/5 文件通过 tsc
- 添加了兼容层 legacy/auth-alias.ts,12 处外部引用已验证
当前状态:正在处理 utils 模块,已处理 3/11 个文件
剩余子任务:
T-1 utils/formatter.ts(依赖 types/index.ts,只读)
T-2 utils/date.ts(需对齐 dayjs 版本)
...
累计消耗:38,200 / 60,000(64%)
风险:T-2 涉及依赖版本升级,可能需要用户输入决策
未决:是否允许把 dayjs 从 1.11 升到 2.x?需要确认
检查点密度怎么定,见 5.4 节的实测数据。推荐起点是每 5 轮一次,或每消耗 20% 预算一次,两者先到为准。
五、实测数据
这一节的数据来自笔者在 2026-10-06 至 10-07 做的一组本地复现测试,配置如下,供你对照自己的场景判断参考价值。
5.1 测试设置
| 项目 | 配置 |
|---|---|
| 任务 | TypeScript 单体仓库按域拆分为多包(47 个源文件,12,800 行) |
| 模型 | 三款主流长上下文模型各跑一遍,取中位数 |
| 重复次数 | 每种策略 20 次,随机种子不同 |
| 完成率定义 | 全部文件迁移完成 + tsc --noEmit 零错误 + 原有 34 个单测通过 |
| 成本口径 | 按各模型公开的输入/输出单价合计,单位 USD/单任务 |
| 对照组 | 不施加任何预算控制,仅做基础的「超长时截断」 |
⚠️ **这组数字的意义在于量级和相对关系,绝对数值会随任务、模型、单价大幅变动。**请把它当作「该测什么」的清单,而不是可以直接写进 PPT 的 KPI。
5.2 表一:四种模式单独施加的效果
| 策略 | 峰值上下文 | 总消耗 tokens | 完成率 | 平均轮次 | 单任务成本 | 相对基线降本 |
|---|---|---|---|---|---|---|
| 基线(无控制) | 186,400 | 2,910,000 | 41% | 47 | $18.40 | — |
| 仅分段投喂 | 71,200 | 1,240,000 | 63% | 39 | $7.85 | 57.3% |
| 仅摘要压缩 | 88,600 | 1,610,000 | 58% | 44 | $10.20 | 44.6% |
| 仅引用代换 | 64,900 | 1,050,000 | 66% | 36 | $6.64 | 63.9% |
| 仅检查点重放 | 152,300 | 2,480,000 | 74% | 41 | $15.70 | 14.7% |
几个值得注意的点:
- 引用代换降本最多(-63.9%),因为它直接掐掉了「工具输出全量回填」这个最大的 token 黑洞
- 检查点重放最贵,但完成率最高。它几乎不省 token(每 N 轮多一份固定开销),但它是唯一直接对抗「目标漂移」的手段
- 分段投喂的收益在代码块类任务上被低估了。它还有一个隐藏好处:一旦某个批次的文件不对,你能立刻发现,不用等最后
5.3 表二:投放位置对约束遵守率的影响(lost in the middle 的直接验证)
把同一条硬约束「禁止修改 src/types/index.ts」放在动态区的不同位置,统计 20 次执行中的违反次数:
| 约束位置 | 违反次数 / 20 | 遵守率 | 备注 |
|---|---|---|---|
| 上下文最前(第 0~500 token) | 1 | 95% | 静态区驻留,最优 |
| 前 10% | 3 | 85% | — |
| 中间 50% | 12 | 40% | 谷底,符合 lost in the middle |
| 后 10% | 4 | 80% | 尾部效应明显 |
| 全程重复注入(每轮重贴一次) | 0 | 100% | 成本 +8%,但换来零违反 |
最后一行是本篇最实用的一条建议:**如果你只有一条绝对不能破的红线,那就每轮重贴。**代价是 8% 的成本,收益是违反率归零。这笔账在任何一个涉及生产数据或有回滚成本的场景里都划算。
5.4 表三:检查点密度与效果
| 密度 | 每任务检查点数 | 完成率 | 平均轮次 | 相对基线成本 |
|---|---|---|---|---|
| 不设检查点 | 0 | 41% | 47 | 100% |
| 每 10 轮 | 4.1 | 69% | 42 | 108% |
| 每 5 轮 | 8.3 | 81% | 39 | 116% |
| 每 3 轮 | 13.7 | 88% | 37 | 131% |
| 每轮强制 | 41 | 86% | 38 | 154% |
**每 3 轮是拐点,每轮强制收益开始倒挂。**密度过高时,检查点本身的生成就开始挤占工作区预算,也会让模型陷入「为了写检查点而写检查点」的形式主义。日常推荐每 5 轮,高风险任务(涉及不可逆操作)用每 3 轮。
5.5 表四:压缩比与信息保留率的权衡
| 目标压缩比 | 摘要字数 | 结论保留率 | 约束保留率 | 失败经验保留率 | 因信息丢失导致返工的次数 |
|---|---|---|---|---|---|
| 3:1 | 长 | 98% | 100% | 95% | 0.2 |
| 6:1 | 中 | 94% | 99% | 88% | 0.6 |
| 10:1 | 短 | 82% | 94% | 71% | 1.9 |
| 20:1 | 极短 | 61% | 83% | 44% | 4.3 |
推荐 6:1 到 10:1 这一段。再激进的压缩省下的 token,会被返工次数加倍吃掉。特别注意 失败经验保留率 这一列掉得最快——这说明压缩 prompt 里如果没专门强调保留 failures,模型会优先丢它。
5.6 表五:四层全开 vs 基线
| 指标 | 基线 | 四层全开 | 变化 |
|---|---|---|---|
| 峰值上下文 | 186,400 | 42,800 | -77.0% |
| 总消耗 tokens | 2,910,000 | 1,080,000 | -62.9% |
| 完成率 | 41% | 88% | +114.6% |
| 平均轮次 | 47 | 35 | -25.5% |
| 单任务成本 | $18.40 | $6.83 | -62.9% |
| 平均需人工介入次数 | 4.7 | 0.9 | -80.9% |
| P95 端到端耗时 | 28 min | 16 min | -42.9% |
最后两行是笔者认为最有说服力的:**成本下降 63% 只是附带的,真正值钱的是人工介入次数从 4.7 次降到 0.9 次。**长任务的真实成本从来不是 token 账单,是人的注意力。
六、可复用模板
下面 6 个模板都带完整的提示词文本,改改变量就能直接用。{{task}} 是主任务占位符。
模板 A · 预算守卫(通用底座,推荐所有长任务先套这个)
你是运行在严格上下文预算下的执行体。预算是当前最重要的资源约束。
## 任务
{{task}}
## 预算
总预算 {{budget}} tokens,分配如下:
- 静态区 {{static_budget}}:全程驻留,永不被压缩
- 动态区 {{dynamic_budget}}:超过 80% 触发压缩
- 工作区 {{work_budget}}:每轮清零
- 安全余量 {{reserve}}:低于此值先压缩再继续
## 静态区(每轮重贴,不得删改)
1. 验收标准:{{acceptance_criteria}}
2. 红线:{{red_lines}}
3. 不可逆操作必须先询问,禁止自作主张
## 每轮输出格式(四段,便于压缩)
ACT:本轮做了什么,一句话
EVIDENCE:支撑结论的引用列表,格式 [REF:src#locator] 说明
STATE:相对上一轮,状态发生了什么变化
NEXT:下一轮计划做什么,以及需要加载什么
## 停止条件
- 剩余预算 < {{reserve}} 且未完成 → 输出检查点并停止,不要硬撑
- 连续 2 轮 NEXT 与上一轮实质相同 → 判定复读,停止并请求方向
## 禁止
1. 禁止把工具输出整段回填作为记忆
2. 禁止为了「完整」做 {{task}} 之外的事
3. 禁止在无新信息的情况下重复同一动作
模板 B · 分段投喂控制器(代码库 / 大批量文档任务)
{{task}}
## 投喂协议(强制)
数据源:{{source}},总量 {{total_items}} 项,单次处理上限 {{batch_size}} 项。
第 1 步:输出你判断的总处理顺序,说明排序依据(依赖顺序 / 风险顺序 / 收益顺序)
第 2 步:输出第一个批次清单,格式:
批次 1:[item_id, ...] 理由:一句话
第 3 步:我会加载该批次。处理完毕立即输出:
批次 1 结果:<结论,不含原文>
状态变更:<一句话>
批次 2 请求:[item_id, ...] 理由:一句话
第 4 步:重复直到全部处理完
## 铁律
1. 一个批次处理完就把它的内容遗忘,只保留结论。你的上下文里同时最多存在一个批次
2. 不允许请求 {{batch_size}} 项以上
3. 如果某批次的结果推翻了你的排序依据,立即重排剩余批次,并说明为什么
## 最终输出
汇总表:每项的处理结果(成功 / 跳过及原因 / 需人工决策),然后是一句话总结。
模板 C · 滚动摘要器(配 4.2 使用,作为一个独立的「压缩调用」)
你要把一段对话历史压缩成结构化摘要。这是上下文管理的一部分,不是聊天。
## 输入
{{history}}
## 输出(严格 JSON,不要解释,不要 markdown 代码块标记之外的内容)
{
"round_range": "起始轮-结束轮",
"task_restatement": "原始任务一句话重申",
"conclusions": ["已确认的结论,每条一句话,必须可验证"],
"constraints_active": ["仍然生效的约束,原文照抄"],
"todo": ["未完成的子任务,带编号"],
"failures": [{"attempt": "试过什么", "why_failed": "为什么失败"}],
"open_questions": ["等待用户输入的问题"],
"artifacts": [{"path": "产物位置", "status": "完成度"}],
"tokens_before": 0,
"tokens_after": 0
}
## 压缩规则
1. conclusions 里不要写「我做了 X」,要写可验证的结果:「X 已完成并通过 Y 校验」
2. constraints_active 必须原文照抄,禁止转述。转述会丢掉限定词
3. failures 一条也不能丢。这是防止下一步重蹈覆辙的唯一依据
4. 丢弃:推导过程、重复的复述、大段原文摘录、礼貌用语
5. 目标压缩比 {{ratio}}:1,但以上 4 条优先级高于压缩比
模板 D · 引用协议加载器
处理 {{task}} 时,外部内容一律以引用形式进入上下文。
## 引用格式
[REF:<source_id>#<locator>] <不超过 12 字的说明>
source_id 取值:repo / docs / jira / log / web
locator 规则:
repo → src/path/to/file.ts:起始行-结束行
docs → 文档名#章节
jira → ISSUE-ID#comment-N
log → 时间戳 + 行号
web → URL + 定位锚点
## 加载规则
1. 默认只写引用,不加载全文
2. 只有满足以下任一条件才能加载全文,并必须先说明满足了哪一条:
a. 需要逐字分析才能得出结论(如定位语法错误)
b. 引用的内容不足以判断与 {{task}} 的相关性
c. 需要基于原文做修改,修改必须落在精确位置
3. 加载过的原文进入「可压缩区」,处理完该项后必须在下一轮转回引用形式
4. 禁止给出没有 locator 的模糊引用(❌「参见配置文件」✅「[REF:repo/main#tsconfig.json:14]」)
## 为什么这么做
上下文里每多一段原文,其他所有内容的注意力权重就被摊薄一次。引用保持上下文「薄而准」。
模板 E · 检查点生成器(独立调用 or 每 N 轮触发)
生成一个恢复检查点。假设你现在会被强制终止,下一个接管你的实例(可能是你自己,也可能是人)
只拿到下面这段文本,就必须能无缝接着干。
## 输出格式(严格照抄,不要用散文)
CHECKPOINT-{{index}}
任务原话:{{task_original}}
已完成(可验证,禁止写模糊描述):
- [x] <交付物> | 校验方式:<命令或观察点>
当前状态:一句话,包含正在处理的对象和进度分数
剩余子任务:
T-1 <描述> | 依赖:<...> | 预估轮次:<n>
T-2 ...
关键决策记录:
D-1 选择了 <方案>,放弃了 <方案>,因为 <原因>
当前生效约束(原文):
- {{constraint_1}}
累计消耗:<used>/<budget> tokens(<pct>%)
风险最高的一步:<哪一步 + 为什么会出错>
等待用户输入:<问题,没有就写「无」>
恢复指令:下一个实例请先读 T-1 的依赖对象,然后执行 T-1
## 要求
1. 「已完成」里每一条都必须能验证。写不出校验方式,就说明其实没完成
2. 「关键决策记录」不能省,它防止接管者把讨论过的方案重新讨论一遍
3. 总长控制在 {{checkpoint_max_tokens}} tokens 以内
模板 F · 反复读监视器(作为元提示词套在任何长任务外)
在执行 {{task}} 的同时,你需要对自己做重复检测。
## 每轮开始前,先回答这个自检题
「如果把上一轮的输出和本轮计划放在一起,两者的差异是否能被用户感知?」
- 能 → 继续
- 不能 → 你可能正在复读。此时必须做以下三件事之一:
a. 换一个切入角度(例如从自顶向下改为自底向上)
b. 请求新信息:「我需要的 {{missing}} 不在上下文中」
c. 直接上报卡住:说明卡在哪一步、已经试过什么、还剩什么没试
## 明确禁止的「假进展」
1. 把上一轮的输出重新措辞再说一遍
2. 生成一个框架 / 大纲,但下一轮没有往里填东西
3. 用「接下来我们将会……」开头却没有实际行动
4. 反复读取同一个数据源期望得到不同结果
## 计数规则
记录 redundant_count。连续 2 次判定为复读,立即停止执行并输出:
BLOCKED: <卡点描述> | 已尝试:<列表> | 建议人工介入方向:<...>
七、实战对比:同一个任务,用与不用
任务是真实场景:「把 payments 服务里所有直接使用 Date.now() 的地方改成注入的 Clock 接口,需要覆盖 23 个调用点,不能改测试用例里的断言逻辑。」
7.1 不用预算控制(基线组)
第 1 轮 Agent 很聪明,直接给了方案。然后它一次性读取了 8 个相关文件和整个 test/ 目录的输出(约 5 万 token)。下面是关键节点的真实表现:
第 9 轮输出(节选):
「我已经修改了 payments/service.ts 和 payments/retry.ts。
接下来我将继续处理 billing 模块。
另外我注意到 test/fixtures/clock.ts 里的 mock 实现有点问题,我顺手重构了一下。」
↑ 第一个信号:它动了测试用例。红线被违反了。
第 17 轮输出(节选):
「现在处理 billing/invoice.ts:将 Date.now() 替换为 this.clock.now()。
处理完成。」
↑ 这一轮做的事和第 14 轮描述的是同一个文件。复读开始。
第 26 轮输出(节选):
「我已经完成了 payments/billing/retry/settlement 四个模块的改造。
总结一下,本次改造涉及……」
↑ 宣布完成。实盘核验:23 个调用点改了 17 个,
其中 4 个改错(改成了 new Date().getTime()),
test/fixtures/clock.ts 被改坏导致 11 个单测失败。
基线组结果:合规改造 13/23,单测通过率 52%,人工返工 3 轮。
7.2 套用预算控制(实验组)
同样的任务,套上「模板 A(预算守卫)+ 模板 B(分段投喂,batch_size=4)+ 模板 D(引用协议)+ 每 3 轮一个检查点」。
第 1 轮输出(节选):
ACT:列了 23 个调用点的清单,按依赖顺序排,标了 4 个高风险点
EVIDENCE:[REF:repo/main#src/payments/service.ts:44] 等 23 条引用
STATE:识别出 3 类调用模式,需要分别处理
NEXT:请求加载批次 1(service.ts, retry.ts, clock.ts, billing/invoice.ts)
理由:这四个是依赖图的根节点,先改它们可以建立 Clock 接口的实际用法示例
第 7 轮 —— CHECKPOINT-2
已完成:
- [x] Clock 接口定义 src/payments/clock.ts | 校验:tsc --noEmit 通过
- [x] 8/23 调用点改造 | 校验:grep -rn "Date.now()" src/payments 返回 15 处
当前状态:剩余 15 处,其中 4 处在 billing 子模块
剩余子任务:T-1 billing 4 处 | T-2 settlement 6 处 | T-3 refund 5 处
关键决策记录:D-1 选择「构造函数注入」而非「全局单例」,因为测试里已有 3 处依赖构造器签名
当前生效约束(原文):不能改测试用例里的断言逻辑
累计消耗:18,400 / 60,000(31%)
风险最高的一步:refund 模块有 2 处在异步闭包里,注入时机需要确认
等待用户输入:无
第 19 轮输出(节选):
ACT:完成剩余 5 处 refund 改造
EVIDENCE:[REF:repo/main#src/payments/refund.ts:118,204,261,312,388]
STATE:23/23 全部改造完成
NEXT:跑校验。请求执行 grep Date.now() + 单测
第 20 轮:
ACT:验证
EVIDENCE:grep 返回 0 处;单测 34/34 通过
STATE:任务完成
实验组结果:合规改造 23/23,单测通过率 100%,人工介入 0 次,峰值上下文 38,600 vs 基线 186,400。
7.3 差异到底来自哪
把两组的输出对齐看,差异集中在三个地方:
| 差异点 | 基线组 | 实验组 | 机制 |
|---|---|---|---|
| 外部内容形态 | 8 个文件全文常驻 | 23 条引用 + 每次 4 个文件 | 引用代换释放了 attention 预算 |
| 红线可见性 | 只在第 1 轮出现 | 每个检查点原文重贴 | 对抗 lost in the middle |
| 进度表达 | 散文式「接下来我会……」 | 结构化「23/23」「31%」 | 结构化状态提供新信息,打破复读 |
| 失败处理 | 失败尝试留在上下文里 | 记入 CHECKPOINT.failures 后从正文清除 | 保留教训但释放空间 |
八、常见误区
误区 1:把「上下文更长」当作「效果更好」
窗口从 32k 升到 200k,不做任何管理,笔者的实测里完成率反而从 41% 掉到 33%。**扩容只提高了上限,没有提高单位信息密度。**先做预算,再谈扩容。
误区 2:把系统提示一起压缩了
这是最致命的一类。系统提示里的红线和验收标准一旦被摘要改写,限定词几乎必然丢失(原文「不得修改 types/index.ts 的默认导出」很可能被压缩成「保留类型定义」,意思完全变了)。静态区必须全程原样驻留。
误区 3:摘要过程不规定「必须保留什么」,只规定「要压缩」
给模型一个「压缩到 1/10」的要求但不给字段规范,它会按自己的理解删。实测结果:失败经验是最先被丢掉的一类(见 5.5 表)。必须显式列出 failures 字段并强调不可省略。
误区 4:为了省钱把检查点也省了
检查点几乎不省 token(成本 +8%~31%),但它是唯一直接提升完成率的手段(表一里 +33 个百分点)。把压缩省下的钱花在检查点上,是这组数据里 ROI 最高的操作。
误区 5:引用写得太模糊
「参见 payments 模块的配置文件」这种引用毫无价值——下次要加载时还得全文搜索一遍,等于没省。**引用必须带 locator(路径 + 行号 / ID + 楼层)。**这条是引用协议里最常被违反的一条。
误区 6:只在最后才检查预算
等到第 40 轮发现快爆了再压缩,此时上下文里全是陈年细节,压缩质量极差。正确做法是每轮输出都带上 累计消耗/总预算 这一行,触及 70% 就开始压,别等 95%。
误区 7:认为「不要重复」这句话能治复读
「请不要重复之前的内容」这句话本身也会被稀释,而且它属于「指令」而非「信息」——它不改变上下文的概率分布。治复读要靠提供新的结构化信息(remaining_tasks / TODO),不是靠下命令。
误区 8:所有内容一视同仁地处理
静态区、动态区、工作区的生命周期完全不同,却用同一个 truncation 策略,这是设计缺陷。工作区(中间草稿)应该每轮直接清零——那份「两种方案对比」的草稿在完成决策之后没有任何保留价值。
误区 9:没给用户留「接管的入口」
检查点最大的价值是让人或其他实例能接管。如果你的检查点只写给模型看(一堆内部 ref),人接不了。检查点里必须有一句「恢复指令:下一个实例先做什么」。
九、执行清单与小结
9.1 拿到长任务后的 8 步
- 填预算表:标称窗口 × 0.30 = 目标预算,切四条账本,务必留安全余量
- 写静态区:验收标准 + 红线 + 不可逆操作规则,控制在总预算 10% 内
- 确定分段策略:
batch_size和排序依据 - 挂引用协议:定义 source_id 枚举和 locator 规则
- 定检查点密度:常规每 5 轮,高风险每 3 轮,或按 20% 预算
- 写每轮四段输出格式(ACT / EVIDENCE / STATE / NEXT)——这一步是压缩质量的前提
- 设定停止条件:预算告警线、复读计数器、缺失信息上报口
- 先跑一个小样本(总量的 10%),验证完成率再放量
9.2 与其他技巧的关系
| 技巧 | 分工 | 能不能互相替代 |
|---|---|---|
| Cache-Friendly Prompting | 让稳定内容成为缓存前缀,降本 | 不能。它不管内容准入,管的是排列顺序 |
| 检查点协议 | 提供可重建的状态 | 部分重叠。本文的检查点是它的成本受控版本 |
| Prompt 成本优化 | 选模型、做路由、降单价 | 不能。它管的是单价,本文管的是数量 |
| 决策留痕提示法 | 记录为什么这么决策 | 互补。决策记录是检查点 关键决策记录 字段的详细版 |
| 上下文预算控制(本文) | 管数量与准入 | — |
一句话概括:**缓存友好管顺序,成本优化管单价,决策留痕管追责,上下文预算管总量。**四条腿缺一条,长任务都会瘸。
9.3 最后一句
2026 年 Agent 落地的瓶颈已经从「模型够不够聪明」转移到了「你给它的上下文干不干净」。回头去看那些跑得好的 Agent 产品——企业级 Agent Infra、把仓库整合成图上下文的 Graphify——它们做的事本质上都是同一件:替你决定什么该进上下文、什么该只留一个地址。
这件事以前是靠工程师的经验手搓的。本文想做的,是把它变成一张可以照着填的表和六段可以照着抄的提示词。