返回 AIPA 笔记
AIPA Day 58

Vercel AI SDK 6 实现 — usage vs totalUsage 的成本陷阱与数据点#1

Vercel AI SDK 6 实现 — usage vs totalUsage 的成本陷阱与数据点#1

2026-08-11
vercel-ai-sdk-6tool-loop-agenttoken-accountingframework-bench

日期: 2026-08-11 阶段: Phase 2 - AI-native 参考架构 标签: #vercel-ai-sdk-6 #tool-loop-agent #token-accounting #framework-bench

核心问题

Day 57 冻结了基准规范,今天把第一个框架(Vercel AI SDK 6)的对账异常调查 agent 跑通,产出数据点#1(质量/token/延迟三指标)。看似是「照文档抄个 ToolLoopAgent」的体力活,但有两个会污染整张对比表的隐藏陷阱,今天必须先钉死:

  1. token 口径陷阱ToolLoopAgent.generate() 返回的结果上有两个 usage 字段——usage(最后一步)和 totalUsage(整个 tool-loop 累计)。框架对比的成本维度若取错字段,单案 token 会差好几倍——而这正是后面要拿去对比其他三框架的数字。
  2. stopWhen 步数陷阱:AI SDK 6 把结构化输出(Output.object())也算作 tool-loop 的一个 step。基准要求「调用 finalReport 即停」,若用 Output.object 而非显式 finalReport 工具,stepCountIs(8) 会因为多吃一步而提前/延后截断,破坏与 orchestrator 的语义对齐。

今天的产出不是「AI SDK 6 多好」的结论,而是一个可信的数据点#1 + 把这两个陷阱写进基准的 score.ts,保证后三个框架用同一口径。

关键内容

A. ToolLoopAgent 的实现骨架与本仓 orchestrator 的同构关系

AI SDK 6(2025-12-22)的 ToolLoopAgent 构造签名(官方 reference):{ model, instructions, tools, stopWhen, output?, onStepFinish?, onFinish?, temperature?, seed?, ... }generate({ prompt, abortSignal, onStepFinish }) 跑完整 loop 返回 GenerateTextResult,含 text / steps / toolCalls / finishReason / usage / totalUsage

对账 agent 实现(bench/frameworks/aiSdk6/reconAgent.ts):

import { ToolLoopAgent, stepCountIs } from 'ai'   // v6
import { RECON_SYSTEM } from '../../reconciliation/system'
import { reconTools } from '../../reconciliation/tools'   // 四框架共享同一份

const reconAgent = new ToolLoopAgent({
  model: 'claude-sonnet-4-6',          // 基准锁定,隔离模型变量
  instructions: RECON_SYSTEM,          // 共享字符串
  tools: reconTools,                   // fetchLedger/fetchStatement/match/classify/finalReport
  stopWhen: stepCountIs(8),            // 对齐 orchestrator 的 8 步上限
  seed: 7,                             // 固定 seed 降低 Stability 维度的随机性
})

const res = await reconAgent.generate({ prompt: caseToPrompt(amlCase) })
// res.totalUsage → 整个 loop 的 token;res.usage → 仅最后一步

与本仓 orchestrator 的同构关系(关键观察):orchestratorAgent.ts 现在是函数式 generateText({ model, system, prompt, tools, stopWhen: ({steps}) => steps.length >= 8 })ToolLoopAgent 本质是把这套 generateText({tools, stopWhen}) 模式对象化封装——instructionssystemtools 同形(都是 Record<string, Tool> via tool()),stopWhen 同义(stepCountIs(8)({steps}) => steps.length >= 8)。所以本实现同时验证了一件对作品①有价值的事:现有 orchestrator 能平滑升级到 v6 Agent 抽象,升级动作 = 把散在闭包里的工具聚成一个可复用 Agent 实例,不改语义。

B. usage vs totalUsage:取错字段=成本数字翻倍的陷阱

这是今天最该警惕的坑。generate() 结果上:

字段含义取它会怎样
usage最后一步单次 LLM 调用的 token严重低估——丢掉中间所有 tool-loop 步的 token
totalUsage整个 loop 累计 token(含每步回灌的工具结果)正确的单案成本口径

每个 usage 对象含 { inputTokens, outputTokens, totalTokens }。一个 6 步的对账 agent:每步都把上一步的工具结果回灌进 prompt,input token 随步数累积膨胀——usage(最后一步)可能只有 800 token,totalUsage 累计可能 5000+。

