第 26 章 · Token 预算控制
一封凌晨发来的账单预警
那个做跨境电商的团队,上线了一个“自动回复买家询盘”的 Agent。白天跑得挺好:买家问运费、问尺码、问能不能改地址,Agent 查订单、算运费、拟回复,人工点一下确认就发出去。运营很满意。
出事是在一个周五的夜里。一个买家发来一段很啰嗦的多语言询盘,里面还贴了半页商品参数表。Agent 读进去、查库存、查物流、又去查了汇率,觉得“信息还不够”,于是再查一轮——它陷进了第 3 章讲过的那种停不下来的循环。没有人盯着,它就这么转了一整晚。
周六早上,负责人被 provider 的账单预警短信叫醒:一夜之间,光这一个会话就烧掉了平时一周的调用额度。更糟的是,账单是事后才看到的——等你收到预警,钱已经花完了。
这一章要解决的就是这件事:怎么给 Agent 套上一根“花钱的缰绳”,让它在花之前就被拦住,而不是花完之后才心疼。这根缰绳,就是 token budget(Token 预算)。
为什么 Agent 的成本天生难控
传统程序的成本是可预估的:一次请求走固定的几步,算力基本恒定。Agent 不一样——它是自主循环的,每一步都是一次大模型 API 调用,而跑多少轮、调多少次工具、塞多少上下文,是模型自己临场决定的。这就带来三种典型的失控:
- 单次任务失控:一个陷入死循环的 Agent,可能反复调同一个工具、始终“觉得没做完”,一夜烧掉一大笔(就是开头那个故事)。
- 上下文膨胀:长任务里每轮都把巨大的工具输出原样塞回对话,越滚越大。而模型计费是按 token 算的,上下文越长,每一轮都更贵——成本是随轮数累加的。
- 恶意或异常输入:一个用户(有意或无意)构造出让 Agent 疯狂调用的任务,把你的成本额度吃干。多 Agent 系统(第 5 部分)里这个风险还要翻几倍。
在生产环境,成本失控就是一起事故,和线上宕机同级。所以预算控制不是“锦上添花的优化”,而是能不能上生产的硬门槛。
把预算当成一等约束
先把核心思想说透:要像对待“正确性”那样对待“成本”,把它当成一个一等约束(first-class constraint),在多个层面设下明确的上限。
具体的控制手段有这么几类,逐个讲清术语:
- 单任务预算(per-task budget):一个任务最多花 N 个 token 或 N 元,超了就停。这是最直接的一道闸。
- 单用户配额(per-user quota):每个用户在一段时间(比如每天)内的额度上限,防止个别用户——不管是不是恶意——把整体成本拖垮。这在多租户(multi-tenancy)系统里尤其关键。
- 循环护栏(loop guardrails):给循环本身设最大轮次(max iterations)和最大工具调用次数(max tool calls)。这两道是第 3 章就强调过的硬刹车,防的就是死循环。
- 模型分层(model tiering):便宜的活用便宜的模型,只在关键推理处才动用贵模型(第 34 章会细讲)。同样一个任务,把简单步骤降级到小模型,成本能差一个数量级。
- 上下文压缩(context compaction):控制上下文长度,本质上就是控制成本(第 9 章)。及时把冗长的历史摘要掉,每一轮省下的是真金白银。
这几类手段里,有一个共同的、也是最容易被忽略的原则:预算检查必须嵌进循环里。
“事中拦截”而不是“事后算账”
开头那个故事的教训,正是这一点。很多人第一反应是:跑完任务后统计一下花了多少钱,超了就告警。这叫事后算账——钱已经花出去了,告警只是让你知道“完蛋了”。
正确的做法是事中拦截:把成本检查放进循环的每一步,每转一轮都问一句“我还在预算内吗?”,一旦超了就优雅地中断,而不是等任务自然结束。用第 3 章那个循环刹车的思路来写,判断逻辑大致是这样:
function shouldStop(state) {
if (state.iterations >= state.maxIterations) return "到达最大轮数";
if (state.toolCalls >= state.maxToolCalls) return "工具调用超限";
if (state.costUSD >= state.budgetUSD) return "超出预算";
return null; // 都没触发,继续转
}
这段判断被放在每一轮循环的开头执行。区别就在这里:事后算账是任务结束后跑一次,事中拦截是每一步都跑一次。前者只能告诉你“已经超了”,后者能在超支的那一步之前就把循环停掉。多花的,最多是当前这一轮的钱,而不是接下来一整晚的钱。
用 pi 实现:先度量,再控制
要控制成本,前提是能准确度量成本——你没法管一个你测不准的东西。这正是 pi 的 pi-ai 层(第 23 章)做的事:它统一了多个 provider(OpenAI / Anthropic / Google / Agnes),把每次模型调用的 token 用量和费用都记录下来。它的增量能力里明确包含两项对预算很关键的东西:
- service-tier(服务档位)定价:很多 provider 提供不同的服务档位——比如标准(standard)、优先(priority)、批处理(batch)——价格和延迟各不相同。批处理慢但便宜,优先快但贵。
pi-ai支持按 tier 计价,意味着你能在“快而贵”和“慢而省”之间做成本权衡,而且账算得准。 - 重试分类(retry classification):网络抖动、限流、真错误——
pi-ai会区分对待。这关系到成本准头:一次因为限流而重试的调用,不该被重复计费两次。
有了准确的度量,预算控制就能用扩展(第 8 章)挂在循环的生命周期事件上。核心是两个钩子:afterModelCall 负责累加成本,beforeTurn 负责检查预算。
// 概念示意:一个把预算检查嵌进循环的扩展
export default function budgetExtension(pi, { maxCostUSD = 1.0, maxTurns = 20 }) {
let spent = 0;
let turns = 0;
// 每次模型调用之后:累加真实花费
pi.on("afterModelCall", (usage) => {
spent += usage.cost; // pi-ai 按 service-tier 给出的准确定价
});
// 每一轮循环开始之前:先问"还在预算内吗"
pi.on("beforeTurn", (ctx) => {
turns += 1;
if (turns > maxTurns) {
return { abort: true, reason: `已达最大轮数 ${maxTurns}` };
}
if (spent >= maxCostUSD) {
return {
abort: true,
reason: `已达预算上限 ${maxCostUSD}(已花 ${spent.toFixed(3)})`,
};
}
});
}
这段代码就是“花钱的缰绳”的骨架:afterModelCall 在每次调完模型后把花费加到 spent 上,beforeTurn 在下一轮开始前拿 spent 和上限比对,超了就 abort。因为 spent 来自 pi-ai 的真实定价,而不是拍脑袋估算,这道闸门是可信的。
还有一个省钱的旋钮值得一提:reasoningEffort。对支持推理档位的模型,你可以调低简单任务的推理强度——不需要深思的活,就别让模型“想太久”,这也是在直接省 token。
从 Token 成本到单位经济性
Token 是技术计量单位,不是经营决策单位。一个任务花了 20 万 Token,单看数字无法判断贵不贵:如果它只是生成一封普通邮件,可能浪费;如果它避免了一次高额缺货或完成了一次原本需要数小时的复杂审核,可能很划算。
| 观察口径 | 计算方式 | 适用场景 |
|---|---|---|
| 单任务 AI 成本 | 模型 + 检索 + 工具 + 基础设施成本 / 完成任务数 | 调研、内容生成、数据分析 |
| 单业务对象成本 | 总成本 / 已处理订单、线索、工单或 SKU 数 | 订单审核、销售跟进、商品运营 |
| 人工复核成本 | 复核时长 × 人力成本 + 返工成本 | 有人机协同和审批的流程 |
| 单位有效结果成本 | AI 与人工总成本 / 正确完成且产生结果的任务数 | 比较不同模型、流程和自动化等级 |
| 成本收益比 | 新增收入或节省成本 / AI 能力总成本 | 决定是否扩量和持续投入 |
优化时不要只追求更少 Token。便宜模型如果导致更多重试、人工复核和业务错误,单位有效结果成本反而更高。正确顺序是:先守住质量与风险底线,再比较不同方案完成同一经营结果的总成本。
成本护栏负责防止失控,单位经济性负责判断值不值得。 两者都要有,但不能互相替代。
分层控制:平台、用户、任务、循环
真实生产系统的预算,从来不是一道闸,而是层层叠加的。每一层管一个粒度,任何一层被触发都能兜底:
- 平台层(platform):整个服务的月度总预算,这是财务红线。所有用户、所有任务的花费汇总起来不能越过它。
- 用户层(per-user):每个用户的配额,防止个别用户——包括开头故事里那种被异常输入拖垮的会话——独自吃掉大盘。
- 任务层(per-task):单个任务的上限,防止单点失控。上面那段
maxCostUSD就是这一层。 - 循环层(loop):最大轮次、最大工具调用次数,防的是死循环这种“技术性失控”。
从粗到细,越往下检查越频繁、越便宜。理想的设计是尽量在低层(循环层、任务层)就拦住——因为循环层的一次比对几乎不花钱,而等到平台层的月度总预算被击穿,损失已经是天文数字了。开头那个故事之所以严重,正是因为它只有事后的账单,四层里一层护栏都没有。
动手看看
打开 pi 的 pi-ai 相关代码,找一找它记录 usage 和 cost 的地方,看看 service-tier 定价是怎么落到每次调用上的——这是你所有预算逻辑的数据源,值得先看明白。
再读一读扩展文档里 afterModelCall、beforeTurn 这些生命周期事件的签名,对照上面那段 budgetExtension,想清楚:usage 对象里到底有哪些字段(token 数?分档的价格?),你的成本累加该读哪一个。
最后给自己出个小实验:把 maxCostUSD 故意设成一个很小的值(比如 0.01),跑一个稍微复杂点的任务,看它是不是真的在中途被 abort 掉、reason 里报出来的花费对不对。能亲眼看到“缰绳”被拉住的那一刻,你就真的理解它了。
实战中的几个坑
坑一:只在任务结束后算账。
- 现象:账单出来才发现某个任务超支了几十倍,钱早花完了。
- 原因:成本统计放在任务收尾处,没嵌进循环。
- 对策:把检查搬到
beforeTurn,每一轮开始前都比对一次,超了立即中断。
坑二:只设预算、不设轮次护栏。
- 现象:模型每轮花得很少,但一直转不停,累计还是烧光了预算。
- 原因:只挡了单次花费,没挡循环次数。
- 对策:预算上限和最大轮次/最大工具调用次数一起设,两道闸互补。
坑三:成本估算不准,闸门形同虚设。
- 现象:以为花了 0.3 元,实际花了 3 元,预算根本没拦住。
- 原因:用固定单价粗估,忽略了 service-tier 差异、缓存命中、重试导致的偏差。
- 对策:用
pi-ai提供的真实定价累加,别自己拍脑袋估价。
坑四:中断太粗暴,用户体验崩了。
- 现象:一到预算上限就硬切,用户看到的是一句冷冰冰的报错、任务半途而废。
- 原因:
abort时没给出可读的交代,也没保留已完成的部分结果。 - 对策:中断时返回清晰的
reason和已产出的中间结果,让用户知道“停在哪、花了多少、还能怎么办”。
对比:其他框架
先说一句实话:主流框架大多不内建“硬预算”——也就是“累计花费一超标就自动中断循环”这种能力。它们普遍提供的是成本可视化(看得见花了多少),而硬性护栏往往得你自己加,或依赖外部基础设施。
| 框架 | Token 预算怎么做 | 特点 |
|---|---|---|
| LangGraph | 用递归/步数上限限制循环长度;成本靠 LangSmith 可视化,“按累计金额中断”需在节点里自己判断并终止 | 有循环护栏,无一等硬预算 |
| CrewAI | 能拿到执行的 usage 统计,预算逻辑要自己在回调或任务边界里写 | 看得见花费,护栏靠自己 |
| PydanticAI | 不内建成本预算;可在依赖注入的上下文里累计 usage、超限抛错 | 自己实现,但写起来干净 |
| Agno | AgentOS 监控能展示成本,硬性中断通常仍需自己实现 | 强在可观测,弱在硬约束 |
| pi | pi-ai 提供含 service-tier 的准确定价,配合扩展把检查嵌进 beforeTurn,实现真正的硬预算 | 度量准 + 可嵌入循环 |
(原书对照的 Shannon 把预算控制放在 Go 编排层,作为企业级一等能力——量级更重,也更复杂;pi 走的是轻量、可嵌入的路子。)
表格里的共性一目了然:几乎所有框架都能“看见”成本,但很少有谁把“花超了就停”做成开箱即用的能力。硬预算的关键从来不是“能不能统计”,而是“能不能在每一步把检查嵌进循环、并在超标的那一步之前拦住”——这也是 pi 靠准确定价加生命周期钩子去补的那一环。
小结
- Agent 是自主循环的、每一步都花钱,跑多少轮由模型临场决定,所以成本失控就是生产事故,预算控制是上生产的硬门槛。
- 要把token budget(预算)当成一等约束,用单任务预算、单用户配额、循环护栏(最大轮次 / 最大工具调用次数)、模型分层、上下文压缩多层设限。
- 核心原则是事中拦截而非事后算账——把检查嵌进循环,
afterModelCall累加成本、beforeTurn检查预算,在超支的那一步之前就停下来。 - 分层控制从粗到细:平台层 / 用户层 / 任务层 / 循环层,尽量在低层就拦住,任何一层触发都能兜底。
- pi 的
pi-ai提供含 service-tier 的准确定价加重试分类,配合扩展的生命周期钩子,能实现主流框架大多缺席的硬预算。
预算管的是“花多少钱”这条量的红线。但生产环境还有另一条同样重要的红线——Agent 能做什么、不能做什么。下一章我们就来谈这种约束:策略治理。