Agent应用上线之后,最让团队头疼的问题之一往往不是效果不好,而是账单失控。一个多步骤的Agent在循环中不断调用大模型,每次调用看似便宜,累积起来却是一笔不小的开销。尤其是当任务陷入死循环、上下文越滚越长、或者把简单任务也丢给旗舰模型时,成本会以肉眼可见的速度膨胀。要解决这个问题,靠口头约定“少调几次模型”是不现实的,必须在工程层面建立Token监控与模型分级调度两套机制,让成本变成一个可观测、可控制的指标。

一、Agent成本为什么会失控
先看清楚问题的来源,才能对症下药。Agent的成本失控通常有四种典型模式。第一种是上下文膨胀:Agent在多轮循环中把历史消息全部带入请求,每一步的输入Token都比上一步多,形成平方级的增长曲线。一个跑二十步的Agent,最后几步的输入可能是第一步的十倍以上。
第二种是循环失控。Agent在工具调用失败后不断重试,或者规划器反复生成计划却无法判定任务完成,导致同一类请求被发送几十上百次。如果没有步数上限和预算护栏,这种循环可以持续到外部干预为止。第三种是模型选择错配,比如把“提取邮件里的日期”这类简单任务也交给旗舰模型处理,单次成本是轻量模型的十倍到几十倍,质量提升却微乎其微。第四种是隐性消耗,包括缓存未命中导致的重复计费、超长系统提示词在每个请求中重复发送、以及结构化输出失败后的整体重试。
这四类问题有一个共同点:它们在开发测试阶段往往不明显,因为测试任务少、上下文短。一旦进入生产环境面对真实流量,问题会集中爆发。所以成本治理必须作为上线前的必做项,而不是事后补救。
二、搭建Token监控体系
监控的前提是埋点。主流模型API的响应中都会返回usage字段,包含prompt_tokens、completion_tokens,部分还区分缓存命中和缓存未命中的Token数。第一步就是在所有模型调用的封装层统一捕获这些数据,连同任务ID、会话ID、模型名称、调用的Agent步骤一起写入日志或时序数据库。
下面是一个用Python编写的调用封装示例,展示了如何在统一入口处记录Token消耗:
import time
import logging
from dataclasses import dataclass, field
logger = logging.getLogger("token_monitor")
# 模型单价表:单位为美元 / 每百万Token
PRICE_TABLE = {
"flagship-model": {"input": 15.0, "output": 60.0},
"lite-model": {"input": 0.5, "output": 1.5},
}
@dataclass
class CallRecord:
task_id: str
step: int
model: str
prompt_tokens: int
completion_tokens: int
cost_usd: float = 0.0
timestamp: float = field(default_factory=time.time)
def call_model_with_monitor(client, task_id, step, model, messages, **kwargs):
response = client.chat.completions.create(
model=model, messages=messages, **kwargs
)
usage = response.usage
price = PRICE_TABLE.get(model)
cost = 0.0
if price:
cost = (usage.prompt_tokens * price["input"]
+ usage.completion_tokens * price["output"]) / 1_000_000
record = CallRecord(
task_id=task_id, step=step, model=model,
prompt_tokens=usage.prompt_tokens,
completion_tokens=usage.completion_tokens,
cost_usd=cost,
)
logger.info("token_usage|task=%s|step=%s|model=%s|in=%s|out=%s|cost=%.6f",
record.task_id, record.step, record.model,
record.prompt_tokens, record.completion_tokens, record.cost_usd)
return response, record
埋点之后需要聚合。单次调用的数据没有太大意义,真正有价值的是按任务、按用户、按模型、按Agent步骤聚合后的视图。建议至少建立四个维度的看板:单任务累计成本分布、每小时总成本趋势、各模型的成本占比、以及上下文长度随步数的增长曲线。最后一条尤其重要,它能直接暴露上下文膨胀问题——如果曲线呈加速上升,说明需要引入历史压缩或摘要机制。
再往上一层是预算控制。推荐采用三级预算:单任务上限、单用户每日上限、全局熔断阈值。预算检查要放在Agent循环内部,每完成一步就累计检查一次,超限后不是直接报错,而是走优雅降级路径:输出已完成的中间结果,标记任务为预算耗尽,方便后续审计和用户沟通。全局熔断则用于防止代码bug导致的循环失控,触发后直接拒绝新的模型调用并告警。
三、模型降级的设计与实践
监控解决的是“看得见”的问题,模型降级解决的是“花得值”的问题。核心思路是把任务按复杂度分级,让绝大多数简单请求走便宜模型,只把真正需要强推理能力的请求留给旗舰模型。实践中有三种常见的路由策略。
第一种是任务类型静态路由。在编排层为每个节点声明模型等级,比如意图识别、格式校验、简单抽取用轻量模型,规划、复杂推理、代码生成用旗舰模型。这种方式实现简单、行为可预测,适合任务结构固定的流水线式Agent。
第二种是动态难度评估。在执行前先用轻量模型判断任务难度,再决定交给谁处理。判断本身也是一次模型调用,会引入额外延迟和成本,所以更适合请求量大、任务差异明显的场景。第三种是失败回退:默认用轻量模型,检测到输出质量不达标时自动升级重试。判断质量可以通过结构化校验、规则检查或一个小的评估模型完成。
def execute_with_fallback(client, task, messages):
"""先尝试轻量模型,校验失败则升级到旗舰模型重试"""
try:
resp, record = call_model_with_monitor(
client, task.id, task.step, "lite-model", messages
)
if task.validator(resp):
return resp, record
except Exception:
pass # 记录日志后进入降级流程
# 升级到旗舰模型,并记录降级原因便于后续分析
resp, record = call_model_with_monitor(
client, task.id, task.step, "flagship-model", messages
)
return resp, record
除了路由之外,还有几个配合降级的省钱手段。一是提示词缓存:把不变的系统提示和工具定义放在消息序列前部,利用API的缓存计费规则,缓存命中部分的输入价格通常只有正常价格的十分之一左右。二是输出长度控制:为轻量模型设置较小的max_tokens,避免它在简单任务上啰嗦。三是历史压缩:当对话超过一定步数后,用一次模型调用把旧历史总结成摘要,替换原始消息,防止上下文无限增长。这三招加在一起,往往能把单任务成本压到原来的三分之一以下。
四、落地时的注意事项
降级策略不是拍脑袋定的,需要数据支撑。上线初期可以开启“影子模式”:所有请求先按你设计的路由规则打标,但实际仍用旗舰模型执行,同时记录轻量模型假如执行会是什么结果(可以通过小比例真实分流验证)。跑一到两周后对比两组的成功率和质量评分,就能得出哪些任务类型可以安全降级,哪些必须保留旗舰模型。这种灰度验证能避免降级过早导致的质量回退。
另外要警惕一个误区:降级不等于全用便宜模型。有些团队看到轻量模型便宜就一刀切,结果复杂任务的失败率上升,重试和人工介入的成本反而超过了省下的钱。正确的做法是让成本数据和质量数据出现在同一张报表里,按“单位成功任务的成本”来评估,而不是单纯看模型单价。建立监控、验证路由、小步灰度,这套组合拳打下来,Agent的成本才能真正做到既透明又可控。