Token预算制:给长期运行Agent装上「油耗表」与「熔断阀」
Agent跑久了账单失控,根因是没有预算概念。给出一套三级预算(会话/任务/租户)、预算传递机制、超限降级策略与成本归因的埋点设计。
失控不是模型贵,是没有刹车
一个数据分析 Agent 上线两周,账单从每天 200 美元涨到 1800 美元。查日志发现:一个用户的提问触发了 47 轮工具调用,每轮都把完整的历史和查询结果重新发给模型,最后一轮上下文已经超过 20 万 token。
模型没变贵,是没有人为这次会话设上限。这篇文章讲怎么把 Token 从”事后账单”变成”事前预算”。
三级预算模型
预算要在三个层级上分别设定,各自解决不同问题:
| 层级 | 覆盖范围 | 解决什么 | 越限动作 |
|---|---|---|---|
| 会话预算 | 一次对话 | 单用户暴走 | 优雅终止 + 建议新开会话 |
| 任务预算 | 一次工作流/批处理 | 流程设计失控 | 暂停待审批 |
| 租户/日预算 | 一个客户或整个产品 | 预算保护与商业模型 | 降级或限流 |
三者是包含关系,任何一级耗尽都必须停。关键是每个请求要携带”剩余预算”这个上下文。
预算传递:让每层都知道自己还剩多少
不要在各处硬编码 max_tokens。正确做法是让预算随请求传递:
@dataclass
class Budget:
remaining: int # 剩余 token
deadline_ts: float # 剩余时间
level: str # session / task / tenant
def call_model(messages, budget, reserve=800):
# 预留输出空间,避免"输入塞满导致无法输出"
usable = budget.remaining - reserve
trimmed = trim_context(messages, max_tokens=usable)
resp = llm.invoke(trimmed, max_tokens=reserve)
budget.remaining -= (resp.usage.total_tokens)
return resp
两个细节决定成败:
- 必须预留输出空间。常见 bug:输入刚好用满预算,模型返回空字符串,重试一次又花一次钱。
- 预算要在工具调用之间精确扣减。工具返回的大段文本(比如一次 SQL 查询返回 5000 行)在进入上下文前就要被压缩或摘要,否则它会把剩余预算一次吃光。
超限时怎么办:四种降级而非报错
预算耗尽直接抛异常是最差的设计——用户看到”出错了”,但其实你可以给他一部分价值。
按优先级:
- 压缩上下文继续:把早期历史摘要化,重新计算预算,继续执行
- 降档模型:剩余预算不足以支撑旗舰模型时,切到小模型完成收尾
- 收窄任务范围:“我已完成前 3 项分析,剩余 2 项需要更多资源,是否继续?”
- 优雅终止并交付中间结果:给用户已得到的内容 + 明确的继续路径
第 4 条尤其重要。一个跑到一半的报告加上”已完成 A、B,C 待补充”,比一个错误页面有价值得多。
上下文经济学:每轮都在为历史付费
在多轮 Agent 里,成本是超线性增长的,因为每轮都要重发全部历史。三种削减手段:
- 摘要压缩:超过 N 轮后,把最早 K 轮压成一段摘要(保留结论,丢弃过程)
- 工具结果瘦身:工具返回的原始 JSON 只保留模型需要的字段,别原样塞回上下文
- 提示词缓存:把稳定的系统提示和知识块放在前缀,用供应商的缓存计费,通常可省 50–90% 的输入成本
第三点需要配合前缀稳定性设计:不要在系统提示里插入时间戳、随机 ID、会话相关变量,否则缓存永不命中。把动态内容挪到用户消息里。
埋点:让成本可归因
只有能归因,才能优化。每次模型调用至少记录:
{
"trace_id": "tr_8f2a",
"tenant": "acme",
"session_id": "s_9921",
"step": 12,
"step_name": "sql_query",
"model": "gpt-5",
"input_tokens": 18234,
"output_tokens": 420,
"cached_tokens": 16000,
"cost_usd": 0.031,
"budget_remaining": 4100
}
然后做三张表,每周看一次:
- 按步骤分组的 token 分布:通常会发现某一个环节吃掉 60%(常见是”读文件/查数据库”)
- 单任务成本分布:看 P95 而非平均值。P95 是 5 美元、均值是 0.3 美元,说明有长尾异常
- 缓存命中率趋势:突然下降通常意味着有人改了系统提示的前缀
三个真实的反模式
- 在工具里做 LLM 调用但不记账:工具内部的模型调用容易被漏计,导致预算体系和实际账单对不上。所有模型调用必须走同一个 client 封装。
- 无限重试:工具失败后自动重试,每次重试都是一次完整上下文。必须给重试次数设上限(建议 2 次),且重试时收缩上下文。
- 预算只按 token 不按时间:一个 Agent 可能 token 很少但跑了 10 分钟。时间预算和 token 预算要同时设。
落地顺序
- 第一周:所有模型调用收敛到一个 client,加埋点,先看清账
- 第二周:加会话级预算与四条降级策略
- 第三周:加任务与租户级预算,接告警
- 第四周起:按步骤分布做针对性优化(工具瘦身、摘要、缓存)
先度量再优化。没有归因数据的成本优化,基本等于凭感觉砍功能。