文章以验证失败后反复重传4000 Token 工具输出的案例,说明 Agent 预算可能被重试与重复上下文耗尽。作者提出按步骤计量用量、识别五类浪费,再决定是否调整模型路由。
我曾合作过的一个团队,有个每晚执行的 Agent 任务,原计划运行六个小时。结果到了上午 11 点,当天的额度就耗尽了,而九份摘要里只有两份能用。
没人说得清,到底是哪一步吃掉了这些 token。router 日志能看到请求,却看不到成本。于是,大家的第一反应就是怪模型。
问题不在模型。真正的原因是第 7 步:校验连续失败了四次,每次重试都重新发送了同一份 4,000 token 的工具输出。
这个小案例浓缩了整个问题:Agent 预算很少被某一次巨大的调用耗尽。真正耗尽预算的,是请求日志里没有记录的结构性浪费。
下面是我现在动手修改之前都会做的一套核算:一个小型计量脚本、五种浪费特征,以及修复它们的先后顺序。
路由决策不该是第一个调整项。如果计费 token 中有 40% 都花在某个始终失败的调用重试上,怎么重新路由都没用;你只是在用更低的成本继续失败。
先按步骤计量。这样,路由问题才能用数字来回答,而不是凭感觉。
第六个术语最重要。一棵每次运行都给出三条建议的决策树,没人会照着执行。
每个步骤需要三个数字,provider 已经在 usage 中返回了这些数据。另外,你还要自己补上两项标注:有多少 token 是静态内容,有多少 token 是从前面的步骤原样复用的。
这些标注可以手动填写,也可以用启发式方法估算。这恰恰是它的价值所在:它会迫使你认真看一遍 prompt。
#!/usr/bin/env python3
"""meter.py - per-step token accounting for agent runs. Python 3.10+, stdlib only."""
from __future__ import annotations
import json
import sys
from dataclasses import dataclass
@dataclass
class Step:
name: str
prompt_tokens: int
completion_tokens: int
static_tokens: int = 0 # system prompt + tool schemas, paid per call
reused_tokens: int = 0 # context carried over unchanged from an earlier step
attempt: int = 1
ok: bool = True
def load(path: str) -> list[Step]:
with open(path, encoding="utf-8") as fh:
return [Step(**row) for row in json.load(fh)]
def summarize(steps: list[Step]) -> dict:
prompt = sum(s.prompt_tokens for s in steps)
completion = sum(s.completion_tokens for s in steps)
if not steps or prompt == 0:
return {"steps": len(steps), "billed_tokens": prompt + completion}
billed = prompt + completion
wasted_retry = sum(
s.prompt_tokens + s.completion_tokens for s in steps if not s.ok
)
ok_steps = [s for s in steps if s.ok]
first = ok_steps[0].prompt_tokens if ok_steps else 0
last = ok_steps[-1].prompt_tokens if ok_steps else 0
return {
"steps": len(steps),
"billed_tokens": billed,
"overhead_ratio": round(sum(s.static_tokens for s in steps) / prompt, 3),
"retry_amplification": round(wasted_retry / billed, 3),
"redundancy_ratio": round(sum(s.reused_tokens for s in steps) / prompt, 3),
"prompt_growth": round((last - first) / first, 3) if first else 0.0,
}
ACTIONS = {
"B_retry": "install a stop rule before touching anything else",
"A_overhead": "shrink the system prompt and tool schemas",
"C_redundancy": "collapse steps or externalize state",
"D_drift": "cap the transcript, move history to a scratch file",
"F_leave_it": "budget is healthy; change nothing",
}
def classify(m: dict) -> str:
if not m.get("billed_tokens"):
return "F_leave_it"
if m["retry_amplification"] > 0.25:
return "B_retry"
if m["overhead_ratio"] > 0.60:
return "A_overhead"
if m["redundancy_ratio"] > 0.40 and m["steps"] >= 8:
return "C_redundancy"
if m["prompt_growth"] > 0.30:
return "D_drift"
return "F_leave_it"
def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: meter.py steps.json", file=sys.stderr)
return 2
report = summarize(load(argv[1]))
leaf = classify(report)
report["leaf"] = leaf
report["first_action"] = ACTIONS[leaf]
print(json.dumps(report, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
输入是一个扁平的 JSON 数组。用三行数据就足以展示它的结构:
[
{"name": "plan", "prompt_tokens": 3200, "completion_tokens": 410, "static_tokens": 2400, "reused_tokens": 0, "attempt": 1, "ok": true},
{"name": "fetch_schema", "prompt_tokens": 5100, "completion_tokens": 120, "static_tokens": 2400, "reused_tokens": 2600, "attempt": 1, "ok": true},
{"name": "validate_schema", "prompt_tokens": 5400, "completion_tokens": 90, "static_tokens": 2400, "reused_tokens": 2600, "attempt": 3, "ok": false}
]
python meter.py steps.json
注意:这个脚本使用 Python 3.10+,不依赖第三方库。我只用合成的 usage 字典测试过它,因此,应当把它的输出看作对运行结构的描述,而不是任何模型或 provider 的基准测试结果。
分支的排列依据是忽视问题的代价,而不是形式上是否优雅。首先检查重试,是因为它修起来成本最低,放着不管的代价却最高。
retry_amplification > 0.25 → 叶节点 B。四分之一的预算花在了失败的尝试上。应该更早终止运行。
overhead_ratio > 0.60 → 叶节点 A。每次 prompt 的大部分内容,都是你写过一次、此后却一直在付费的固定模板。
redundancy_ratio > 0.40,且至少有 8 个步骤 → 叶节点 C。长流程一直在重复发送相同的材料。
prompt_growth > 0.30 → 叶节点 D。对话记录的增长速度超过了实际工作的推进速度。
以上均不满足 → 叶节点 F。什么都不用做。这个叶节点确实有必要存在,而且应该经常命中。
比例只能帮助诊断;你还需要在代码里设置强制停止条件。
DAILY_ALLOWANCE = 1_000_000 # your number, from your own account
MAX_WASTE_RATIO = 0.25
wasted = 0
for step in steps:
result = run_step(step)
if not result.ok:
wasted += result.prompt_tokens + result.completion_tokens
if wasted > DAILY_ALLOWANCE * MAX_WASTE_RATIO:
raise SystemExit("stop: waste budget exhausted, fix the failing step first")
这样就把看不见的资源流失变成了看得见的崩溃。崩溃会有人修;资源流失只会继续出现在账单上。
下面的数字来自用于说明问题的示例工作负载,并非对任何产品的实测结果。
叶节点 B——重试放大比例为 0.41。 九个步骤,计费 token 共 180k,其中 74k 花在了失败的校验尝试上。第一步:让校验器返回准确的字段路径,而不是 "invalid input"。第二步:应用上面的停止规则。预期效果有明确边界:你能限制损失,却无法收回已经消耗的 token。
叶节点 A——固定开销比例为 0.72。 平均每次 prompt 为 4,300 token,其中包含一份 3,100 token 的 system prompt 和四份 tool schema。第一步:把三个很少使用的工具移到第二次调用中。如果某份 schema 只有在规划之后才需要,就不该放进规划器的 prompt。
叶节点 C——冗余比例为 0.55,共 12 个步骤。 同一份代码仓库结构图被粘贴进了八个 prompt。第一步:将它移到外部文件中,让 Agent 按路径读取,只在 prompt 中传入一行索引。第二步:检查第 4 步和第 5 步能否合并。这些步骤之所以分开,往往是因为过去存在某个执行顺序约束,而如今这个约束已经不成立了。
叶节点 D——prompt 增长比例为 0.84。 第一次成功调用的 prompt 为 2,900 token,最后一次为 5,300 token。第一步:每三轮对话做一次摘要,写进临时笔记,然后移除原始对话。第二步:重新计量。如果仍在增长,说明 Agent 正在积累自己根本不会读取的状态。
叶节点 F——所有指标均低于阈值。 这套流程的正确输出,往往就是“不做改动”。记录报告,为 retry_amplification 设置告警,然后去做别的事。
有一种叶节点能从低成本算力中受益,但它并不是人们以为的那个。
叶节点 C 和叶节点 D 往往需要一个摘要器:在昂贵步骤读取历史记录之前,先用一个步骤将历史压缩。这一步重复性高、风险低,而且容易校验,很适合交给免费 endpoint,因为糟糕的摘要会在下一步明显暴露问题,而不是悄悄出错。
MonkeyCode 适合在这个环节进入工作流,而不是出现在文章标题里。该项目宣称提供免费的模型访问和免费服务器选项,撰文时还提供 1,000 万 token 的额度。这些都是运营方公布的数字,在据此制定计划之前,你应该先核实当前条款。利益披露:本文是作为 MonkeyCode 产品推广活动的一部分撰写的。
我不会先把规划步骤迁过去。先迁移那个最容易发现失败的步骤,再跑一次计量脚本,对比迁移前后的 billed_tokens。如果数字没有变化,免费额度就只是分散了注意力,没有带来优化。
usage 核算方式无法直接跨 provider 通用。 不同 provider 对缓存 token、推理 token 和工具调用 token 的报告方式各不相同。必须先统一口径,否则算出来的比例会误导你。
标注需要手动完成。 static_tokens 和 reused_tokens 来自你对 prompt 的阅读判断,而不是 API 返回的数据。
它衡量的是消耗,不是质量。 一次运行即使命中叶节点 F,结果也可能毫无用处。还需要配合任务级别的成功检查。
如果已经有 tracing,可以跳过。 如果你的技术栈已经记录了每个 span 的 token 数量,而且你信任这些数据,就直接用它们,只借鉴这里的修复顺序。
如果延迟才是主要瓶颈,可以跳过。 在受延迟限制的流水线中,为了节省 token 而缩短 prompt,可能付出的代价比节省的还多。
修复顺序比阈值更重要。先处理重试,再处理固定模板,然后处理冗余,最后处理增长。做完这些,通常就不需要再改什么了。
如果今天就想试试,把一次运行的 usage 字段保存成 JSON,再用计量脚本分析。整个过程很小,午饭前就能完成,而它返回的叶节点,比再重写一遍 router 更适合作为起点。如果你判断摘要器值得迁移,该项目的免费额度是个合理的试验选择——同时,继续运行计量脚本。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。