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 适合处理机械改名,但不能完成以下审计:
- 顶层 Tool Call 表示最后一步还是整次 Run;
- Persistence 是否依赖 SDK 返回结构;
- Abort 后已经完成的 Tool 如何保存;
- Retry 是否会重复产生副作用;
- Timeout 是否传递到 Provider 和 Tool;
- 失败后系统能否恢复和回滚。
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:
完整生产迁移文章: