TokenTracker:给AI调用装个计费器

TokenTracker:给AI调用装个计费器

做AI应用开发有个很现实的问题------你不知道每次分析到底烧了多少钱

SmartInspector跑一次完整分析,涉及意图路由、性能分析、源码归因、报告生成等多个LLM调用环节。如果不做Token追踪,成本完全是个黑箱。今天聊聊我们是怎么实现TokenTracker的,以及它在实际项目中怎么帮你控制成本。

为什么需要Token追踪

先看一个真实场景。SmartInspector的完整分析pipeline包含这些LLM调用:

  1. orchestrator(意图路由)------ 判断用户想做什么,max_tokens=5
  2. perf_analyzer(性能分析)------ 分析CPU热点、帧数据
  3. attributor(源码归因)------ 把耗时方法关联到项目源码
  4. reporter(报告生成)------ 输出结构化分析报告
  5. frame_analyzer(帧分析)------ 分析具体帧的渲染问题

跑一次完整分析,少说5-8次LLM调用,多的十几次。如果不追踪,你根本不知道:

  • 哪个阶段最费Token?
  • 一次分析大概多少钱?
  • 有没有某个阶段在浪费Token?

这些问题不能靠猜,得靠数据。

设计思路:够用就行

TokenTracker的设计原则很简单:线程安全、按阶段统计、全局单例

为什么是全局单例而不是依赖注入?因为SmartInspector的调用链比较深------orchestrator调agent,agent里调LLM,LLM返回结果。如果用依赖注入,tracker要一层层传下去,侵入性太大。全局单例虽然不够"优雅",但在这种场景下最实用。

python 复制代码
# 全局单例
_tracker = TokenTracker()

def get_tracker() -> TokenTracker:
    return _tracker

需要重置的时候(比如开始新一轮分析),调reset()就行。

核心实现

TokenTracker的完整代码不到150行,核心逻辑更少。看一下关键部分:

数据结构

每个阶段(stage)维护三个指标:input_tokens、output_tokens、调用次数。

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

dict[str, dict]而不是自定义类,是因为这个结构足够简单,没必要过度设计。threading.RLock保证线程安全------SmartInspector有些场景下会并发调用LLM。

记录一次调用

python 复制代码
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

    with self._lock:
        if stage not in self._stages:
            self._stages[stage] = {"input_tokens": 0, "output_tokens": 0, "calls": 0}
        self._stages[stage]["input_tokens"] += input_tokens
        self._stages[stage]["output_tokens"] += output_tokens
        self._stages[stage]["calls"] += 1

这里有个细节:key的兼容处理 。不同LLM provider返回的Token用量字段名不一样------OpenAI兼容接口用prompt_tokens/completion_tokens,Anthropic用input_tokens/output_tokens。两个都取,哪个有值用哪个。

从LangChain消息中提取

LangChain的AIMessage对象把Token信息藏在两个地方:usage_metadataresponse_metadata.token_usage。所以封装了一个便捷方法:

python 复制代码
def record_from_message(self, stage: str, message: object) -> None:
    um = getattr(message, "usage_metadata", None)
    if um:
        self.record(stage, um)
        return

    rm = getattr(message, "response_metadata", {})
    tu = rm.get("token_usage") if rm else None
    if tu:
        self.record(stage, tu)

优先取usage_metadata,这是LangChain标准的Token用量字段。如果没有(某些provider不支持),降级到response_metadata

实际接入:每个Agent都在用

接入非常简单,每个Agent在拿到LLM响应后加一行:

python 复制代码
# orchestrator.py
from smartinspector.token_tracker import get_tracker

response = self.llm.invoke(messages)
get_tracker().record_from_message("orchestrator", response)
python 复制代码
# attributor.py
get_tracker().record_from_message("attributor", response)
python 复制代码
# perf_analyzer.py - 甚至区分了重试
get_tracker().record_from_message("perf_analyzer", response)
# 重试时用不同的stage名
get_tracker().record_from_message("perf_analyzer_retry", retry_response)

用不同的stage名,这样在统计里能看到重试消耗了多少Token。这个设计在排查成本异常时特别有用------如果perf_analyzer_retry的Token占比异常高,说明这个Agent的prompt需要优化,导致频繁重试。

输出:一目了然的成本报表

TokenTracker的summary()方法输出一个表格:

markdown 复制代码
Token usage:
Stage                   Input   Output    Total  Calls
------------------------------------------------------
orchestrator              120        5      125      1
perf_analyzer           2.3k      856    3.2k      2
attributor              1.8k      412    2.2k      1
reporter                3.1k    1.2k    4.3k      1
frame_analyzer          1.5k      380    1.9k      1
------------------------------------------------------
TOTAL                   8.8k    2.9k   11.7k      6

