7.1 窗口预算与优先级分配


7.1 窗口预算与优先级分配

本节摘要:把 3.3 节的"预算分配"四个字展开成可执行的工程。窗口是零和游戏,先扣不可动的三块:常驻层(L1 系统提示 + L3 工具 schema,"常驻税")、输出预留(20%,供料塞满的第一个症状是模型说话越来越短)、pinned 清单(任务目标、当前计划、红线约束——丢了它们,后面全白干)。剩余预算按优先级装配动态层;优先级的工程形态不是一张"谁更重要"的表,而是驱逐顺序(eviction order):超限时从最廉价的层开始丢——陈旧工具结果最先丢,已完成子任务的中间历史次之,探索性对话再次,最近 K 轮最后丢;每层内部还要"丢大块保线索"(截断长输出而非整条删除)。本节给出 budget.py,并交代 token 估算的示意做法与升级路径。

学习目标

阅读完本节,你应当能够:

  1. 为一类具体任务写出窗口预算表(含常驻税与输出预留)。
  2. 写出驱逐顺序并解释每个条目的排位理由。
  3. 列出 pinned 清单的最小集合。
  4. 实现"丢大块保线索"的逐层驱逐。

一、预算表:先扣不可动的三块

以"中型仓库里修 bug 并跑测试"为例(示意数字,总窗口按 128k 计):

内容 预算占比(示意) 性质
L1 系统提示 身份、环境、约束 3~5% 常驻税
L3 工具 schema 全部工具的契约清单 8~12% 常驻税(4.2 节路由可节税)
输出预留 模型本轮生成空间 20% 不可动(3.3 节第一课)
pinned 清单 任务目标 / 当前计划 / 红线约束 2~5% 不可动
L2 任务与历史 对话与决策记录 剩余的 1/3 动态
L4 工具结果 文件内容、命令输出 剩余的 2/3 动态(无限增长层)
L5 环境状态 文件树、git 状态 按需 5% 动态

两条读表结论:其一,常驻税 + 输出预留合计约 1/3——供料策略真正能调配的只有 2/3 窗口,这是很多人配预算时漏掉的前提;其二,L4 注定是主战场——它是唯一无限增长的层,所有驱逐策略首先冲它去。

二、优先级 = 驱逐顺序的反面

说"优先级高"太空洞,工程上直接写谁先丢

顺位 被驱逐的层 为什么它最先丢 丢了之后的补偿
1 陈旧工具结果(旧日志、已读过的文件原文) 时效最差:文件还在盘上,可重读 引用化:留"路径 + 一行摘要"(3.3 节)
2 已完成子任务的中间历史 结论已沉淀进计划/摘要,过程无价值 结论并入 pinned 的计划
3 探索性对话(早期试错讨论) 信息密度低 关键发现并入任务简报(7.2 节)
4 环境状态快照 可重新探查 下一步按需再取
pinned + 最近 K 轮 不驱逐:任务目标丢了等于失忆;最近 K 轮丢了模型接不上话

pinned 清单的最小集合:任务原文、当前计划(含 TodoWrite 类产物)、红线约束("不许做 X"的约定)、关键文件的引用化条目。第四项最容易被忽略——红线条目丢了,模型可能把 7.2 节说的"为什么不做 X"的约束忘掉,转个圈又把 X 做了。

三、丢大块保线索

驱逐不等于整条删除。同一条 3000 token 的测试输出,整条删除丢掉的信息远多于截成"首 200 token + [中间截断] + 尾 200 token"——尾部尤其重要:pytest、编译器的结论(成败、错误摘要)几乎总在末尾。这就是 6.2 节 sandbox_exec 做"首尾保留、中间截断"的预算学依据。

四、budget.py:逐层驱逐的实现

# budget.py —— 窗口预算与驱逐顺序(写法示意,以实际工程为准) def est_tokens(msgs: list[dict]) -> int: """粗估 token 数(示意):中英混排按字符数 / 3;生产应换分词器精确计数。""" return sum(len(str(m.get("content") or "")) for m in msgs) // 3 # 驱逐顺序:越靠前越先丢;不在表内的层默认保留 EVICT_ORDER = ["stale_output", "done_history", "old_chat", "env_state"] def fit_budget(msgs: list[dict], budget: int, reserve: float = 0.2) -> list[dict]: """超预算时按层驱逐;pinned 与最近 K 轮(recent)永不丢。""" cap = int(budget * (1 - reserve)) # 输出预留(3.3 节第一课) out = [dict(m) for m in msgs] # 每条消息需带 "layer" 标注 for layer in EVICT_ORDER: if est_tokens(out) <= cap: return out out = [m for m in out if m.get("layer") != layer or m.get("pinned")] for m in reversed(out): # 还超:丢大块保线索(尾部敏感) if m.get("layer") == "stale_output" and not m.get("pinned"): c = str(m.get("content") or "") m["content"] = c[:200] + "\n…[中间截断]…\n" + c[-200:] if est_tokens(out) <= cap: break if est_tokens(out) > cap: raise RuntimeError("pinned 与近期轮次已超预算:该压缩了(7.2 节)") return out

两个实现要点:layer 标注在写入时打——工具结果落进 messages 的那一刻就该标 layer: "stale_output"(随年龄升级,如 10 轮后从 fresh 转 stale),驱逐器只做机械筛选;估算精度——字符数 / 3 是示意,生产换所用模型的分词器,或直接用 API 返回的用量校准(第 9.3 节账本顺便提供校准数据)。

💡 驱逐是"每一步都可能发生的小事",压缩是"水位告警后的大扫除"——驱逐解决日常超支,压缩解决结构性不够用。下一节讲后者。

本节要点回顾

  1. 预算表三块不可动:常驻税(L1+L3)、输出预留(20%)、pinned 清单;可调配的只有约 2/3 窗口。
  2. 驱逐顺序四顺位:陈旧工具结果 → 已完成子任务历史 → 探索性对话 → 环境快照;pinned 与最近 K 轮永不丢。
  3. pinned 最小集:任务原文、当前计划、红线约束、关键文件引用——第四项最易漏。
  4. 丢大块保线索:截断优先于删除,尾部比头部值钱;layer 标注在写入时打。

逐层驱逐能撑过日常超支,但长任务迟早触到水位线——那时候需要的不是再截一段,而是把历史整体换一种形态。下一节:压缩与摘要。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U