从零开始理解 Agent(番外篇):Token 都花在哪了?

举报
AGENT魔方 发表于 2026/04/15 09:12:48 2026/04/15
【摘要】 「从零开始理解 Agent」系列番外 —— 在前面的七篇正文里(文末超链接直达),我们从来没关心过一个问题:跑一次 Agent 到底消耗多少 Token?每轮循环花了多少?工具返回结果占了多大比例?这篇番外给 Agent 装上一个 Token 仪表盘,让消耗一目了然。

专栏1.png

「从零开始理解 Agent」系列番外 —— 在前面的七篇正文里(文末超链接直达),我们从来没关心过一个问题:跑一次 Agent 到底消耗多少 Token?每轮循环花了多少?工具返回结果占了多大比例?这篇番外给 Agent 装上一个 Token 仪表盘,让消耗一目了然。

作者:十一

一、为什么要关心 Token?

用 Agent 和用普通对话最大的成本差异在于:对话是一问一答,Agent 是一个循环。

一次普通对话:1 次 API 调用,消耗一份 Token。

一次 Agent 任务:可能调用 5-15 次 API,每次调用都带着完整的 messages 历史,而且 messages 每轮都在增长——每调用一次工具,messages 至少新增两条(LLM 的回复 + 工具返回结果)。输入 Token 是累积增长的,不是线性增长的。

具体消耗多少,取决于工具返回结果的长度——ls 返回几行和 cat 一个千行文件,差距可以是几十倍。所以 Token 消耗不能靠估算,要靠实际测量。

二、API 返回的 usage 字段

好消息是,OpenAI 兼容的 API 每次调用都会返回 Token 使用情况:

response = client.chat.completions.create(
    model=MODEL, messages=messages, tools=TOOLS
)

# response.usage 包含这三个字段:
# - prompt_tokens:     输入 Token 数(messages + tools schema)
# - completion_tokens: 输出 Token 数(LLM 的回复)
# - total_tokens:      两者之和

我们只需要在每轮循环中把这个数据收集起来。

三、给 Agent 加一个 Token 追踪器

在第一篇的 agent.py 基础上,只需要加一个简单的数据结构:

class TokenTracker:
    """追踪 Agent 整个生命周期的 Token 消耗"""

    def __init__(self):
        self.rounds = []        # 每轮的详细数据
        self.total_input = 0
        self.total_output = 0

    def record(self, round_num, usage, message_count):
        """记录一轮循环的 Token 消耗"""
        input_tokens = usage.prompt_tokens
        output_tokens = usage.completion_tokens

        self.rounds.append({
            "round": round_num,
            "input": input_tokens,
            "output": output_tokens,
            "total": input_tokens + output_tokens,
            "messages": message_count
        })
        self.total_input += input_tokens
        self.total_output += output_tokens

    def summary(self):
        """打印消耗摘要"""
        print(f"\n{'='*50}")
        print(f"Token 消耗统计")
        print(f"{'='*50}")
        print(f"{'轮次':<6} {'输入':>8} {'输出':>8} {'合计':>8} {'消息数':>6}")
        print(f"{'-'*50}")
        for r in self.rounds:
            print(f"{r['round']:<6} {r['input']:>8} {r['output']:>8} "
                  f"{r['total']:>8} {r['messages']:>6}")
        print(f"{'-'*50}")
        print(f"{'合计':<6} {self.total_input:>8} {self.total_output:>8} "
              f"{self.total_input + self.total_output:>8}")
        print(f"{'='*50}")

嵌入 Agent 循环:

def run_agent(user_message, max_iterations=10):
    tracker = TokenTracker()
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_message}
    ]

    for i in range(max_iterations):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=TOOLS
        )
        message = response.choices[0].message

        # 记录本轮消耗
        tracker.record(i + 1, response.usage, len(messages))

        ifnot message.tool_calls:
            tracker.summary()  # 任务结束时打印统计
            return message.content

        # 执行工具调用...
        messages.append(message)
        for tool_call in message.tool_calls:
            result = execute_tool(tool_call)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

    tracker.summary()
    return"Max iterations reached"

四、实际输出长什么样

需要说明的是,不同任务的 Token 消耗差异很大——一个"创建 hello.py"可能 2 轮就结束,一个"重构整个项目"可能跑 20 轮。下面的数据只是一个具体案例,目的是让大家对 Agent 的 Token 消耗有个感性认识,而不是一个通用基准。

让 Agent 执行"找到当前目录的 Python 文件,统计行数,写入报告",Token 追踪器的输出类似这样(以下为示意数据,实际数值因模型和任务而异):

==================================================
Token 消耗统计
==================================================
轮次     输入     输出     合计   消息数
--------------------------------------------------
1         523      87      610      2
2        1204     103     1307      5
3        2891      76     2967      8
4        3542      45     3587     11
5        3870     156     4026     13
--------------------------------------------------
合计    12030     467    12497
==================================================

几个一眼就能看出的规律:

输入 Token 逐轮递增。 第 1 轮 523,第 5 轮 3870——因为每轮都要把完整的 messages 历史发给 LLM,历史越长输入越大。

输出 Token 相对稳定。 每轮只有几十到一两百——LLM 的回复通常就是一段思考 + 一次工具调用的 JSON。