一眼就能看出:

  • reporter最费Token------输入3.1k,因为要把所有分析结果喂进去
  • orchestrator几乎不花钱------max_tokens=5的效果,单次才125个Token
  • 总消耗约12k Token------按GPT-4的定价,大约几毛钱一次

这个表格在两个地方展示:pipeline结束时自动打印,以及用户执行/token命令时手动查看。

用TokenTracker做成本优化

有了数据,优化就有方向了。举几个实际例子:

1. 意图路由的极致压缩

orchestrator阶段单次只消耗125个Token,input 120 + output 5。这不是巧合,而是刻意设计的------意图路由只需要返回一个标签,所以把max_tokens设成5,prompt也尽量精简。

如果没有TokenTracker,你可能不会意识到这个优化空间。看到数据后就会想:还有什么地方可以像意图路由一样压缩?

2. 发现重试成本

perf_analyzer_retry这个stage单独记录重试的Token消耗。有一次发现重试消耗占总Token的30%,原因是prompt里给的上下文不够精确,导致LLM输出格式不对,触发了JSON解析失败→重试。优化prompt后,重试率降到了5%以下。

3. 报告生成的Token占比

reporter阶段的input_tokens是最高的,因为它要接收前面所有Agent的分析结果。这启发我们做了消息窗口裁剪------不是把所有原始数据都喂给reporter,而是先做一轮摘要。

换算成钱

TokenTracker目前只统计Token数量,不做金额换算。原因是不同模型价格差异太大,硬编码不合适。但在to_dict()输出的数据基础上,做换算很简单:

python 复制代码
tracker = get_tracker()
data = tracker.to_dict()
# 假设用GPT-4o: input $2.5/1M tokens, output $10/1M tokens
cost = data["total_input"] * 2.5 / 1_000_000 + data["total_output"] * 10 / 1_000_000
print(f"本次分析成本: ${cost:.4f}")

这是我们下一步打算加的功能------根据当前使用的模型自动估算成本。

一些设计取舍

为什么不做实时监控? 没必要。一次分析通常几十秒就跑完了,结束时打印汇总就够了。如果要做成本预警(比如单次分析超过$1就报警),可以在record里加个阈值检查,但目前还没这个需求。

为什么用线程锁而不是asyncio锁? 因为LangChain的invoke是同步调用,整个pipeline跑在线程池里。如果以后改成全async,换成asyncio.Lock就行,接口不变。

为什么不做持久化? 每次CLI会话创建新的tracker,会话结束数据就丢了。如果要做历史趋势分析,可以把to_dict()的结果写到数据库或文件里。目前没有这个需求,保持简单。

小结

TokenTracker不是什么复杂的东西,但它解决了一个真实问题:让AI调用的成本可见

150行代码,全局单例,线程安全,按阶段统计。接入成本几乎为零------每个Agent加一行代码就行。但带来的收益是实打实的:你能看到每个环节花了多少Token,哪个环节需要优化,一次分析到底多少钱。

做AI应用,Token就是你的"电费"。不装电表,你怎么知道该关哪盏灯?


本文是「SmartInspector架构解读」系列第6篇,源码在 GitHub,核心文件:src/smartinspector/token_tracker.py

相关推荐
wangl_923 小时前
C#性能优化完全指南 - 从原理到实践
开发语言·性能优化·c#·.net·.netcore·visual studio
晚风_END6 小时前
Linux|操作系统|zfs文件系统的使用详解
linux·运维·服务器·数据库·postgresql·性能优化·宽度优先
czlczl2002092513 小时前
松散索引扫描/跳跃索引扫描
数据库·mysql·性能优化
czlczl200209251 天前
MAX()和MIN()优化
数据库·mysql·性能优化
山峰哥1 天前
SQL优化从入门到精通:20个案例破解性能密码
数据库·sql·oracle·性能优化·深度优先
小杍随笔2 天前
Rust桌面GUI框架:性能优化与实战避坑指南
开发语言·性能优化·rust
MU在掘金916952 天前
报告生成:怎么让AI输出结构化内容
性能优化
光影少年2 天前
Webpack打包性能优化方面的经验
前端·webpack·性能优化
一起搞IT吧3 天前
Android性能系列专题理论之十:systrace/perfetto相关指标知识点细节含义总结
android·嵌入式硬件·智能手机·性能优化