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 小时前
Linux软件开发性能优化
linux·c++·性能优化
优测云服务平台6 小时前
压力测试怎么做?从场景设计到瓶颈定位的完整实践指南
测试工具·性能优化
爱喝水的鱼丶14 小时前
SAP-ABAP:变量、常量、结构与内表声明(10篇博客合集) 第九篇:声明阶段的性能优化:如何从定义环节减少程序内存占用与运行耗时
开发语言·学习·算法·性能优化·sap·abap
auspicious航16 小时前
PostgreSQL性能优化实战:从查询慢如蜗牛到飞一般的体验
数据库·postgresql·性能优化
Gauss松鼠会19 小时前
GaussDB(DWS)数据融合:Oracle增量数据迁移到DWS
java·数据库·算法·oracle·性能优化·gaussdb
SilentSamsara1 天前
Python 性能优化:tracemalloc、profiling 与 C 扩展加速
开发语言·python·青少年编程·性能优化
筠筠喵呜喵1 天前
Linux CPU性能优化:D状态和Z状态排查与处理
linux·服务器·性能优化
huangdong_1 天前
图片下载工具性能优化:并发控制与内存管理
性能优化
松☆2 天前
昇腾NPU上的Vector算子模板库,性能优化案例实录
性能优化
松☆2 天前
Triton推理服务接昇腾NPU,GE后端怎么搭?
华为·性能优化·numpy·信号处理·harmonyos