输入远大于输出。 在这个示例中,输入占了总消耗的绝大部分。这在 Agent 场景中是普遍规律——意味着降低成本的关键是控制输入,不是控制输出。

五、Token 都花在哪了?

输入 Token 可以拆成三部分:

输入 Token = system prompt(含 Skills)+ tools schema + 历史 messages

其中:

system prompt 每轮都要带,固定成本。如果只有基础指令,通常几百 Token。但回忆第三篇——Rules 和 Skills 的内容都是注入到 system prompt 中的。一旦挂载了几个 Skill(每个 Skill 的描述可能几百到上千 Token),system prompt 就会从几百膨胀到几千甚至上万。这是一个容易被忽略的固定开销:每一轮 API 调用都要重复发送全部 Skill 描述。

tools schema 也是每轮都要带。nanoAgent 的三个工具(read_file、write_file、execute_bash),JSON Schema 大约几百 Token(具体取决于参数描述的详细程度)。但这是最简情况。生产级 Agent 动辄注册十几个甚至几十个工具,每个工具的参数描述、枚举值、嵌套结构都会占 Token。工具数量增长十倍,tools schema 的开销也会相应增长——而且这个成本每轮都要付。这也是为什么第三篇的 MCP 动态加载工具、而不是把所有工具都塞进去的原因之一:按需加载,用不到的工具不注册,省的是每一轮的固定税。

历史 messages 这是大头,也是唯一会增长的部分。增长速度取决于工具返回结果的长度——ls 返回几行,cat 一个大文件可能返回几千行。

总结一下:system prompt(含 Skills)和 tools schema 是"固定税",每轮都交;历史 messages 是"累进税",越跑越多。降本要两手抓——减少固定税(精简 Skill 数量与描述、精简工具)和控制累进税(截断输出、及时压缩)。

六、和第六篇压缩的关系

现在回头看第六篇的上下文压缩,它做的事情就清楚了:砍掉历史 messages 中的旧内容,降低每轮的输入 Token。

没有压缩时,Token 消耗曲线是这样的:

输入 Token
    ^
    |          /
    |        /
    |      /       ← 越来越贵
    |    /
    |  /
    |/
    +------------→ 轮次

有压缩时:

输入 Token
    ^
    |    /\  /\
    |   /  \/  \   ← 锯齿形,有上限
    |  /
    | /
    |/
    +------------→ 轮次

压缩把一条单调递增的曲线变成了有上限的锯齿波。Token 追踪器加上压缩,你就能精确看到每次压缩省了多少 Token。

在 TokenTracker 中加一行标记压缩事件:

def record_compaction(self, round_num, before_tokens, after_tokens):
    """记录一次压缩事件"""
    saved = before_tokens - after_tokens
    print(f"  [压缩] 轮次 {round_num}: {before_tokens} → {after_tokens} "
          f"(节省 {saved} tokens, {saved/before_tokens*100:.0f}%)")

七、几条实用的成本控制经验

有了 Token 追踪器之后,一些优化方向会变得很直观:

截断工具输出。 第七篇安全篇里已经做了输出截断(MAX_OUTPUT_LENGTH),它不只是为了安全,也是成本控制的第一道防线。cat 一个 10000 行的文件会让后续每一轮都多带 10000 行的历史——截断到前 200 行,后续每轮都能省下大量输入 Token。

减少不必要的工具调用。 有时 LLM 会先 ls 看一下目录,再 cat 某个文件,再 grep 搜索内容——而实际上一条 grep -r "keyword" . 就能搞定。更好的 system prompt 可以引导 LLM 用更少的步骤完成任务。

清理不用的 MCP 和 Skill。 第五节讲了,tools schema 和 Skill 描述是每轮都要付的"固定税"。注册了 10 个 MCP 工具但日常只用 3 个,剩下 7 个的 schema 每轮都在白白消耗 Token。Skill 同理——挂载了五个 Skill 但当前任务只涉及其中一个,其余四个的描述都是浪费。定期审视已注册的 MCP 和 Skill,删掉不用的,是最简单的降本手段。

选对模型。 简单任务(文件操作、格式转换)用便宜的小模型,复杂任务(代码重构、架构分析)用贵的大模型。这就是为什么有些 Agent 框架支持"模型路由"——根据任务复杂度自动选模型。

及时压缩。 第六篇的压缩阈值不要设太高。阈值越高,压缩前的几轮输入 Token 越大。根据 Token 追踪器的数据调整阈值,找到"压缩频率"和"摘要质量"之间的平衡点。

八、小结

Token 追踪器的代码量很少,但它把一个黑盒变成了白盒——Agent 每轮花了多少、花在哪了、哪里可以省,全都看得见。

回到 Harness 番外篇的视角:Token 追踪是 Harness 的"仪表盘"。没有它,你只知道"任务完成了",但不知道 Agent 用了几轮、每轮输入多少 Token、哪一轮因为工具返回了大量内容导致消耗飙升。有了它,这些问题都有了数据支撑,优化才有方向。

容器.jpg

关注 AGENT 魔方公众号,回复 Agent

免费领取「从零开始理解 Agent」全套资料包

加速入门和掌握 Agent:

资料二维码.jpg


【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。