1. 预算烧穿那一刻,压缩不是“省字”而是调度决策
我在一个跑 40 分钟以上的 Agent 长任务里踩过一个很典型的坑:第 27 步模型开始重复确认已经定下来的目录结构,第 31 步把两轮前已经否掉的方案重新提出来,第 35 步直接抛context_length_exceeded。这不是模型变笨,是 harness 层缺少预算检查——它只知道往后追加消息,不知道在哪个水位该卸载、在哪个水位该压缩、压缩一次又要花掉多少 Token。后来我把长任务的调用入口统一收拢到 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_intro),用同一个 Key 在 https://taotoken.net/api 上出请求,才终于能把“预算检查”“压缩调用”“上下文回灌”三件事的 Token 账对齐到同一张表里。这篇就按 Agent 调度工程师的视角,把长任务里四类上下文机制——预算与卸载、压缩、todo-state 复述、跨会话记忆——的触发阈值、Token 开销和切换配置拆开讲清楚,最后给一份可以直接抄的阈值配置和不同压缩类型的 Token 对照。
先说结论:压缩不是等到报错才做的补救,它是一次有成本的调度动作。压缩调用本身要读一遍剩下的上下文、再写出一份更短的内容,这两头都在烧 Token。如果你的预算检查只盯着“当前上下文多长”,不盯着“压缩这次要花多少”,那大概率会出现一个尴尬场面——为了省 Token 而触发的压缩,直接把剩余预算吃掉三分之一。
所以本文所有阈值都围绕一个前提设计:预算检查与压缩调用也是 Token 消耗方,必须单独建账。
2. 先把账本立起来:预算检查本身要花多少 Token
很多同学写 harness 的时候,预算检查是这么写的:每一步都把完整消息列表丢给模型,问它“现在大概占了多少 Token”。这个做法在长任务里是自杀式的,因为你每次都在为“统计”付一次全量上下文的价格。
正确的拆分是三层:
第一层是本地估算。用对应模型的 tokenizer 或者社区近似算法,对当前 transcript 做一次离线计数,误差控制在 5% 以内就够用。这一步零 API 成本。
第二层是周期性校准。每 N 步(我一般设 3 步)拿一次真实用量,和本地估算对齐,修正系数。真实用量可以从流式响应里累计input_tokens/output_tokens得到。
第三层是压缩预算预留。在总预算里切出一块固定的“压缩专用额度”,跟主任务额度分开记账。这样即使压缩调用比预期贵,也不会把主任务的推理预算吃穿。
账本字段建议至少包含这些:
{ "window_tokens": 200000, "reserve_output_tokens": 16000, "compact_budget_tokens": 12000, "compact_spent_tokens": 0, "estimate_ratio": 1.0, "last_calibrate_step": 0, "calibrate_every_steps": 3 }window_tokens是模型窗口,reserve_output_tokens是留给最终输出的,compact_budget_tokens是这块任务允许在压缩上花掉的总额度。真正参与水位判断的可用额度是:
usable = window_tokens - reserve_output_tokens ratio = used_tokens / usable注意used_tokens里要包含 tool 返回的原始内容。Agent 长任务里最容易被忽略的一类膨胀源就是工具输出——一次目录树、一次全量日志、一次 diff,都能瞬间把上下文推高几万 Token。这些内容不该长期驻留在 transcript 里,它们属于“卸载”的范畴,不是“压缩”的范畴。
3. 四类压缩的触发阈值怎么定
这一步是全文的核心。四类机制对应四类不同的问题,触发水位也不同,混着用就会出现“明明压缩了却还是丢失目标”的情况。
第一类:上下文预算与卸载(offload)。解决的是“工具原始输出太长”。做法是把大块内容写到外部存储(本地文件、对象存储、记忆库),transcript 里只留一个指针加一句摘要。触发水位我建议放在 0.55——也就是可用额度用掉一半多的时候就开始卸载,而不是等满了再卸。卸载的成本几乎为零,越早做越好。
第二类:压缩(compaction)。解决的是“历史对话太啰嗦”。把前 N 轮的消息合并成一份结构化摘要。触发水位放 0.70。这里有个关键点:压缩一定要产出结构化内容,不要产出自由散文。自由散文的摘要里,模型最容易丢的就是“已经否决的方案”和“硬约束”,而这两样恰恰是长任务跑偏的主要来源。
结构化摘要的 schema 建议固定为五段:
goal: 当前任务的最终目标(一句话) constraints: 不可违反的硬约束(列表) decisions: 已确认的决策 + 被否决的方案及原因 artifacts: 已产出的文件路径 / 函数名 / 接口签名 open: 未决问题和下一步待办第三类:todo-state 复述(restate)。解决的是“目标漂移”。做法是不做内容压缩,而是周期性把当前目标、已完成项、待办项、验收条件重新回灌到上下文尾部。触发水位放 0.75,并且每 K 步强制执行一次,不管水位。我一般 K 取 8。复述内容的 Token 非常小,通常几百个,但对缓解目标丢失的效果立竿见影。
第四类:跨会话记忆(memory)。解决的是“任务跨进程 / 跨天重启后从零开始”。做法是把长尾事实写入外部记忆库,需要时按查询召回。写入触发水位 0.55(和卸载同步做),召回触发水位 0.75。
把四类放在一张表里对照:
| 机制 | 解决的问题 | 触发水位 | 回灌 Token(约) | 单次调用开销 |
|---|---|---|---|---|
| 卸载 offload | 工具输出膨胀 | 0.55 | 50~200 | 接近 0(本地写入) |
| 压缩 compact | 历史对话啰嗦 | 0.70 | 1.5k~3k | 读全量 + 写摘要 |
| todo 复述 | 目标漂移 | 0.75 / 每 8 步 | 300~600 | 接近 0(本地拼装) |
| 记忆召回 | 跨会话断点 | 0.75 | 300~800 | 单次检索 + 注入 |
| 硬停 hard stop | 兜底 | 0.88 | — | 中止并落盘 |
再给一份不同压缩类型的 Token 对照。测试场景是一个已经跑到约 120k Token 的编码类长任务,输出预留 16k,可用额度 184k:
| 压缩类型 | 回灌 Token(约) | 压缩调用开销(约) | 保留度 | 适用水位 |
|---|---|---|---|---|
| 不压缩全量回灌 | 120k | 0 | 100% | 仅作基线 |
| 自由文本摘要 | 2.5k~4k | 读 120k / 写 3k | 中,约束易丢 | 0.70 |
| 结构化摘要 | 1.2k~2k | 读 120k / 写 1.5k | 高 | 0.70 |
| todo-state 复述 | 300~600 | 读 120k / 写 0.4k | 目标保真,细节不保 | 0.75 |
| 卸载 + 指针 | 50~200 | 本地写入 | 内容可回溯 | 0.55 |
| 记忆写入 + 按需召回 | 300~800 | 写 1k / 召回 0.6k | 长尾事实可检索 | 0.55 写入 / 0.75 召回 |
这张表的意义在于:压缩类型不同,Token 量级差一个数量级。用自由文本摘要去处理工具输出,等于花高价买了一个还不一定记得住的结果;用卸载去处理历史决策,又等于什么都没压缩,因为决策本来就不该被卸载掉。
阈值配置可以直接落成一份 JSON:
{ "budget": { "window_tokens": 200000, "reserve_output_tokens": 16000, "compact_budget_tokens": 12000 }, "triggers": { "offload_ratio": 0.55, "compact_ratio": 0.70, "restate_ratio": 0.75, "memory_recall_ratio": 0.75, "hard_stop_ratio": 0.88, "restate_every_steps": 8 }, "compact_schema": [ "goal", "constraints", "decisions", "artifacts", "open" ] }4. 把 Base URL 切到 TaoToken:Claude Code / Codex / CC Switch
阈值配好之后,下一步是让压缩调用真的走同一条链路。如果你在同一个长任务里混用了多个供应商,压缩调用的 Token 就不在同一个账本上,前面做的预算对齐全白费。
先去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_key 拿到 Key,然后在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_keys 创建一把长期 Key。后面所有配置里的YOUR_API_KEY都换成它。
Claude Code:用 settings.json 或 ANTHROPIC_环境变量。*
~/.claude/settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-5", "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5" } }如果你更喜欢 shell 层注入:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export ANTHROPIC_MODEL="claude-sonnet-4-5" export ANTHROPIC_SMALL_FAST_MODEL="claude-haiku-4-5"ANTHROPIC_SMALL_FAST_MODEL建议单独指向一个便宜的小模型。todo-state 复述、轻量分类、摘要拼接这类低智力密度调用可以走它,能明显压低压缩总开销。
Codex:用 config.toml,不要套 ANTHROPIC_*。
~/.codex/config.toml:
model = "gpt-5-codex" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "responses"Key 单独放环境变量,不要写进 toml:
export TAOTOKEN_API_KEY="YOUR_API_KEY"这里强调一遍:Claude Code 用ANTHROPIC_*,Codex 用config.toml+env_key,两套体系不能互相套用。把ANTHROPIC_BASE_URL写到 Codex 的配置里,或者反过来,只会得到一堆 401 和连接超时。
CC Switch:记住三件套 = 供应商名 + Base URL + API Key。
CC Switch 在切换 Claude Code 供应商时,实际改的就是三个字段:
供应商名: taotoken Base URL: https://taotoken.net/api API Key: YOUR_API_KEY切完之后一定做两件事:一是重启终端里已有的 Claude Code 会话,环境变量是进程级缓存的;二是跑一次/status或等价命令确认当前生效的 Base URL。多供应商并存时最容易出的问题就是“配置改了但没生效”,长任务跑到一半才发现压缩调用打到了旧地址。
切换到 TaoToken 之后,建议把官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_switch 上的模型列表和文档对照一遍,确认你要用的压缩模型和目标模型都在可用范围内。
5. 压缩后目标丢失的排障顺序
压缩触发之后,最常见的三类异常和对应的排查顺序如下。
症状一:压缩后立刻重复之前的动作。大概率是结构化摘要里decisions字段丢了。检查方法:把压缩产出的摘要单独打印出来,数一下里面有没有“被否决的方案”。如果只有结论没有过程,模型就会重新走一遍已经被否掉的路。修法是让压缩调用产出的 schema 强制包含decisions,并且要求每条决策后面必须跟一句否决理由。
症状二:压缩后整体跑偏,开始做任务目标之外的事。这是 todo-state 没有复述的典型表现。压缩只保留内容,不保留“我们现在在干什么”。修法是把复述频率提高到每 5 步一次,并且把goal字段放在 transcript 最末尾——模型对上下文尾部的注意力最强。
症状三:压缩调用本身就超预算。表现为压缩之后剩余额度比压缩之前还紧张。原因是压缩调用没有被单独记账。修法是把compact_budget_tokens从主预算里切出来,压缩调用前先检查这块额度还剩多少,超了就直接降级到 todo-state 复述,不做全量摘要。
症状四:跨会话恢复后重新探索目录结构。说明记忆写入做了,但召回没做,或者召回索引对不上。检查方法:看恢复后的第一轮请求里有没有注入记忆召回结果。修法是给记忆库加一个按artifact path的索引,恢复时用任务名做一次定向召回,而不是泛召回。
症状五:直接抛context_length_exceeded。如果阈值都配了还报这个错,通常是本地估算系数偏了。把estimate_ratio往上调 0.05 再跑一轮,或者把calibrate_every_steps从 3 改到 1,先确认估算和真实用量对齐。
排障的时候有一个通用原则:先看压缩产出的内容,再看触发水位,最后看配置链路。绝大多数“压缩没用”的案例,问题都出在压缩产物质量上,而不是阈值上。
6. 把压缩闸门接进 harness 主循环
阈值配置和供应商配置都准备好之后,最后一步是把它接进主循环。下面是一个本地执行的守卫函数示例,逻辑是“先校准、再判断、再决定动作、最后记账”:
BUDGET = { "window_tokens": 200_000, "reserve_output_tokens": 16_000, "compact_budget_tokens": 12_000, "compact_spent_tokens": 0, "calibrate_every_steps": 3, "restate_every_steps": 8, } def guard(used_tokens: int, step: int, compact_cost_estimate: int): usable = BUDGET["window_tokens"] - BUDGET["reserve_output_tokens"] ratio = used_tokens / usable remain_compact = BUDGET["compact_budget_tokens"] - BUDGET["compact_spent_tokens"] # 硬停兜底 if ratio >= 0.88: return {"action": "hard_stop", "reason": "budget_exhausted"} # 目标复述:低水位也按步数强制触发 if ratio >= 0.75 or step % BUDGET["restate_every_steps"] == 0: return {"action": "restate_todo"} # 压缩:额度不够就降级 if ratio >= 0.70: if remain_compact >= compact_cost_estimate: BUDGET["compact_spent_tokens"] += compact_cost_estimate return {"action": "compact_structured"} return {"action": "restate_todo", "reason": "compact_budget_low"} # 卸载:最早触发,成本最低 if ratio >= 0.55: return {"action": "offload_tool_output"} return {"action": "continue"}几个实现细节值得留意。
第一,compact_cost_estimate要按“输入全量 + 输出摘要”两头估,只估输出会低估三到五倍。
第二,restate_todo的触发条件用了or,意味着它既受水位驱动也受步数驱动。这个设计是为了防止一种情况:任务在低水位区反复小步前进,走了 30 步都没触发复述,目标早就飘了。
第三,compact_budget_low的降级路径一定要有。压缩额度用完时,退回到 todo-state 复述比强行压缩安全得多——前者最多丢细节,后者可能把上下文挤出窗口。
第四,所有触发动作都要落日志。长任务出问题时,日志里的触发序列比任何事后分析都直接。
接好之后,跑一个长任务验证一下。观察指标是三个:压缩触发次数、压缩累计 Token、任务最终是否命中目标。理想状态下,压缩触发 2 到 4 次,累计压缩 Token 控制在总预算的 8% 以内,任务目标不变。
7. 一条可复现的验证路径
把上面所有内容串起来,一条完整的验证路径是:
第一步,在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_flow 拿 Key,在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_flow_key 建一把长期 Key。
第二步,按第 4 节把 Claude Code 或 Codex 的 Base URL 统一指向 https://taotoken.net/api,确认切换生效。
第三步,把第 3 节的阈值 JSON 落进 harness 配置,把第 6 节的守卫函数接进主循环。
第四步,跑一个至少 20 步的长任务,记录每次触发的动作类型和当步 Token 用量。
第五步,对照第 3 节的 Token 对照表,看实际压缩开销落在预期区间没有。偏差超过一倍,先查估算系数,再查压缩 schema 是不是退化成自由文本了。
第六步,做一次跨会话恢复,验证记忆召回是否生效。
如果你还没决定用哪个模型来跑压缩调用,可以先到 https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_chat 对比一下各模型在长上下文场景下的表现,再决定主模型和小模型的搭配。
长任务跑得多的话,https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_plan 里的 Coding Plan 可以直接覆盖编码类工作流,配合上面的阈值配置用起来更省心;Claude Code 侧的具体接入细节在 https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=budget_compact_doc 有完整说明。
回到最开始那个坑:Agent 长任务真正难的不是模型能力,而是调度层愿不愿意把 Token 当成一种需要精算的资源。预算检查、卸载、压缩、todo-state 复述、跨会话记忆,这五件事分开看都不复杂,难的是把它们放进同一本账里,并且在正确的水位触发正确的动作。阈值配错一次,你可能要多花三倍预算才能跑完同一件事。