Token消耗优化:多Agent协作的隐形成本
SmartInspector 一次完整分析要跑 4-5 轮 LLM 调用,每轮的输入 token 数不同。如果不对 token 做控制,随着对话轮次增加,上下文会指数级膨胀。这篇文章讲我是怎么把 token 消耗压下来的。
先看数据:一次分析要花多少 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 分钱。
几条务实建议
-
先量化再优化。没有 TokenTracker 之前,我不知道 token 花在哪里。加了之后才发现 orchestrator 的输出占了不小比例。
-
max_tokens 是最便宜的优化。一行代码的事,但效果立竿见影。任何只需要短输出的场景都应该设。
-
裁剪上下文比优化 prompt 有效。prompt 写得再好,上下文里塞了一堆无关的 ToolMessage 结果也是浪费。过滤比精炼更直接。
-
预处理 > LLM 处理。能本地算的就本地算。Collector 把 50MB trace 压缩成 2KB JSON,这比任何 prompt 工程都管用。
-
不要过早优化。20k token 一次分析,用 DeepSeek 成本不到 1 分钱。Token 优化的 ROI 取决于你的调用频率和模型价格。
下一篇预告:AI 写的技术文章,为什么总有"AI 味"------14 条去味规则,让 AI 写出人味。