Agent 为什么必须有 Token Budget?
上一篇我们给 Agent Loop 加了 maxSteps。
很多人做到这里,会松一口气:至少 Agent 不会无限跑了。
但只靠 maxSteps,控不住成本,也控不住上下文质量。
原因很简单:
同样是 8 步,
每一步 2K token 和每一步 60K token,
不是同一个系统。
一个 Agent 可以在 5 步里烧掉几十万 token。也可以在 20 步里保持很轻。真正决定成本的,不只是跑了几步,而是每一步带了多少上下文、输出了多少内容、工具结果有没有被降级、工具 schema 有没有全量暴露、失败后有没有重复把大日志塞回模型。
所以 Agent Runtime 不能只有 step budget。
还必须有 token budget。
Token Budget 不是省钱按钮
很多团队一听 token budget,第一反应是'降成本'。
这当然对,但不完整。
Token budget 同时影响三件事:
成本
延迟
判断质量
上下文太少,模型会失忆。上下文太多,模型会变慢、变贵,还可能被噪声干扰。旧日志、过期计划、全量搜索结果、无关文件片段,都可能把模型带到错误路径上。
这也是为什么长上下文不是万能解法。
更大的 context window 解决'放不放得下',不解决'该不该放进去'。OpenAI 和 Anthropic 都提供 prompt caching,说明重复前缀值得优化。但缓存只能帮你处理稳定前缀,不能替你判断动态工具结果该摘要、截断还是引用。
Runtime 要做的是预算分配,不是盲目压缩。
先把预算拆成四类
一个可用的 Agent Budget 不该只有 maxTokens。
我会先拆成四类:
type RunBudget = {
maxSteps: number;
maxModelCalls: number;
maxToolCalls: number;
maxRunTokens: number;
maxInputTokensPerStep: number;
maxOutputTokensPerStep: number;
maxWallClockMs: number;
};
这里每一项都解决不同问题。
maxSteps 防止 loop 无限跑。
maxModelCalls 防止某些实现里一次 step 内多次模型请求。
maxToolCalls 防止模型一轮里并行扔出一堆工具调用。
maxRunTokens 控制整个 run 的总预算。
maxInputTokensPerStep 控制每一步上下文大小。
maxOutputTokensPerStep 控制模型输出。
maxWallClockMs 控制任务耗时。
这些预算都应该进 trace。否则出了成本事故,大家只知道'这个任务很贵',不知道贵在哪里。
Context Builder 是预算执行器
预算不是写在配置里就生效。
真正执行预算的是 Context Builder。
本书 demo 里有一个 BudgetedContextBuilder,它会估算每条 message 的 token,把超出预算的内容放进 droppedItems。教学版很朴素,但方向对:Context Builder 不负责把所有东西塞给模型,它负责在预算内装载当前最该出现的事实。
一个更完整的结果应该长这样:
type ContextBuildResult = {
messages: RuntimeMessage[];
report: {
budget: number;
used: number;
mustKeep: string[];
included: string[];
summarized: string[];
dropped: string[];
reasons: Record<string, string>;
};
};
这个 report 很重要。
没有它,你只能说'模型忘了'。有了它,你能看到用户约束有没有保留,关键文件有没有进入上下文,测试日志是全量、片段、摘要,还是 artifact 引用。
预算要可解释,不然就会变成另一种玄学。
装载顺序要先保命
超预算时,不能谁先来谁占位置。
装载顺序建议这样:
1. 系统安全边界
2. 当前用户目标和明确约束
3. 当前 run 状态和未完成事项
4. 最近失败原因
5. 相关文件片段和错误栈
6. 工具结果摘要
7. 历史摘要
8. 低相关旧日志
真正不能轻易丢的是:
- 用户当前目标。
- 用户明确禁止事项。
- 安全和权限策略。
- 最近失败原因。
- 当前计划和未完成事项。
- 直接证据。
可以先降级的是:
- 旧工具日志。
- 全量搜索结果。
- 重复测试输出。
- 过期计划。
- 低相关历史。
我见过最糟糕的上下文策略,是为了塞更多日志,把用户最新要求摘要掉。模型最后跑偏,团队还以为是模型能力问题。
不是。
是预算策略把最该保留的东西挤掉了。
工具输出必须有降级策略
工具输出是 token 黑洞。
一次 npm test 失败,可能有几万字符。一次 rg 搜索,可能几百行。一次构建失败,日志能把上下文撑爆。
工具结果进入模型前,应该分四档:
full:短配置、短搜索结果
snippet:错误栈顶部、关键行号
summary:长日志结构化摘要
artifact_ref:完整内容存外部,只给引用
工具定义里要写输出策略:
type ToolOutputPolicy = {
maxCharsForModel: number;
summarizeWhenLarge: boolean;
artifactWhenLarge: boolean;
redactSecrets: boolean;
};
返回给模型时,要明确告诉它看到的是摘要:
{
"content": "npm test 失败:math.test.ts 第 8 行,期望 3,实际 -1。",
"truncated": true,
"artifactRef": "artifact:run_12_step_5_npm_test"
}
不要默默截断。
模型不知道自己看到的是半截证据,就会把半截当全量。
预算耗尽时,不要假装完成
预算耗尽应该是明确 stop reason。
type StopReason =
| "final"
| "max_steps"
| "budget_exhausted"
| "repeated_error"
| "permission_denied"
| "user_cancelled";
当 remainingRunTokens 不够下一轮安全执行时,Runtime 应该停下来:
function shouldStopForBudget(state: {
remainingRunTokens: number;
minTokensForNextStep: number;
}) {
if (state.remainingRunTokens < state.minTokensForNextStep) {
return {
stop: true as const,
reason: "budget_exhausted" as const,
};
}
return { stop: false as const };
}
最终回答也要讲清楚:
我没有继续执行,因为本次任务的 token 预算已经不足以安全进入下一步。
已完成:
- 读取了 package.json。
- 定位到 src/runtime/loop.ts。
- 发现测试失败来自 run_command timeout。
建议:
- 缩小任务范围。
- 或允许我开启新的 run 继续分析。
预算停止不是失败。胡乱继续才是失败。
Token 估算不准也要做
很多人会说:本地 token 估算不准。
对,不准。
但粗估也比没有强。
第一版可以用字符数粗估:
function estimateTokens(text: string) {
return Math.ceil(text.length / 4);
}
中文、代码、JSON、工具 schema 的 token 比例都不一样。后面可以换成 provider tokenizer,或者用模型响应里的 usage 校正。
一个简单策略:
发送前:粗估,决定能不能发。
发送后:读取 provider usage,校正账本。
报告里:同时记录 estimated 和 actual。
不要因为估算不完美,就放弃预算。
工程上很多控制都是先粗后细。
Prompt caching 怎么放进预算
OpenAI 和 Anthropic 都支持 prompt caching。Agent Runtime 可以利用它,但别把它当预算系统。
适合缓存的内容:
- 稳定 system prompt。
- 开发者策略。
- 稳定工具 schema。
- 项目固定背景。
- 长期不变的文档片段。
不适合当缓存核心的内容:
- 当前用户目标。
- 最新工具结果。
- 当前错误日志。
- 权限决策。
- checkpoint 恢复说明。
所以 Context Builder 最好把稳定内容放前面,动态内容放后面。工具 schema 保持稳定顺序,也有助于缓存命中。
缓存能减少重复前缀成本,但不能替你决定'这次该给模型什么事实'。
Budget 要进 eval
预算策略改了,行为可能变。
比如你把搜索结果从 50 条降到 10 条,成本降了,但某些任务可能找不到入口。你把最近历史从 8 轮降到 4 轮,延迟降了,但长任务可能丢掉决策。你把 maxSteps 从 20 降到 10,成本降了,但复杂任务通过率可能掉。
所以 eval 报告不能只看 pass rate。
至少记录:
平均 step 数
平均 tool call 数
平均 input token
平均 output token
平均 latency
估算成本
budget_exhausted 次数
输出截断次数
被 drop / summarize 的上下文项
成本门禁可以先写得很简单:
{
"costPolicy": {
"maxEstimatedUsdPerTask": 0.25,
"maxStepsPerTask": 20,
"maxToolCallsPerTask": 40,
"maxP95LatencyMs": 120000
}
}
这不是抠门。
这是防止一次策略调整,把简单任务拖成昂贵长跑。
第一版实现清单
如果你已经有了前面文章里的 Agent Loop,可以按这个顺序加预算:
1. 在 RunState 里加入 tokenUsage。
2. 在每个 step 记录 estimatedInputTokens。
3. 读取模型返回的 actual usage。
4. Context Builder 输出 budget report。
5. 工具结果增加 truncated、artifactRef。
6. 加 maxRunTokens。
7. 加 maxInputTokensPerStep。
8. budget_exhausted 作为 StopReason。
9. trace 记录每步 token 和 droppedItems。
10. eval 报告加入 token/cost 指标。
先别追求完美。
先让系统知道自己花了多少,以及为什么花。
最后
Agent 不是一次模型请求。
Agent 是一个 run。
run 里有 step,有 tool call,有 retry,有上下文构建,有 trace,有 eval。没有 token budget,Agent 就像一辆没有油表的车:能跑,但你不知道什么时候该停,也不知道油耗为什么突然高。
maxSteps 控制它别无限循环。
tokenBudget 控制它别带着整座仓库跑马拉松。
两者都要有。
参考资料
- OpenAI API Docs: Prompt caching
- OpenAI API Docs: Rate limits
- OpenAI API Docs: Production best practices
- Anthropic Docs: Prompt caching
- LangSmith Docs: Token usage and cost tracking


