Token消耗优化:多Agent协作的隐形成本

Token消耗优化:多Agent协作的隐形成本

SmartInspector 一次完整分析要跑 4-5 轮 LLM 调用,每轮的输入 token 数不同。如果不对 token 做控制,随着对话轮次增加,上下文会指数级膨胀。这篇文章讲我是怎么把 token 消耗压下来的。

项目地址:github.com/mufans/AppS...

先看数据:一次分析要花多少 token

SmartInspector 的分析流程是这样的:

复制代码
用户输入 → orchestrator(意图路由)→ collector(采集trace)
→ analyzer(LLM分析)→ attributor(源码归因)→ reporter(生成报告)

每个环节都要调 LLM,我用 TokenTracker 记录了每阶段的消耗:

阶段 输入 Token 输出 Token 说明
orchestrator ~800 ~5 意图分类,只输出一个路由标签
analyzer ~3,000 ~1,500 性能分析,输入包含trace摘要
attributor ~5,000 ~2,000 源码归因,输入包含代码片段
reporter ~4,000 ~3,000 生成报告,输入包含前面所有结果
合计 ~12,800 ~6,500

一次分析大约 20k token,用 DeepSeek 的话成本不到 1 分钱。但如果用户连续分析多次,token 会因为上下文累积而飙升。

第一刀:意图路由用 max_tokens=5

这是最简单也最有效的一刀。

意图路由的目的就是把用户输入分类到对应的 Agent:collector、analyzer、attributor、explorer 等。它只需要输出一个标签,比如 "analyze" 或 "attribute"。

python 复制代码
# orchestrator.py
_route_llm = ChatOpenAI(**get_llm_kwargs(temperature=0, max_tokens=5))

max_tokens=5,意思是 LLM 最多输出 5 个 token。对于一个只需要返回 "analyze" 或 "attribute" 的任务来说绰绰有余。

效果:意图路由的输出 token 从几百降到个位数。虽然输入 token 没变,但输出 token 是计费的大头(通常输出比输入贵 3-5 倍),这一刀砍掉了路由阶段 90%+ 的输出成本。

而且还有一个隐藏好处:max_tokens=5 限制了 LLM 的"发挥空间",它不可能返回一大段废话,响应速度也更快。

第二刀:消息窗口裁剪

多轮对话中,上下文会不断累积。用户第 1 次分析的 trace 数据、第 2 次分析的 trace 数据、第 3 次的......全部塞进 messages 列表里。

如果不控制,第 N 次分析时,LLM 要处理的上下文是:

复制代码
系统提示 + 第1次trace + 第1次分析 + 第2次trace + 第2次分析 + ... + 第N次输入

这就是 O(n²) 增长------每多一次分析,后续所有调用都要多处理一次历史数据。

SmartInspector 的做法:

python 复制代码
# orchestrator.py - fallback_node
# Extract recent conversation for context (filter out ToolMessage to save tokens)
recent = []
for m in messages:
    msg_type = getattr(m, "type", "")
    if msg_type in ("human", "ai"):
        recent.append(m)
# Keep only the last 6 valid conversation messages
recent = recent[-6:]

只保留最近 6 条有效消息(Human + AI),过滤掉 ToolMessage。这样不管用户分析多少次,上下文大小都是固定的。

关键点

  • 过滤 ToolMessage 很重要,因为 Agent 的工具调用结果(grep 输出、文件内容等)是 token 消耗的大户
  • 只保留最近 6 条,够理解上下文,又不会累积
  • State 里通过 _pass_through 传递关键数据(trace 摘要、归因结果),不依赖完整历史

第三刀:Reporter 的 token 预算控制

Reporter 是最后一个环节,它的输入包含前面所有阶段的结果。如果前面几个阶段产生了大量内容,Reporter 的输入会非常大。

python 复制代码
# reporter/__init__.py
MAX_REPORT_INPUT_TOKENS = get_report_max_tokens()
estimated_tokens = len(user_content) / 1.5  # CJK: 1 token ≈ 1.5 chars
if estimated_tokens > MAX_REPORT_INPUT_TOKENS:
    # truncate...

Reporter 会先估算输入内容的 token 数,超过预算就裁剪。优先级是:归因结果 > 分析结果 > trace 数据。

这样做的好处是:不管前面产生了多少中间数据,Reporter 的输入 token 不会超过预算,输出质量也稳定。

第四刀:Collector 的本地预处理

这一刀不是省 LLM token,而是省网络传输和 LLM 处理时间。

Perfetto trace 文件可能有几十 MB,原始 SQL 查询结果也可能很大。SmartInspector 的 Collector 不会把原始数据丢给 LLM,而是先做本地预处理:

python 复制代码
# collector.py 的工作流程:
# 1. 采集 trace → 保存为 .pb 文件
# 2. 用 Perfetto SQL 查询关键指标(CPU、帧率、耗时方法等)
# 3. 将查询结果压缩成 ~2KB 的 JSON 摘要
# 4. 把摘要(不是原始数据)传给后续 Agent

效果:原始 trace 几十 MB → 压缩后 ~2KB JSON。后续 Agent 处理的是结构化的摘要,不是原始 trace。

这是最关键的一刀。因为如果让 LLM 直接处理原始 trace 数据,光输入 token 就可能超过模型上下文窗口。

TokenTracker 的实现

TokenTracker 是一个简单的线程安全 token 计数器:

python 复制代码
class TokenTracker:
    def __init__(self):
        self._lock = threading.RLock()
        self._stages: dict[str, dict] = {}

    def record(self, stage: str, usage: dict | None) -> None:
        if not usage:
            return
        input_tokens = usage.get("input_tokens") or usage.get("prompt_tokens") or 0
        output_tokens = usage.get("output_tokens") or usage.get("completion_tokens") or 0
        # ... 累加到对应 stage

    def summary(self) -> str:
        # 输出每个阶段的 token 消耗表格

每调用一次 LLM,就在对应的 stage 里记录 token 数。最后 summary() 输出一个表格:

markdown 复制代码
Token usage:
Stage                Input   Output    Total  Calls
------------------------------------------------------
orchestrator          0.8k     0.0k     0.8k      1
analyzer              3.2k     1.5k     4.7k      1
attributor            5.1k     2.0k     7.1k      1
reporter              4.0k     3.2k     7.2k      1
------------------------------------------------------
TOTAL                13.1k     6.7k    19.8k      4

这个表格是分析完成后的最后一行输出。它本身不省 token,但让你知道 token 花在哪里,方便持续优化。

实际效果

做了这四刀之后,一次完整分析的 token 消耗:

优化前(估算) 优化后
orchestrator 输出 ~200 token ~5 token
多轮对话上下文 O(n²) 增长 固定最近 6 条
Reporter 输入无上限 预算控制
Collector 传原始 trace ~50MB 压缩后 ~2KB

一次分析从可能消耗 50k+ token (多轮后)稳定在 20k token 左右。用 DeepSeek 的话,成本从每次几分钱降到不到 1 分钱。

几条务实建议

  1. 先量化再优化。没有 TokenTracker 之前,我不知道 token 花在哪里。加了之后才发现 orchestrator 的输出占了不小比例。

  2. max_tokens 是最便宜的优化。一行代码的事,但效果立竿见影。任何只需要短输出的场景都应该设。

  3. 裁剪上下文比优化 prompt 有效。prompt 写得再好,上下文里塞了一堆无关的 ToolMessage 结果也是浪费。过滤比精炼更直接。

  4. 预处理 > LLM 处理。能本地算的就本地算。Collector 把 50MB trace 压缩成 2KB JSON,这比任何 prompt 工程都管用。

  5. 不要过早优化。20k token 一次分析,用 DeepSeek 成本不到 1 分钱。Token 优化的 ROI 取决于你的调用频率和模型价格。


下一篇预告:AI 写的技术文章,为什么总有"AI 味"------14 条去味规则,让 AI 写出人味。

相关推荐
欧阳天风5 小时前
vue3的组件优化
前端·vue.js·性能优化
zmsculpture5 小时前
现代玻璃钢雕塑制作工艺
性能优化·雕塑制作过程·玻璃钢雕塑
小雨青年5 小时前
GitHub Actions 工作流性能优化实战
性能优化·github
倔强的石头1065 小时前
表空间自动目录创建与存储管理实践:参数化配置与性能优化
数据库·oracle·性能优化
leonkay1 天前
到底应不应该写注释?
性能优化·架构·个人开发·注释·代码规范·设计·规格说明书
qq_283720051 天前
Python GIL 底层实现与高并发突破实战
python·性能优化·高并发·全局锁
whuhewei1 天前
React性能优化
前端·react.js·性能优化
quxuexi1 天前
MySQL B+树与复合索引完全指南:从底层原理到高性能优化
b树·mysql·性能优化
leonkay1 天前
关于.NET中的队列理解
数据库·性能优化·.net·个人开发·设计规范·队列