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

9 阅读1分钟

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"。

# 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 的做法:

# 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 的输入会非常大。

# 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,而是先做本地预处理:

# 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 计数器:

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() 输出一个表格:

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 写出人味。