反直觉洞察①(多步 agent 的成本不是「最后一次调用」,是「每步 input 的累加」,且 input 随步数二次增长):直觉以为 token 成本 ≈ 最终答案长度,所以盯 usage。但 tool-loop 的真实成本结构是:第 k 步的 input = 系统提示 + 原始 query + 前 k-1 步的全部(工具调用 + 工具结果)。即每步都把历史全量重发,input token 随步数近似二次增长(∑ 历史长度)。一个 8 步 agent 的累计 input 可能是单步的 5-10×。框架对比的成本维度必须取 totalUsage;本仓 orchestrator 早已用 r.totalUsage ?? r.usage(见 orchestratorAgent.ts 第 36/53 行)——这个 ?? 顺序就是对的:优先累计,回退单步。score.ts 严格沿用此口径,喂 estimateCost(model, totalUsage.inputTokens, totalUsage.outputTokens)

token 累积也解释了 Anthropic「multi-agent +90.2% 但 15× token」(2025-06)的成本结构来源——多 agent = 多个独立 tool-loop 各自累积 + lead 汇总时再次回灌子结果,token 乘性叠加,这正是 ADR#1(Day 33)「暂不上多 agent」的成本论据。

C. stopWhen 与 Output.object 的步数会计:为何选显式 finalReport 工具

AI SDK 6 统一了 generateText/generateObject:可以在 tool-loop 末尾用 output: Output.object(schema) 直接产出结构化结果。但官方明确警告:结构化输出生成会多占一个 step——「When using output with tool calling, adjust your stopWhen condition to account for the additional step」。

这对基准是个语义坑:基准定义「调用 finalReport 即停 / 或步数 ≥ 8」。两种实现路径:

路径终止语义步数会计基准取舍
output: Output.object(reportSchema)loop 跑完后额外一步生成结构化报告stepCountIs(8) 实际容纳 7 个工具步 + 1 个 output 步❌ 与其他框架步数口径不一致
显式 finalReport 工具(execute 内校验+回传)agent 主动调 finalReport 即自然结束8 步全部是「真实 tool-loop 步」✅ 与 orchestrator 的 finalAnswer 工具同构

显式 finalReport 工具——理由:(1) 与本仓 orchestrator 的 finalAnswerTool 模式完全一致(orchestratorAgent.ts 第 74-89 行),跨框架可复用同一终止语义;(2) 避免 Output.object 的「隐藏 +1 步」让 AI SDK 6 的步数口径与不支持该特性的框架(如手写 LangGraph 节点)对不齐;(3) finalReport 的 execute 里能塞确定性校验(如「报告的 break 必须引用真实 txId」),等于把 evalChecks.ts 的断言前移到 agent 边界。

D. 数据点#1:质量/token/延迟(带诚实限定语)

在 N=20 案 × k=5 次上跑 AI SDK 6 实现,score.ts 聚合(以下为基准框架与口径示意,真实数字执行当日回填):

指标口径AI SDK 6(示意占位,待回填)限定语
break_recall(质量)golden 答案键确定性比对待回填仅在本 20 案分布内有效
break_precision同上待回填
hallucination 数报告了不存在的 break待回填(目标 0)
单案 tokentotalUsage.totalTokens 中位数待回填取 totalUsage 非 usage(B 节)
单案成本 $estimateCost(claude-sonnet-4-6, …)待回填cost.ts 2026-05 登价
延迟 p50/p95performance.now() 包裹 generate待回填含真实 LLM 网络往返
Stabilityk=5 次 recall 方差 / break 集合 Jaccard待回填seed 固定后仍有供应商侧非确定性

