Vercel AI SDK 6 实现 — usage vs totalUsage 的成本陷阱与数据点#1
Vercel AI SDK 6 实现 — usage vs totalUsage 的成本陷阱与数据点#1
日期: 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」的体力活,但有两个会污染整张对比表的隐藏陷阱,今天必须先钉死:
- token 口径陷阱:
ToolLoopAgent.generate()返回的结果上有两个 usage 字段——usage(最后一步)和totalUsage(整个 tool-loop 累计)。框架对比的成本维度若取错字段,单案 token 会差好几倍——而这正是后面要拿去对比其他三框架的数字。 - 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}) 模式对象化封装——instructions ↔ system,tools 同形(都是 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) | — |
| 单案 token | totalUsage.totalTokens 中位数 | 待回填 | 取 totalUsage 非 usage(B 节) |
| 单案成本 $ | estimateCost(claude-sonnet-4-6, …) | 待回填 | cost.ts 2026-05 登价 |
| 延迟 p50/p95 | performance.now() 包裹 generate | 待回填 | 含真实 LLM 网络往返 |
| Stability | k=5 次 recall 方差 / break 集合 Jaccard | 待回填 | seed 固定后仍有供应商侧非确定性 |
反直觉洞察②(数据点#1 单独不能下任何结论,它的价值是「校准基准本身」):第一个框架跑出来的数字,最大用途不是「AI SDK 6 得分」,而是验证基准规范有没有坑——若 recall 异常低,先怀疑工具 schema 描述歧义或 golden 埋点错误,而非框架差;若 hallucination > 0,先查 finalReport 的校验是否生效。数据点#1 = 基准的烟雾测试。在拿到至少两个框架数字之前,任何「AI SDK 6 比 X 好/差」的话都是过度解读——这是 Day 60 才能做的事。
设计要点/决策表
| 要点 | 决策 | 理由 |
|---|---|---|
| 成本 token 口径 | totalUsage 非 usage | 多步 agent input 随步数累积,usage 仅最后一步 |
| 回退顺序 | totalUsage ?? usage | 沿用 orchestrator 现有正确口径 |
| 终止实现 | 显式 finalReport 工具,不用 Output.object | 避免 Output 隐藏 +1 步破坏跨框架步数对齐 |
| 模型/seed | 锁 claude-sonnet-4-6 + seed: 7 | 隔离模型变量 + 降 Stability 随机性 |
| stopWhen | stepCountIs(8) | 对齐 orchestrator 8 步上限 |
| 数据点#1 定位 | 基准烟雾测试,不下框架结论 | 单点无对照,结论需 ≥2 框架 |
对本项目的落地
- 落地
bench/frameworks/aiSdk6/reconAgent.ts:按 A 节骨架,ToolLoopAgent(v6),instructions/tools/system 全部 importbench/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),喂estimateCost(src/agent/shared/cost.ts);质量分复用evalChecks.ts的CheckResult风格断言比对 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 目录退化。
参考资料
- AI SDK — ToolLoopAgent reference(ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent):构造选项
model/instructions/tools/stopWhen/output/onStepFinish/onFinish/seed/temperature;generate()返回GenerateTextResult(text/steps/toolCalls/finishReason/usage/totalUsage);usage 含inputTokens/outputTokens/totalTokens(2026,执行当周确认小版本) - AI SDK — Agents: Loop Control + stepCountIs(ai-sdk.dev/docs/agents/loop-control):
stopWhen触发多步生成直到无 tool call 或停止条件满足;默认stepCountIs(20);onStepFinish({stepNumber, usage, toolCalls})逐步回调(2026) - AI SDK — Generating Structured Data / troubleshooting:AI SDK 6 统一
generateObject/generateText;Output.object结构化输出额外占一个 step,需相应调大 stopWhen(2026) - Vercel — AI SDK 6(vercel.com/blog/ai-sdk-6,2025-12-22):Agent 一等抽象、
ToolLoopAgent生产级、@ai-sdk/mcp稳定、rerank()原生(2025-12) - Anthropic — How we built our multi-agent research system:multi-agent +90.2% 内部 eval、15× token——token 乘性叠加的成本结构(2025-06)
- 本仓物证:
src/agent/orchestrator/orchestratorAgent.ts(generateText+r.totalUsage ?? r.usage第 36/53 行、finalAnswerTool第 74-89 行、stopWhen第 101 行)、src/agent/shared/cost.ts(estimateCost)、src/aml/evalChecks.ts(CheckResult断言)、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 是否够)。