一个任务从 4 次工具调用变成 6 次,代码只多了两行 if。上线后成本涨了 40 %,P99 从 4 秒到 9 秒,错误率翻了 2 倍。团队的第一反应是"优化 prompt 省 token",但省下的那点 token 只抵掉了 3 %。
多数人把 Agent 的成本当作"token 的账",而它其实是**"轮次的账"**:每多一次工具调用,你要付的是 一次完整的模型往返(含全部上下文)+ 一次工具执行 + 一次把结果塞回上下文的空间。而这三笔是乘在一起算的,不是加在一起。
1. 一次工具调用的三层成本
| 层 | 成本 | 为什么容易被漏 |
|---|---|---|
| 模型往返 | 输入 = 当前全部上下文 | 只看"输出很短"就以为便宜 |
| 工具执行 | 延迟 + 下游压力 + 配额 | 被算成"后端的事" |
| 结果回填 | 返回内容进入下一轮输入 | 体积常被低估(一条搜索结果几千 token) |
第一行是全篇的核心机制:每次调用都要把"到目前为止的全部上下文"再发一遍。所以第 6 次调用的输入是第 1 次的数倍,而输出可能都只有几十 token。"输出短"完全不能推出"这次调用便宜"。
// 一个 6 次调用的任务,逐轮输入量的真实形状
轮1 工具调用 输入 1,200
轮2 工具结果+再调 输入 3,400 ← 回填 1,800
轮3 又调 输入 5,900
轮4 又调 输入 8,100
轮5 又调 输入 10,600
轮6 汇总 输入 13,200
// 总输入 ≈ 42,400 token,是"单轮 prompt"的 35 倍
// 而 4 次调用时总输入 ≈ 18,600 —— 多了 2 次,成本翻一倍多
2. 为什么"再加一次调用"不是线性变贵
第 N 次调用的输入包含前面所有内容,所以总成本随调用次数呈近似平方增长。
| 调用次数 | 累计输入(相对) | 累计延迟(相对) |
|---|---|---|
| 2 | 1× | 1× |
| 4 | 2.4× | 2× |
| 6 | 4.2× | 3× |
| 10 | 9.8× | 5× |
// 同一次任务,"单轮视角"与"累计视角"差 5.9 倍
单轮视角:1,200 × 6 = 7.2k token ← 严重低估
累计视角:1.2+3.4+5.9+8.1+10.6+13.2 = 42.4k ← 真实
// 且这个倍数随调用次数上升而变大,不是常数
第三行就是开头那次"多两次调用涨 40 %"的来源——它其实是 4.2/2.4 = 1.75 倍的输入量,只是被"输出很短"和"缓存命中"部分掩盖了。
3. 什么在决定"必须调几次"
| 因素 | 影响 | 能否压缩 |
|---|---|---|
| 任务的固有步骤数 | 最少调用次数的下界 | 否(除非拆任务) |
| 工具粒度 | 一个工具做多件事 → 调用少 | 能(合并工具) |
| 是否允许并行 | 并行调用不减少次数但压缩墙钟 | 能(延迟) |
| 失败重试 | 每次失败 +1 次调用 | 能(幂等 + 退避) |
| 探索式查询 | 一次查询不够、要多次试探 | 能(改查询策略) |
第二行是唯一"结构上"能减次数的手段:把"先查列表、再查详情"合并成一个返回摘要的工具,调用次数直接减半,而输入体积只增加一点点。这与"工具粒度要细"的直觉相反——细粒度省认知负担,粗粒度省调用次数,两者要按任务选。
// 两种工具设计,同一个任务的成本差一倍
细粒度: search(keyword) → get_detail(id) → get_comments(id)
3 次调用,累计输入 3 轮全上下文
粗粒度: search_with_summary(keyword) 一次返回摘要+评论数
1 次调用,输入 1 轮全上下文
// 代价:粗粒度工具返回体积大,且不灵活;适合"高频固定路径"
4. 三种"看起来省了、实际更贵"的做法
| 做法 | 表面效果 | 实际代价 |
|---|---|---|
| 自动重试 | 成功率上升 | 每次重试 = 一次完整往返 |
| 每步都验证(再调一次确认) | 更可靠 | 调用次数 ×2 |
| 用长上下文减少调用 | 次数少了 | 单次输入变巨大,且注意力衰减 |
第三行是最容易做出错误取舍的地方:把"少调几次"换成"一次塞很多",成本未必降(上下文税),质量还可能降(长上下文里的中段信息被忽略)。
// 一个可用的预算约束(写在编排层,不写在 prompt 里)
agent_budget:
max_tool_calls: 8 # 硬上限,超了降级为"给已有信息"
max_input_tokens_per_call: 12000
retry: { max: 2, backoff: exp, only_idempotent: true }
on_budget_exhausted: summarize_and_return # 不是报错,是给部分结果
5. 归因:把成本落到"轮次"上
| 指标 | 用途 |
|---|---|
| 每任务平均调用次数 | 最直接的效率指标 |
| 每任务累计输入 token | 真实成本 |
| 调用成功率(按工具) | 找出"总在重试"的工具 |
| 每工具回填体积 | 找出"返回一大坨"的工具 |
// 从日志里算"每任务累计输入",而不是"每调用平均输入"
$ python3 - <<'EOF'
rows = group_by_task(usage_log)
for task, rs in rows.items():
print(task, len(rs), sum(r.input_tokens for r in rs)) // 次数, 累计
EOF
// 判据:累计输入 / 单轮基准 > 10 → 这个任务值得单独看
第四行常被忽略:某个工具每次返回 5,000 token,它在后续每一轮里都要被重发一次——于是它一次执行的成本,被后面所有轮次放大。"减少回填体积"往往比"减少调用次数"收益更大。
6. 排错:五种"次数不多但很贵"
| 现象 | 八成原因 | 处理方向 |
|---|---|---|
| 调用 4 次成本却像 8 次 | 某工具回填体积过大 | 截断/摘要工具结果 |
| 某些任务偶尔极贵 | 重试风暴 | 只对幂等工具重试 |
| 加了一次确认后成本翻倍 | 确认也是一次完整往返 | 合并进主调用 |
| 长上下文替代多轮后更贵 | 上下文税 | 回到多轮 + 摘要 |
| 平均次数正常、P99 很高 | 少数任务在探索 | 加最大轮次限制 |
第二行值得拆开:非幂等工具的重试不只是贵,还会产生重复副作用(重复下单、重复发消息)。所以"只对幂等工具重试"既是成本纪律,也是正确性纪律。
7. 边界:什么时候该允许它多调
| 场景 | 为什么可以 | 说明 |
|---|---|---|
| 结果错了代价极高(资金/合规) | 多一次校验是划算的 | 但要接受成本 |
| 探索型任务(调研、排障) | 调用次数不可预估 | 设上限而非目标 |
| 人工在环、可随时打断 | 中断能止损 | 配合取消语义 |
| 低频高价值任务 | 次数不影响总账 | 先看总量 |
判断口径一句话:Agent 的成本单位是"轮次",不是"token"——所以优化顺序是减少调用次数 → 缩小回填体积 → 最后才抠 prompt。而"每次调用都重发全部上下文"这个机制决定了:次数与输入体积的乘积才是账单,任何只优化其中一项的做法都只能拿到部分收益。
8. 自测
- 一次工具调用有哪三层成本?为什么"输出很短"不能推出"这次调用便宜"?
- 为什么调用次数增加会让成本近似平方增长?
- 工具粒度"细"与"粗"分别省什么、费什么?什么场景该用粗粒度?
- 为什么"减少工具回填体积"的收益可能大于"减少调用次数"?
- 为什么只能对幂等工具做重试?这既是成本问题也是什么问题?
- 优化顺序应该是什么?判据是什么?
9. 一句话总结
Agent 的成本单位是轮次,不是 token:每次工具调用都要重发"到目前为止的全部上下文",于是次数与输入体积相乘才是账单——多两次调用足以让成本翻倍。优化顺序固定为减少调用次数(合并工具)→ 缩小回填体积(截断/摘要)→ 最后才抠 prompt;重试只对幂等工具开,因为非幂等重试同时放大成本与副作用。判据:先设 max_tool_calls 硬上限,超了给部分结果,而不是继续跑。