你的Agent每次调用花了多少钱?我手写了一个Token追踪器,发现了3个意外
你的Agent每次调用花了多少钱?我手写了一个Token追踪器,发现了3个意外
上周我的Agent跑了一整晚的任务链–5个模型轮番上阵,42次API调用,第二天醒来一看账单,懵了。花了多少钱?哪个模型最慢?输出质量有没有退化?全不知道。
这不是我一个人的问题。所有在生产环境跑Agent的人,迟早会碰到同一个坑:你的Agent在烧钱,你却连账本都没开。
踩完这些坑,我总结出一条铁律:生产级Agent必须有三层监控–Token追踪(花钱账本)+ 质量预警(健康体检)+ 调用链可视化(病历档案)。缺任何一层,你都是在盲人摸象。
这篇文章从Week6的4个AtomicInteger计数器开始,一路走到Week7的生产级可观测性四件套。全Java手写,零依赖,控制台直接看。每一步踩的坑、发现的意外,我都原样写出来。
第一部分:Token追踪器–你的Agent烧了多少钱?
Week6的时候,我对Token的监控就是4个AtomicInteger–输入Token数、输出Token数、总调用数、总Token数。简单粗暴,能跑就行。
但当你真正跑起多模型Agent,这玩意儿就不够用了。GLM-4和GLM-5.1的定价不一样,你混着用,一个AtomicInteger怎么可能告诉你哪个模型烧钱多?更别说成本了–我光知道用了10000个Token,但到底是花了0.5元还是5元?不知道。
所以Week7我把计数器升级成了ProductionTokenTracker,改了三个地方:
1. 微元整数存成本,不碰double
这是个容易被忽视的细节。double浮点数累加10000次,偏移能到0.01。看起来很小?但Agent一天跑几千次调用,这个偏移会累积,月底对账的时候你会发现账本和实际差了好几块。
我的做法是把1元拆成1000000个微元(micro-cent),用long整数存。加法永远不会丢精度,显示的时候除一下就行:
// 成本存储用微元(1元 = 1,000,000微元),避免浮点累加偏移
private final AtomicLong totalCostMicroCents = new AtomicLong(0);
public void addCost(double yuan) {
long microCents = (long)(yuan * 1_000_000);
totalCostMicroCents.addAndGet(microCents);
}
public double getTotalCostYuan() {
return totalCostMicroCents.get() / 1_000_000.0;
}
2. 按模型分类统计
多模型Agent的标配。每个模型有自己的统计桶:
private final ConcurrentHashMap<String, ModelStats> modelStats = new ConcurrentHashMap<>();
public static class ModelStats {
private final AtomicLong inputTokens = new AtomicLong(0);
private final AtomicLong outputTokens = new AtomicLong(0);
private final AtomicLong callCount = new AtomicLong(0);
private final AtomicLong costMicroCents = new AtomicLong(0);
// ... 延迟统计见下文
}
这样你能一眼看到GLM-5.1烧了多少钱、GLM-4调了多少次。之前用一个大桶混着装,根本没法对比。
3. P50/P95/P99分位数延迟
为啥不用平均值?因为极端值会把平均值拉偏到离谱的程度。比如99次调用都是200ms,1次调用是15秒–平均值是350ms,看起来还行,但95%的用户体验是200ms。P95才是真实体验的指标。
我用了一个简单的高效分位数算法–固定桶计数器,不需要存所有样本:
// 延迟分位数统计(桶式计数,不需要存所有样本)
private final AtomicLongArray latencyBuckets = new AtomicLongArray(50); // 50个桶,每桶200ms
public void recordLatency(long latencyMs) {
int bucket = (int) Math.min(latencyMs / 200, 49);
latencyBuckets.incrementAndGet(bucket);
}
public long getP95LatencyMs() {
// 从高桶往低桶累加,找到95%分位点
long total = getTotalCalls();
long target = (long)(total * 0.95);
long accumulated = 0;
for (int i = 49; i >= 0; i--) {
accumulated += latencyBuckets.get(i);
if (accumulated >= total - target) {
return (i + 1) * 200;
}
}
return 200;
}
整个追踪器挂在LangChain4j的ChatModelListener上,每次模型调用自动触发,不用手动埋点:
public class ProductionTokenTracker implements ChatModelListener {
@Override
public void onRequest(ChatModelRequest request) {
requestStartTimes.put(request.id(), System.currentTimeMillis());
}
@Override
public void onResponse(ChatModelResponse response) {
String model = response.modelName();
long latency = System.currentTimeMillis() - requestStartTimes.get(response.id());
ModelStats stats = modelStats.computeIfAbsent(model, k -> new ModelStats());
stats.inputTokens.addAndGet(response.tokenUsage().inputTokenCount());
stats.outputTokens.addAndGet(response.tokenUsage().outputTokenCount());
stats.callCount.incrementAndGet();
stats.addCost(calculateCost(model, response.tokenUsage()));
stats.recordLatency(latency);
}
}
对比一下OpenClaw的session_status机制–OpenClaw能告诉你session用了多少Token、当前模型是什么,但它不区分模型,不算成本,不做分位数。它是全局视角,我们是手术刀级别的精细统计。
3个意外发现
跑了两天数据之后,我发现了三个挺吓人的事儿:
意外1:输出Token比输入Token贵4倍
GLM-5.1的定价:输入¥0.5/M Token,输出¥2/M Token。意味着同样的1000 Token,输出比输入贵4倍。我之前一直以为"差不多",结果光输出Token就占总成本的70%以上。优化Agent的时候,缩减输出比缩减输入效果好得多。
意外2:P95比平均值高3倍
平均值告诉我"一次调用大概350ms",但P95是1200ms。啥意思?每20次调用里就有1次超过1.2秒。平均值骗人–它把极端值"摊平"了,让你以为一切正常。真正影响用户体验的是P95,不是平均值。
意外3:模型切换不改价格表 → 成本统计全是0
这个坑最隐蔽。我的calculateCost方法里有个价格映射表:
private static final Map<String, PricingInfo> PRICING_TABLE = Map.of(
"glm-4", new PricingInfo(0.001, 0.002), // 输入/输出每千Token
"glm-5.1", new PricingInfo(0.0005, 0.002)
);
有天我把Agent切换成了glm-4-plus,价格表里没这个key,calculateCost返回0。跑了200次调用,账本显示¥0.00。我还以为自己优化得很成功,结果月底真实账单啪啪打脸。
教训:价格表必须在模型切换的时候同步更新,或者用兜底策略(未知模型按最高价估算)。
第二部分:ASCII Dashboard面板–终端里的监控大屏
有了数据,得能看见。我不想引入什么Grafana、Prometheus–跑Agent的开发环境就一台机器,装一堆依赖太麻烦。所以用纯ASCII画了一个Dashboard,零依赖,控制台直接看。
Dashboard有6个区域,每个解决一个具体问题:
- 概览区:总调用数、总Token、总成本–一眼看全局
- 模型对比区:每个模型的调用数和成本–谁烧钱多一目了然
- 延迟分布区:P50/P95/P99–比平均值靠谱
- 成本分析区:按模型的成本占比–优化优先级
- 趋势图区:ASCII柱状图–看调用趋势
- 对比OpenClaw区:把我们的数据和session_status对齐–双保险
最有意思的是趋势图。我用renderTrendChart画ASCII柱状图,纯字符串拼出来的:
还有一个区域我觉得特别实用,是成本分析区。它不光列出每个模型花了多少钱,还会自动算占比,让你一眼看到哪个模型最"奢侈":
public String renderCostAnalysis() {
StringBuilder sb = new StringBuilder();
sb.append("\n ╔═ 成本分析 ═╗\n");
double totalCost = getTotalCostYuan();
for (Map.Entry<String, ModelStats> entry : modelStats.entrySet()) {
double modelCost = entry.getValue().getTotalCostYuan();
double percentage = (modelCost / totalCost) * 100;
sb.append(String.format(" │ %-12s ¥%.4f (%.1f%%) %s\n",
entry.getKey(),
modelCost,
percentage,
renderPercentageBar(percentage)));
}
sb.append(String.format(" │ 总计 ¥%.4f\n", totalCost));
sb.append(" ╚═══════════╝\n");
return sb.toString();
}
控制台效果:
╔═ 成本分析 ═╗
│ glm-5.1 ¥3.200 (71.4%) ████████████████████
│ glm-4 ¥1.280 (28.6%) ████████
│ 总计 ¥4.480
╚═══════════╝
⚠️ glm-5.1占比71.4%,建议对简单任务切换glm-4
71.4%–光一个模型就占了大头。如果你不看占比,光看绝对数"3块2",觉得不算多。但一算比例才发现,8成钱都花在同一个模型上。优化的时候,先砍大头才有用。
public String renderTrendChart(String title, List<Long> data, int maxWidth) {
long max = data.stream().mapToLong(Long::longValue).max().orElse(1);
StringBuilder sb = new StringBuilder();
sb.append("\n ╔═ " + title + " ═╗\n");
for (int i = 0; i < data.size(); i++) {
long value = data.get(i);
int barWidth = (int)((value / (double)max) * maxWidth);
sb.append(String.format(" %-3s │%s %d\n",
labelForIndex(i),
repeat("█", barWidth),
value));
}
sb.append(" ╚══════╝\n");
return sb.toString();
}
private String repeat(String s, int count) {
return s.repeat(Math.max(0, count));
}
控制台效果大概是这样的:
╔═ Token使用趋势(最近10次) ═╗
01 │████████████████████ 2450
02 │█████████████████ 2100
03 │███████████████████ 2300
04 │█████████ 980 ← 这里突然少了
05 │███████████████████████ 2800
06 │████████████ 1500
07 │██████████████ 1800
08 │█████████████████████ 2600
09 │███ 320 ← 又突然少了
10 │████████████████ 1900
╚══════╝
04和09那两次明显矮了–说明模型在那两次生成了很短的输出。如果正好是关键任务节点,这可不是好事。
成本优化建议自动生成也是这个Dashboard的一个实用功能。它会自动扫描当前数据,发现贵模型就提示切换:
public List<String> generateOptimizationSuggestions() {
List<String> suggestions = new ArrayList<>();
for (Map.Entry<String, ModelStats> entry : modelStats.entrySet()) {
String model = entry.getKey();
ModelStats stats = entry.getValue();
double avgCostPerCall = stats.getTotalCostYuan() / stats.callCount.get();
if (avgCostPerCall > CHEAP_MODEL_THRESHOLD) {
suggestions.add("⚠️ " + model + " 单次调用成本¥" + format(avgCostPerCall)
+ ",建议切换到更便宜的模型处理简单任务");
}
}
return suggestions;
}
还有一个挺实用的设计–速度分级进度条。根据延迟把每个模型标成快/正常/慢/很慢:
延迟评估:
glm-4 [████████████████████████████] 快 P50=180ms
glm-5.1 [████████████████████████ ] 正常 P50=450ms
glm-4-plus [██████████████ ] 慢 P50=800ms
对比OpenClaw的heartbeat健康检测–OpenClaw定时检测Agent"活着没",我们检测"活得好不好"。活着和活得好是两回事。一个Agent可能心跳正常,但每次调用都要10秒、输出质量在悄悄退化。Dashboard就是让你看到"活得好不好"。
第三部分:质量下降预警–Agent"悄悄变笨"你怎么知道?
Token追踪告诉你烧了多少钱,Dashboard让你看见数据,但还有一个更隐蔽的问题:Agent可能在悄悄变笨。
"变笨"不是我瞎说。LLM的质量退化有两种常见情况:
- 模型提供商后台调了参数(你不知道)
- 你的Prompt在多轮对话中累积了噪声(你没注意到)
这两种情况都不会报错–Agent照样跑,照样返回结果,只是结果越来越短、越来越不靠谱。如果不监控,你就是温水煮青蛙。
我定了三条监控规则,每个对应一种退化模式:
规则1:输出长度异常
突然变短,说明模型在偷懒或者输出被截断了;突然变长,说明模型在幻觉或者重复输出。两种都不是好信号。
检测方法分两层:绝对阈值 + 相对偏差。绝对阈值是硬杠–输出低于50个Token,大概率不正常。相对偏差是跟基线比–如果近5次输出的平均值跟基线偏差超过40%,告警。
public Alert checkOutputLength(String model, int outputTokens) {
ModelBaseline baseline = baselines.get(model);
// 绝对阈值:低于50 Token大概率是截断或偷懒
if (outputTokens < ABSOLUTE_MIN_THRESHOLD) {
return Alert.critical(model + " 输出仅 " + outputTokens + " Token,可能截断或偷懒");
}
// 相对偏差:近5次平均值跟基线偏差超过40%
double recentAvg = getRecentAverage(model, 5);
double deviation = Math.abs(recentAvg - baseline.avgOutputLength) / baseline.avgOutputLength;
if (deviation > RELATIVE_DEVIATION_THRESHOLD) {
return Alert.warning(model + " 输出长度偏差 " + (deviation * 100) + "%,基线="
+ baseline.avgOutputLength + ",近期=" + recentAvg);
}
return Alert.none();
}
规则2:错误率飙升
用滑动窗口看最近20次调用的出错比例。如果30%都失败了,立刻告警:
public Alert checkErrorRate(String model) {
RingBuffer<Boolean> window = errorWindows.get(model);
long errorCount = window.stream().filter(b -> b).count();
double errorRate = errorCount / (double)window.size();
if (errorRate > ERROR_RATE_THRESHOLD) {
return Alert.critical(model + " 错误率 " + (errorRate * 100) + "%,滑动窗口 "
+ window.size() + " 次中 " + errorCount + " 次出错");
}
return Alert.none();
}
这里有个取舍值得说说:为啥滑动窗口是20而不是50或100?20是个平衡点–太少(比如5)容易被偶然波动误触发,太多(比如100)会让早期问题反应太慢。Agent的典型调用间隔是秒级,20次大概覆盖最近2-3分钟的行为变化。这个时间窗足够捕捉突发问题,又不至于被远古噪声拖累。
Ring Buffer的另一个好处是内存友好–固定大小,不膨胀。如果用ArrayList存全部历史,一个Agent跑一万次调用,你就得存一万条记录。Ring Buffer永远只占20个槽位,老数据自动丢弃。
public class RingBuffer<T> {
private final Object[] buffer;
private int head = 0;
private int count = 0;
public void add(T item) {
buffer[head] = item;
head = (head + 1) % buffer.length;
if (count < buffer.length) count++;
}
public Stream<T> stream() {
return IntStream.range(0, count)
.mapToObj(i -> buffer[(head - count + i + buffer.length) % buffer.length]);
}
}
规则3:延迟退化
近5次的P95延迟如果比基线翻倍,告警。这个坑不少人会踩:基线只建一次,不更新。
为啥?因为如果基线跟着实际数据"滑动",那退化就会被"温水煮青蛙"掩盖。假设基线本来是200ms,现在退化到了400ms。如果基线也跟着更新到400ms,那下次500ms的时候,基线又更新到500ms…你永远检测不到退化,因为基线一直在"追"退化。
public Alert checkLatencyDegradation(String model) {
ModelBaseline baseline = baselines.get(model); // 只建一次,不更新
long recentP95 = calculateRecentP95(model, 5);
if (recentP95 > baseline.p95Latency * 2) {
return Alert.warning(model + " P95延迟退化:基线=" + baseline.p95Latency
+ "ms,近期=" + recentP95 + "ms(翻倍)");
}
return Alert.none();
}
告警怎么传达给开发者?我用观察者模式–AlertHandler接口。你可以把告警打印到控制台、发到飞书、记到日志,随你:
public interface AlertHandler {
void onAlert(Alert alert);
}
// 注册多个处理器,告警会广播给所有注册者
private final List<AlertHandler> alertHandlers = new ArrayList<>();
private void fireAlert(Alert alert) {
for (AlertHandler handler : alertHandlers) {
handler.onAlert(alert);
}
}
对比OpenClaw的heartbeat机制:OpenClaw检测的是"Agent有没有响应"–活没活着。我们检测的是"Agent活得好不好"–输出质量有没有退化、延迟有没有飙升。两者完全不同层级。
打个比方:heartbeat是看心电图有没有波形,质量预警是看心率是不是从60变到了120。波形存在不代表健康,心率异常才真正值得警惕。
第四部分:调用链可视化–谁调用了谁,一目了然
前面三个组件解决了"花多少钱"和"活得好不好"的问题,但还有一个问题没解决:谁调用了谁?哪个节点最耗时?
Week6的TraceListener记录的是扁平的轨迹列表–一条条日志排下来,看不出嵌套关系。像这样:
[CodeReviewer] → onRequest
[CodeReviewer] → onResponse (耗时800ms)
[CodeAnalyzer] → onRequest
[CodeAnalyzer] → onResponse (耗时1200ms)
[CodeSuggester] → onRequest
[CodeSuggester] → onResponse (耗时500ms)
这3条日志是平铺的,你看不出CodeReviewer内部调了CodeAnalyzer和CodeSuggester。想分析瓶颈?你得自己脑补调用树。
我的解决方法是用栈模拟嵌套调用,把扁平列表重构成树形:
public class CallTreeBuilder implements ChatModelListener {
private final Deque<CallNode> callStack = new ConcurrentLinkedDeque<>();
private CallNode root = null;
@Override
public void onRequest(ChatModelRequest request) {
CallNode node = new CallNode(request.modelName(), request.id());
if (callStack.isEmpty()) {
root = node; // 根节点
} else {
callStack.peek().addChild(node); // 子节点
}
callStack.push(node);
}
@Override
public void onResponse(ChatModelResponse response) {
CallNode node = callStack.pop();
node.setLatency(System.currentTimeMillis() - node.startTime);
node.setOutputTokens(response.tokenUsage().outputTokenCount());
}
}
重构后的树形结构,用ASCII渲染出来的效果:
CodeReviewer [800ms, 150 tokens]
├── CodeAnalyzer [1200ms, 300 tokens] ← 最耗时节点
└── CodeSuggester [500ms, 80 tokens]
一眼就能看到瓶颈在CodeAnalyzer–它耗了1200ms,占了总时间的60%。这比看扁平日志效率高太多了。
自动瓶颈分析也很简单–遍历树的每个节点,找耗时最高的:
public CallNode findBottleneck(CallNode root) {
CallNode bottleneck = root;
for (CallNode child : root.children) {
CallNode childBottleneck = findBottleneck(child);
if (childBottleneck.latency > bottleneck.latency) {
bottleneck = childBottleneck;
}
}
return bottleneck;
}
对比OpenClaw的session transcript–OpenClaw记录的是线性的对话历史,一条条往下排。它能看到"发生了什么",但看不到"谁嵌套了谁"。这就像看日志和看OpenTelemetry Span树的区别:日志是线性输出,Span树是嵌套结构。你需要的是后者。
类比一下:你去医院看病,医生先看你的病历档案(扁平的就诊记录),但真正能看出病因的是各项检查的关联关系(哪个指标异常导致了另一个异常)。调用链可视化就是给你的Agent做"关联分析"。
结尾:四件套 vs OpenClaw–谁的场景用谁
踩完这些坑,总结几条实用的东西。
| 组件 | 解决的问题 | 对比 OpenClaw | 适用场景 |
|---|---|---|---|
| Token追踪器 | 烧了多少钱、谁烧得多 | session_status不区分模型、不算成本 | 多模型混用、成本敏感项目 |
| ASCII Dashboard | 终端直看数据趋势 | 无内建可视化、需外部工具 | 开发环境、单机部署 |
| 质量预警 | Agent变笨了你知道吗 | heartbeat只检测"活着没",不检测质量 | 生产环境、长时运行Agent |
| 调用链可视化 | 谁调了谁、瓶颈在哪 | session transcript是线性的、无树形结构 | 多Agent协作、复杂任务链 |
怎么选也简单:
- 单模型、短任务 → Token追踪器就够了,成本是唯一的关注点
- 多模型、成本敏感 → Token追踪器 + Dashboard,看清谁烧钱
- 长时运行、质量重要 → 四件套全上,特别是质量预警,别让Agent悄悄变笨
- 多Agent协作 → 调用链可视化必上,否则瓶颈分析纯靠猜
四件套加起来不到2000行代码,零外部依赖,控制台直接看。比引入Prometheus + Grafana那套运维体系轻得多,但覆盖了Agent场景最关键的三个维度:钱、质量、调用关系。
OpenClaw的可观测性体系是平台级别的–它管的是多个Agent、多个session、跨用户的全局状态。我们的四件套是Agent级别的–管的是单个Agent的健康、成本和内部逻辑。两者互补,不是替代。
下次聊Week7 Day7–Agent部署与运维,把这四件套怎么打包成生产环境可用的体系说清楚。
如果你正在搞Agent项目,我建议先上Token追踪器–这是投入产出比最高的一个。200行代码就能让你知道每次调用花了多少钱,比事后看账单靠谱多了。质量预警和调用链可视化可以等Agent跑稳定了再加,但Token追踪应该是从Day1就有的。
有一句话我一直觉得挺对:没有监控的系统不是生产系统,是实验。 你可以实验,但别把实验当成生产。四件套就是帮你把实验变成生产的那根线。
每天分享AI Agent实战经验。踩坑不藏私,干货不打折。*
更多推荐


所有评论(0)