本节摘要:预算恒等式给了科目,本节给分配算法。第一步是写预算分配表:以一个 128k 窗口的客服 Agent 为例(示意数字),常驻层(指令 4% + 工具 10%)与输出预留(20%)先扣,剩余在记忆与素材之间倾斜分配——素材是主战场,因为它是总量最大、增长最快的成分。第二步是"不可动的三块":常驻层(刚性税)、输出预留、pinned 清单(任务原文、当前计划、红线约束、关键引用——丢了它们后面全白干)。第三步把"优先级"翻译成工程上可执行的驱逐顺序:超限时依次丢陈旧工具结果 → 已完成子任务的历史 → 早期探索对话 → 可重取的环境快照,每层内部"丢大块保线索"(截断优于整条删除,尾部比头部值钱)。最后给出
ce_budget.py预算核算示意,并交代重新分配的三个触发时机。
阅读完本节,你应当能够:
ce_budget.py,把四成分账本接到自己的应用上。以"128k 窗口的客服 Agent,带知识库检索与 15 个工具"为例(全部为示意数字):
| 科目 | 内容 | 占比(示意) | 性质 |
|---|---|---|---|
| 指令 | 系统提示(角色 / 边界 / 契约) | 4% | 常驻税 |
| 工具 | 15 个工具 schema | 10% | 常驻税 |
| 记忆 | 长期记忆召回(用户偏好) | 3% | 动态 |
| 记忆 | 会话历史 | 16% | 动态(线性增长) |
| 素材 | 检索片段 + 工具结果 | 42% | 动态(最大头) |
| 输出预留 | 本轮生成空间 | 20% | 不可动 |
| 机动 | 未分配缓冲 | 5% | 应对突发长输出 |
两条读表结论:其一,常驻税 + 输出预留 ≈ 1/3——供料策略真正能调配的只有 2/3 窗口,这是很多人配预算时漏掉的前提;其二,素材拿最大份额是常态——它单条就可达数千 token,且质量上限最高(一条精准片段顶十轮寒暄)。
💡 分配表是"设计期预算",不是"运行期事实"。运行时每轮的实际占用会偏离它——偏离的校正机制就是下面第三节的驱逐顺序。
⚠️ pinned 不是"越多越安全":pinned 条目本身也占预算,全 pin 等于没有驱逐空间。工程纪律是只 pin 上述四类,其余一切皆可驱逐。
说"素材的优先级低于记忆"太空洞,工程上直接写谁先丢:
| 顺位 | 被驱逐的内容 | 为什么它最先丢 | 丢了之后的补偿 |
|---|---|---|---|
| 1 | 陈旧工具结果(旧日志、已读过的文件原文) | 时效最差:文件还在盘上、日志可重查 | 引用化——留"路径 + 一行摘要"占位 |
| 2 | 已完成子任务的历史 | 结论已沉淀进当前计划,过程无价值 | 结论并入 pinned 的计划条目 |
| 3 | 早期探索对话(试错讨论) | 信息密度低 | 关键发现并入任务简报(第 6 章,后半) |
| 4 | 可重取的环境快照(文件树、状态查询) | 下一步需要时可再取 | 按需再查 |
| — | pinned 四项 + 最近 K 轮(K 取 3~5,示意) | 不驱逐:任务丢了等于失忆;最近轮丢了模型接不上话 | — |
排位的统一逻辑是重取成本:越容易重新获取的内容越先丢。文件可以重读、快照可以重查、结论已经沉淀——丢它们的代价最小;而任务原文与最近对话不可重取,丢一次就永久失忆。
每层内部还有一条细则——丢大块保线索:一条 3,000 token 的测试输出,整条删除不如截成"首 200 + [中间截断] + 尾 200"。尾部尤其值钱:编译器与测试的结论(成败、错误摘要)几乎总在末尾。
ce_budget.py:预算核算示意# ce_budget.py —— 四成分预算核算与驱逐建议(写法示意,以实际工程为准) # 成分口径与 1.2 节四成分表一致;后半各章(压缩 / 框架)请沿用此口径 WINDOW = 128_000 # 总窗口 token(示意) RESERVE = 0.20 # 输出预留比例(示意) PINNED_TAGS = ("task", "plan", "redline", "keyref") # pinned 最小集四类 RECENT_K = 4 # 最近 K 轮不驱逐(示意) # 驱逐顺序:重取成本从低到高;pinned 与最近 K 轮不在序列内 EVICT_ORDER = ["stale_tool_output", "done_subtask_history", "early_chat", "env_snapshot"] def est_tokens(text: str) -> int: """粗估 token(示意):中文约 1 字 ≈ 1 token,其余约 4 字符 ≈ 1 token。 生产应换所用模型的分词器,或用 API 返回的用量回填校准。""" cn = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff") return cn + (len(text) - cn) // 4 def audit(entries: list[dict]) -> dict: """entries 每条形如 {"kind": 指令/工具/记忆/素材, "tag": 具体标记, "text": 内容, "turn": 所在轮次}。返回成分账本与总占用。""" cap = int(WINDOW * (1 - RESERVE)) # 输出预留先扣 ledger: dict[str, int] = {} for e in entries: ledger[e["kind"]] = ledger.get(e["kind"], 0) + est_tokens(e["text"]) return {"cap": cap, "ledger": ledger, "used": sum(ledger.values())} def evict_until_fits(entries: list[dict], max_turn: int) -> list[dict]: """超预算时按 EVICT_ORDER 逐层驱逐;pinned 与最近 K 轮永不丢。""" out = [dict(e) for e in entries] def protected(e): # 保护判定:pinned 标记或最近 K 轮 return e["tag"] in PINNED_TAGS or e["turn"] > max_turn - RECENT_K for layer in EVICT_ORDER: if audit(out)["used"] <= audit(out)["cap"]: break out = [e for e in out if e["tag"] != layer or protected(e)] for e in out: # 还超:丢大块保线索(尾部敏感,首尾各留 200 字符) if e["tag"] == "stale_tool_output" and not protected(e) and len(e["text"]) > 800: e["text"] = e["text"][:200] + "\n…[中间截断]…\n" + e["text"][-200:] if audit(out)["used"] > audit(out)["cap"]: raise RuntimeError("驱逐后仍超预算:该触发压缩了(第 6 章,后半)") return out
三个实现要点:tag 在写入时打——工具结果落进消息列表的那一刻就标 stale_tool_output(随年龄升级,如 10 轮后 fresh 转 stale),驱逐器只做机械筛选;每轮审计——audit() 的输出顺手就是第 6 章(后半)可观测的账本;抛错的含义——驱逐是日常超支的止血带,抛错说明结构性不够用,出路是压缩或按需检索,不是更狠的截断。
预算分配表不是一次写死的:任务阶段切换(检索期素材占比高、写码期工具结果占比高)时要换表;水位逼近上限(如用量达 80%,示意)时触发压缩(第 6 章,后半);成分结构变化(新增 10 个工具 = 常驻税上涨)时全表重算。
ce_budget.py 抛错的那一行,就是压缩的入口。量的问题解决了——每个科目给多少、超了砍谁。但同样的内容按不同顺序摆,命中率可以差出一截:模型对窗口的开头、结尾与中部并不一视同仁。下一节讲位置效应。