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 一个可配置的预算上限,花超了要么降级、要么报错、要么用自己的方式想办法。

思路很简单:

  1. Agent 每次执行时分配一个 Token 预算(比如每轮 4000 tokens)
  2. Agent 每次调 LLM 之前估算这次调用会花多少
  3. 累加到 Agent 的已消耗账单上
  4. 如果即将超预算,不让它继续调,而是走降级逻辑

这样每个 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 系统有算过账吗?

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个