Agent 系统的 Token 预算管理:我让三个 Agent 各自记账
去年我写了一个多 Agent 系统,跑了三天,花了 200 刀 API 费用。不是因为流量大,是因为每个 Agent 都不知道其他 Agent 花了多少钱。后来我在系统里加了一层 Token 预算管理——不是简单地限制上下文长度,而是让每个 Agent 有一个"账户",花超了自己想办法。这篇文章聊聊这个思路和落地时踩的坑。
今年年初我搭了一个多 Agent 系统做知识库问答。结构很简单:一个路由 Agent 接用户问题,分发给检索 Agent 和推理 Agent,各自干活后再汇总。
跑了一个 demo,感觉不错。然后我把系统挂上去跑了三天,准备拿些真实数据做分析。
第三天收到 AWS 账单的时候我愣住了。
不是流量暴增,不是用户量很大——三天总共就几十次查询。但是总 Token 消耗将近 400 万,换算下来大概 200 刀。我一个个查日志才发现问题:
路由 Agent 每次把完整的历史对话塞给子 Agent。检索 Agent 调用三次工具,每次返回几万字的原始文档,全部原样塞回上下文。子 Agent 完成任务后,它的整段对话又被父 Agent 原封不动吞进去。
每个 Agent 都觉得自己花的 Token 不多。但从全局看,同一段对话被复制粘贴了四五遍,每一遍都在交钱。
我当时的反应是:这不是架构问题,这是财务问题。我需要给每个 Agent 一个"账户"。
每个 Agent 都应该知道自己花了多少
先明确一下目标。我不是要做"省 Token"优化——省 Token 往往牺牲质量。我要做的是预算管理:给每个 Agent 一个可配置的预算上限,花超了要么降级、要么报错、要么用自己的方式想办法。
思路很简单:
- Agent 每次执行时分配一个 Token 预算(比如每轮 4000 tokens)
- Agent 每次调 LLM 之前估算这次调用会花多少
- 累加到 Agent 的已消耗账单上
- 如果即将超预算,不让它继续调,而是走降级逻辑
这样每个 Agent 对自己花的钱有感知。更重要的是,当一个 Agent 占用太多预算时,系统能及时发现,而不是等到月底出账单才傻眼。
第一版:先能算账
我最先做的事情不是做限制,是先记账——没有数据,谈什么预算。
class TokenBudget:
def __init__(self, limit: int, scope: str):
self.limit = limit # 总预算(tokens)
self.consumed = 0 # 已消耗
self.scope = scope # 标识是哪个 Agent
self._history = [] # 逐笔记录
def estimate(self, messages: list) -> int:
"""预估一段消息列表会花多少 token"""
total = 0
for msg in messages:
total += len(msg.get("content", "")) // 2 # 中文约 0.5 token/字
total += 4 # 每条消息的 meta token
return total
def deduct(self, messages: list, response: str):
"""调用后记录实际消耗"""
cost = self.estimate(messages) + len(response) // 2
self.consumed += cost
self._history.append({"cost": cost, "remaining": self.remaining})
if self.remaining < 0:
logger.warning(f"[{self.scope}] 预算已超支: {self.consumed}/{self.limit}")
@property
def remaining(self) -> int:
return self.limit - self.consumed
这个 estimate 方法很糙。我用的是最简单的中文字符除 2 来估算 token 数,加上每条消息的开销。准确率大概在 80% 左右——够了,预算管理不需要精确到个位数,误差在可接受范围内就行。
把这段代码挂到每个 Agent 的调用链路上之后,我拿到了第一张"账单":运行一个完整的多轮对话,哪个 Agent 花了多少,一清二楚。不出所料,检索 Agent 是耗 token 大户——每次查知识库返回的文档 chunk 太多。
第二版:花超了就降级
记账只是第一步。重点是:预算花完了怎么办?
最简单的方案是抛异常。但我觉得这不是 Agent 该有的行为——Agent 应该有能力在资源受限的情况下完成任务,而不是直接摆烂。就像你出差预算不够了,你不会直接取消行程,你会选择住便宜点的酒店。
于是我加了一个超预算后的降级策略:
class BudgetAwareAgent:
def __init__(self, budget: TokenBudget):
self.budget = budget
self.fallback_model = "gpt-4o-mini" # 便宜模型
async def think(self, task: str, context: list) -> str:
if self.budget.remaining < 2000:
# 预算紧张,走省 token 模式
return await self._think_cheap(task, context)
# 预算充足,调主力模型
return await self._think_expensive(task, context)
async def _think_cheap(self, task: str, context: list) -> str:
# 1. 用更便宜的模型
# 2. 截断上下文,只保留最近 3 轮对话
truncated = context[-6:] if len(context) > 6 else context
response = await call_llm(
model=self.fallback_model,
messages=truncated + [{"role": "user", "content": task}],
max_tokens=512
)
self.budget.deduct(truncated, response)
return response
async def _think_expensive(self, task: str, context: list) -> str:
response = await call_llm(
model="gpt-4o",
messages=context + [{"role": "user", "content": task}],
max_tokens=2048
)
self.budget.deduct(context, response)
return response
这里有一个关键设计:预算紧张时自动切模型,同时截断上下文。不是简单地限制 max_tokens,而是从多个维度压缩。
具体来说,我的降级策略分了三级:
- 剩余 < 2000 tokens: 切 gpt-4o-mini,截断历史到最近 3 轮,max_tokens 降到 512
- 剩余 < 500 tokens: 只允许最后一次推理,尝试用缓存结果回复
- 剩余 < 100 tokens: 直接返回"预算不足,当前无法处理"(至少没亏钱)
这个分级设计的核心思想是:Agent 花超之后不能"坏掉",要优雅地退化。
一个意外的收获:上下文剪枝反而变好了
让我没想到的是,加上预算限制之后,Agent 的回复质量反而提升了。
原因是:预算紧张时自动截断了上下文,把那些无关的历史对话丢掉了。Agent 被迫只关注最近几轮交互,反而减少了上下文噪声。
之前我花了很多时间研究怎么保留完整的对话历史"以防万一"。现在预算限制了,我不得不剪掉那些"万一"——结果发现大多数时候,那些"万一"根本不会发生。
这个经验让我反思了一个问题:上下文窗口是你的资产,不是你的垃圾场。 每个 token 都是花钱买的,你塞没用的话进去就是在烧钱。限制预算本质上是在强迫你做好上下文管理。
生产环境落地时要注意什么
实际上线之后还有几个坑,简单记一下:
估算不准会出问题。 我用的是简单的字符估算,但不同模型的分词器不一样。gpt-4o 和 gpt-4o-mini 用同一个 tiktoken 编码还好,但换成 Claude 或本地模型就需要单独适配。最好在 estimate 里注入模型对应的 tokenizer。
预算粒度怎么分。 我试过两种:按 Agent 实例分(每个 Agent 实例独立预算)和按会话分(一场对话的所有 Agent 共享预算)。前者容易导致一个 Agent 花完了其他 Agent 还很充裕,后者管理更公平但实现复杂。我的建议是:早期按 Agent 分,简单可控;后期按会话分,精确核算。
流式场景下怎么记账。 如果 Agent 用流式输出,你没法在调用前知道最终输出了多少 token。我改成了一个两阶段方案:调用前用 max_tokens 先预留预算,流式结束时用实际数量结算,多退少补。
预算状态是否需要持久化。 跨轮对话的 Token 预算应该跟对话状态一起持久化。否则 Agent 重启动就忘了自己花了多少,预算形同虚设。
最后说两句
Token 预算管理不是什么高深的技术。它本质上就是一个计数器加上一些条件判断。
但它改变了我设计 Agent 的方式。之前我总想着如何最大化地利用模型的上下文窗口,现在我会先问一句:这轮调用值不值得花这些 token?如果答案是我也不知道,那说明这个设计本身有问题。
好,今天就聊到这。你的 Agent 系统有算过账吗?
评论
发表评论