反直觉洞察②(数据点#1 单独不能下任何结论,它的价值是「校准基准本身」):第一个框架跑出来的数字,最大用途不是「AI SDK 6 得分」,而是验证基准规范有没有坑——若 recall 异常低,先怀疑工具 schema 描述歧义或 golden 埋点错误,而非框架差;若 hallucination > 0,先查 finalReport 的校验是否生效。数据点#1 = 基准的烟雾测试。在拿到至少两个框架数字之前,任何「AI SDK 6 比 X 好/差」的话都是过度解读——这是 Day 60 才能做的事。

设计要点/决策表

要点决策理由
成本 token 口径totalUsageusage多步 agent input 随步数累积,usage 仅最后一步
回退顺序totalUsage ?? usage沿用 orchestrator 现有正确口径
终止实现显式 finalReport 工具,不用 Output.object避免 Output 隐藏 +1 步破坏跨框架步数对齐
模型/seedclaude-sonnet-4-6 + seed: 7隔离模型变量 + 降 Stability 随机性
stopWhenstepCountIs(8)对齐 orchestrator 8 步上限
数据点#1 定位基准烟雾测试,不下框架结论单点无对照,结论需 ≥2 框架

对本项目的落地

  • 落地 bench/frameworks/aiSdk6/reconAgent.ts:按 A 节骨架,ToolLoopAgent(v6),instructions/tools/system 全部 import bench/reconciliation/ 的固定层文件(Day 57 建),唯一框架特有代码是这个 Agent 实例的装配。onStepFinish 回调把每步 usage.totalTokens + toolCalls 推进 useTraceStore(复用本仓 trace 底座),让 bench trace 在 agent-lab 可视。
  • bench/score.ts 钉死 token 口径:导出 scoreFramework(traces) → FrameworkScore,成本取 res.totalUsage(断言非空,否则回退 usage 并打 warning),喂 estimateCostsrc/agent/shared/cost.ts);质量分复用 evalChecks.tsCheckResult 风格断言比对 break 集合;latency 用 performance.now() 中位数+p95。这个 token 口径是四框架共享的——在 score.ts 钉死一次,后三框架自动一致
  • 验证 orchestrator 可升级 v6:本实现顺带产出一条对作品①有价值的结论——generateText({tools, stopWhen})(现状)↔ ToolLoopAgent(v6)同构,orchestrator 升级 v6 是「装配重构」非「语义重写」。记入 ADR 附注(Day 60 trade-off 矩阵的工程性维度证据)。
  • 诚实标注:本笔记 D 节数字为占位,执行当日 bench/score.ts 实跑回填;reconAgent.ts 头注明确「单案成本取 totalUsage、N=20/k=5 小规模、数据点#1 仅校准基准不下框架结论」。bench 不进阻断式 CI gate(研究产物非质量门),但须保证现有 evalChecks/p1evals.test.ts 不因新增 bench 目录退化。

参考资料

  1. AI SDK — ToolLoopAgent reference(ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent):构造选项 model/instructions/tools/stopWhen/output/onStepFinish/onFinish/seed/temperaturegenerate() 返回 GenerateTextResulttext/steps/toolCalls/finishReason/usage/totalUsage);usage 含 inputTokens/outputTokens/totalTokens(2026,执行当周确认小版本)
  2. AI SDK — Agents: Loop Control + stepCountIs(ai-sdk.dev/docs/agents/loop-control):stopWhen 触发多步生成直到无 tool call 或停止条件满足;默认 stepCountIs(20)onStepFinish({stepNumber, usage, toolCalls}) 逐步回调(2026)
  3. AI SDK — Generating Structured Data / troubleshooting:AI SDK 6 统一 generateObject/generateTextOutput.object 结构化输出额外占一个 step,需相应调大 stopWhen(2026)
  4. Vercel — AI SDK 6(vercel.com/blog/ai-sdk-6,2025-12-22):Agent 一等抽象、ToolLoopAgent 生产级、@ai-sdk/mcp 稳定、rerank() 原生(2025-12)
  5. Anthropic — How we built our multi-agent research system:multi-agent +90.2% 内部 eval、15× token——token 乘性叠加的成本结构(2025-06)
  6. 本仓物证:src/agent/orchestrator/orchestratorAgent.tsgenerateText+r.totalUsage ?? r.usage 第 36/53 行、finalAnswerTool 第 74-89 行、stopWhen 第 101 行)、src/agent/shared/cost.tsestimateCost)、src/aml/evalChecks.tsCheckResult 断言)、src/agent/trace/useTraceStore.ts(trace 底座)(2026-06)

SOTA 检查 (2026-08-11)

  • AI SDK 6 ToolLoopAgent 是当前 TS 框架的标准 agent 抽象(2025-12-22 GA,2026-08 当前):usage/totalUsage 双字段、stepCountIs 停止条件、onStepFinish 逐步回调均为稳定 API;执行当周 npm view ai version 确认 6.x 小版本,核对 totalUsage 字段名未在小版本改名(v5→v6 曾有 usage 字段重命名史,须警惕)。
  • 「多步 agent 成本取 totalUsage」是硬口径不是偏好:本仓 orchestrator 早已 totalUsage ?? usage,与官方 reference 一致——取 usage(最后一步)做成本是常见踩坑,本笔记洞察①把它显式化为基准纪律。
  • Output.object 的 +1 step 是真实文档警告(官方 troubleshooting 明示),不是猜测——基准选显式 finalReport 工具规避,保证跨框架步数口径一致。
  • 过时认知警示:(1) 不可把 usage(单步)当单案成本——低估数倍(洞察①);(2) 数据点#1 不可单独下框架结论——无对照(洞察②),结论留 Day 60。
  • 待跟踪:Day 59 Claude Agent SDK 实现时,须确认其 usage 回调是否同样区分「单步 vs 累计」,若口径不同需在 score.ts 做归一化映射(否则成本维度不可比);seed 固定后 claude-sonnet-4-6 的供应商侧非确定性残差有多大,决定 Stability 维度的可解释性(k=5 是否够)。