AI SDK 7 迁移实战:TypeScript 通过后,生产环境还会坏在哪里?

AI SDK 7 迁移实战:TypeScript 通过后,生产环境还会坏在哪里?

AI SDK 7 值得升级,但生产迁移绝不是执行 Codemod、修完类型错误就结束。

对于只做文本生成的页面,升级可能看起来很简单:

perl 复制代码
-system: 'Use tools before answering.'
+instructions: 'Use tools before answering.'

流式结果也从:

dart 复制代码
-for await (const part of result.fullStream) {
+for await (const part of result.stream) {
   persistPart(part);
 }

生命周期回调建议改为:

javascript 复制代码
-onFinish({ steps }) {
+onEnd({ steps }) {
   saveRun(steps);
 }

但这些只是语法层变化。

真正影响生产环境的是:

  • 顶层 toolCalls 的聚合范围;
  • response.messages 的结构;
  • Tool Call 与 Tool Attempt 的区分;
  • Abort 后部分状态如何保存;
  • Timeout 是否真的停止底层工作;
  • Retry 是否会重复执行带副作用的 Tool。

为了把这些问题分开验证,我没有直接升级主项目,而是建立了两个隔离 Fixture。

测试环境

复制代码
xbstack-ai-sdk-7-migration-demo
├── v6
├── v7
├── migration-diff
├── benchmarks
├── docs
└── scripts

测试版本:

yaml 复制代码
Node.js: 22.18.0
AI SDK 6: ai@6.0.230
AI SDK 7: ai@7.0.31
TypeScript: strict
Provider: ai/test deterministic mock

两个版本执行相同的:

  • Prompt;
  • Mock Model;
  • Tool;
  • Tool Result;
  • 失败路径;
  • Abort 场景;
  • Retry 场景;
  • Timeout 场景。

不使用真实模型,不读取 API Key,也不受网络和模型随机性干扰。

AI SDK 7 迁移前还需要先确认两个运行时要求:

sql 复制代码
Node.js 22+
ESM only

最小配置可以写成:

json 复制代码
{
  "type": "module",
  "engines": {
    "node": ">=22"
  }
}

如果本地、CI、Serverless Runtime 或容器还停留在旧版 Node,后续的 API 修改没有意义。

同一个 Tool Calling 任务,v6 和 v7 的结果差异

测试任务:

css 复制代码
What is the status of order A-100?

模型第一步调用:

css 复制代码
lookupOrder({ orderId: "A-100" })

Tool 返回:

json 复制代码
{
  "orderId": "A-100",
  "status": "ready_for_pickup"
}

模型第二步生成:

css 复制代码
Order A-100 is ready for pickup.

两个版本的最终文本相同,Step 数量也相同。

AI SDK 6:

ini 复制代码
result.toolCalls.length = 0
result.toolResults.length = 0
result.steps[0].toolCalls.length = 1
result.steps[0].toolResults.length = 1

AI SDK 7:

ini 复制代码
result.toolCalls.length = 1
result.toolResults.length = 1
result.finalStep.toolCalls.length = 0
result.finalStep.toolResults.length = 0

在 v7 中:

ini 复制代码
result.toolCalls
= 整次 Run 中出现过的 Tool Call

result.finalStep.toolCalls
= 最后一步中的 Tool Call

最后一步只输出最终文本,所以 finalStep.toolCalls.length 是 0。

这会直接影响旧项目里的业务判断。

例如:

ini 复制代码
const usedTool = result.toolCalls.length > 0;

在本次实验中:

vbnet 复制代码
v6: false
v7: true

因此迁移时,应该把范围写清楚:

ini 复制代码
const runToolCalls = result.toolCalls;
const finalStepToolCalls = result.finalStep.toolCalls;

不要继续使用:

ini 复制代码
const toolCalls = result.toolCalls;

字段名没有消失,不代表语义没有变化。

TypeScript 通过为什么不代表迁移完成

我也运行了官方 Codemod 的隔离样本。

它处理了:

perl 复制代码
system → instructions
fullStream → stream

但样本中的 onFinish 没有自动替换成 onEnd

由于固定版本仍接受兼容别名,代码依然可以通过 TypeScript 检查。

这说明:

编译成功只能证明当前语法仍被接受,不能证明成功、失败、Abort 和 Timeout 路径中的生命周期语义仍然正确。

Codemod 适合处理机械改名,但不能完成以下审计:

  1. 顶层 Tool Call 表示最后一步还是整次 Run;
  2. Persistence 是否依赖 SDK 返回结构;
  3. Abort 后已经完成的 Tool 如何保存;
  4. Retry 是否会重复产生副作用;
  5. Timeout 是否传递到 Provider 和 Tool;
  6. 失败后系统能否恢复和回滚。

Persistence:不要直接保存 SDK 返回对象

同一个成功任务中:

ini 复制代码
AI SDK 6 response.messages = 3
AI SDK 7 response.messages = 1

AI SDK 6 的原始消息:

scss 复制代码
assistant(tool-call)
tool(tool-result)
assistant(final-text)

AI SDK 7 的原始响应消息:

arduino 复制代码
assistant(final-text)

Tool Call 和 Tool Result 并没有消失,而是由 Run 聚合字段和 Step 结构表达。

因此:

vbscript 复制代码
response.messages

适合作为当前版本的结果表示,不适合作为应用永久稳定的数据契约。

生产环境更适合自己建立领域模型:

arduino 复制代码
conversation
  ↓
message
  ↓
run
  ↓
step
  ↓
tool_call
  ↓
tool_attempt
  ↓
tool_result
  ↓
final_response

推荐的 Run 状态:

arduino 复制代码
queued
running
succeeded
failed
aborted
timed_out

消息状态至少需要:

csharp 复制代码
pending
partial
completed
failed
aborted

原始 SDK Payload 可以继续保存:

复制代码
sdk_payload_json

但只用于:

  • 调试;
  • 审计;
  • 故障复盘;
  • 版本迁移;
  • Provider 问题定位。

核心业务恢复逻辑不应依赖它。

为什么 Tool Call 和 Tool Attempt 必须分开

假设模型要求创建一个工单:

yaml 复制代码
tool_call: create_ticket #123

第一次调用超时,第二次成功:

yaml 复制代码
attempt 1: timeout
attempt 2: success

这仍然是一个逻辑 Tool Call,不是两个 Tool Call。

推荐结构:

复制代码
tool_call 1
  ├── tool_attempt 1 failed
  └── tool_attempt 2 succeeded
  └── tool_result 1

如果只保存最终状态:

ini 复制代码
tool = succeeded

运维系统会看不到第一次失败,也无法统计外部服务的重试率。

如果保存成:

ini 复制代码
tool = failed
tool = succeeded

又会误以为模型要求执行两次业务操作。

因此需要分开:

sql 复制代码
Run
Step
Tool Call
Tool Attempt
Tool Result

Abort:页面关闭后,Tool 可能已经执行成功

我测试了三个场景:

复制代码
page_closed
network_disconnected
manual_cancel

执行流程:

sql 复制代码
用户消息已保存
→ Tool Call 已记录
→ Tool 已执行
→ Tool Result 已保存
→ 等待模型生成最终文本
→ Abort

最终不变量:

ini 复制代码
partialMessagesSaved = true
finalResponseSaved = false
toolExecutions = 1
duplicateToolExecution = false

这说明 Abort 不能被理解成事务回滚。

例如 Tool 已经在 CRM 创建了一条线索,用户在最终总结生成前关闭页面。此时外部副作用已经存在。

正确做法是:

rust 复制代码
保存已完成 Tool Result
→ Run 标记 aborted
→ 不写 completed final response
→ 恢复时读取已有 Tool Result
→ 从下一步继续

有副作用的 Tool 还需要幂等键:

复制代码
conversation_id + run_id + tool_call_id

恢复前先检查:

ini 复制代码
const existing = await toolExecutions.findByIdempotencyKey(key);

if (existing?.status === 'succeeded') {
  return existing.result;
}

Retry:不要把所有失败都交给 maxRetries

至少需要区分三种重试:

复制代码
Provider Retry
Schema Retry
Tool Retry

Provider Retry 通常处理:

  • 限流;
  • 临时网络错误;
  • 服务短暂不可用。

Schema Retry 处理:

  • JSON 不合法;
  • 输出不符合 Schema;
  • 缺少必要字段。

Tool Retry 则可能产生业务副作用。

适合自动重试:

  • 幂等查询;
  • 带幂等键的写操作;
  • 明确返回临时错误;
  • 可安全重复的缓存操作。

不应默认自动重试:

  • 支付;
  • 发邮件;
  • 创建订单;
  • 发布文章;
  • 删除资源;
  • 无法判断第一次执行结果的写操作。

对于无法确认是否成功的请求,建议进入:

sql 复制代码
unknown → reconciliation

先查询外部系统,再决定是否重试。

Timeout:配置更方便,不代表底层工作已经停止

AI SDK 7 支持:

yaml 复制代码
timeout: {
  totalMs: 30_000,
  stepMs: 15_000,
  chunkMs: 5_000,
}

分别用于:

复制代码
totalMs:整次 Run 总时间
stepMs:单个步骤最大时间
chunkMs:流式 Chunk 最大静默时间

这比手动拼装 AbortController 更清晰。

但还必须确认:

  • Provider Adapter 是否监听 AbortSignal;
  • HTTP 请求是否真正被取消;
  • Tool 是否接收 Signal;
  • 长查询是否支持终止;
  • Timer 是否清理;
  • Run 是否记录为 timed_out;
  • 已完成的 Tool Result 是否保留。

如果 Mock 或 Tool 只是:

scss 复制代码
await sleep(1000);

却不监听 Signal,那么外层即使已经抛出 Timeout,内部操作仍可能继续运行。

Timeout 必须是完整的 Cancellation,而不只是调用方提前收到错误。

推荐的生产迁移顺序

不要在主项目里直接升级依赖,然后边报错边修。

更稳的顺序是:

markdown 复制代码
1. 建立 v6 和 v7 隔离 Fixture
2. 固定相同输入和成功标准
3. 对比 Tool Call、Step 和 Message 结构
4. 在 v6 阶段建立版本无关 Persistence
5. 增加 Abort、Retry、Timeout 测试
6. 增加 Tool 幂等和 Attempt 记录
7. 建立 v6/v7 Adapter
8. 先灰度无副作用查询
9. 再灰度低风险 Tool
10. 最后迁移高风险写操作

Adapter 可以通过 Feature Flag 切换:

ini 复制代码
const adapter = featureFlag.aiSdk7
  ? createV7Adapter()
  : createV6Adapter();

这样回滚不需要重新安装 npm 包,只需要切换流量和 Adapter。

上线前检查清单

迁移前至少验证:

css 复制代码
[ ] Node.js 已升级到 22+
[ ] 项目已经使用 ESM
[ ] Codemod 修改已人工复核
[ ] 顶层 Tool Call 聚合范围已确认
[ ] finalStep 使用位置已确认
[ ] 没有直接依赖 response.messages 数量
[ ] 数据库使用版本无关结构
[ ] AbortSignal 传递到 Provider
[ ] AbortSignal 传递到 Tool
[ ] 已完成 Tool Result 会立即保存
[ ] 有副作用的 Tool 具备幂等键
[ ] Tool Call 与 Tool Attempt 已分离
[ ] Timeout 后 Run 状态可解释
[ ] 不安全的 Tool 不会自动 Retry
[ ] v6 Adapter 可以随时回滚
[ ] 灰度期间有日志和告警

最终判断

AI SDK 7 最值得升级的地方,不只是新的 API 名称,而是它让整次 Run、最终 Step 和 Timeout 的边界变得更清楚。

但它不会自动解决:

  • 持久化;
  • Tool 幂等;
  • Retry;
  • 部分状态恢复;
  • 外部副作用;
  • 灰度和回滚。

真正的迁移完成标准不是:

复制代码
npm install 成功
TypeScript 通过
页面能够输出文字

而是:

复制代码
同一个 Tool 不重复执行
Abort 后状态可以解释
Retry 过程可以审计
Timeout 能真正停止工作
历史数据不依赖 SDK 版本
失败时能够灰度回滚

完整的 AI SDK 6/7 Fixture、原始 JSON 结果与 Migration Diff:

github.com/xbstack/xbs...

完整生产迁移文章:

www.xbstack.com/ai/vercel-a...

相关推荐
极光技术熊1 小时前
AI应用开发中的流式输出:从协议原理到工程实战的完整指南
后端·架构
Lcos1 小时前
我顺手跑了个 go test,结果跑出了 panic
后端
码上解惑1 小时前
2026 年智能体开发平台怎么选:从开源产品、云厂商到私有化平台
人工智能·ai·开源·智能体·spring ai
数智化管理手记1 小时前
应收应付资金占用过高怎么办?应收应付搭配账龄分析怎么做
大数据·网络·数据库·人工智能·数据挖掘
Kel1 小时前
Node.js 没那么复杂
人工智能·node.js·全栈
Revolution611 小时前
Agent 最怕的不是不会写代码:一个 TodoWrite 如何让它不跑偏?
人工智能
茶马古道的搬运工1 小时前
Qoder 多角色协同开发:用 Custom Agent 搭一条软件生产线
人工智能
程序员David1 小时前
雪花 ID + MyBatis = 隐式转 Double 撞键?我排查了一整天的隐蔽坑
后端
xd1855785551 小时前
睡眠质量评估 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