AI SDK 7 迁移实战:流式中断、Cloudflare 524 边界与 Tool Call 恢复
模型层结果来自 Node.js 22.18.0、[email protected] 与 [email protected] 的隔离 Mock;断线层使用真实 localhost HTTP/SSE 与 TCP 连接;524 仅为本地反向代理模拟。本文不代表真实模型质量、供应商网络延迟、Cloudflare Ray ID 或计费表现。
AI SDK 7 迁移实战:流式中断、Cloudflare 524 边界与 Tool Call 恢复
先说结论。
AI SDK 7 值得升级,但它不是一次“把 system 改成 instructions、把 fullStream 改成 stream”就结束的版本升级。
真正决定迁移是否安全的,是下面五件事:
- 你原来读取
toolCalls和toolResults的代码,到底把它们理解成“最后一步”,还是“整次 Run”; - 你有没有把
response.messages直接塞进数据库,并默认它的数量和层级长期不变; - 用户关闭页面、网络中断或主动取消时,已经执行完成的 Tool 要怎么保存;
- Tool 第一次失败、第二次成功时,你能不能区分 Run、Step、Tool Call 和 Tool Attempt;
- Timeout 到底只是让请求报错,还是能完整传递到 Provider、Tool 和持久化层。
我没有用真实模型做“感觉更快”“好像更稳定”这种主观判断,而是在 XBSTACK 里建立了一个隔离实验仓库:
xbstack-ai-sdk-7-migration-demo
├── v6
├── v7
├── migration-diff
├── benchmarks
├── docs
└── scripts
两个 Fixture 使用相同输入、相同 Mock Model、相同 Tool、相同失败路径。它们不读取 API Key,不调用外部模型,不受网络波动、模型随机性和账单限制影响。
实验环境固定为:
- Node.js:
22.18.0 - AI SDK 6:
[email protected] - AI SDK 7:
[email protected] - TypeScript:严格模式
- Provider:
ai/test中的确定性 Mock
完整仓库会放在:
https://github.com/xbstack/xbstack-ai-sdk-7-migration-demo
这篇文章不讨论模型质量,也不宣称 v7 一定更快。它只回答一个生产问题:当同一套 Tool Calling 工作流从 v6 切到 v7,控制流、返回结构和可靠性边界到底发生了什么。
一、为什么 AI SDK 7 值得关注
AI SDK 过去容易被当成一个“帮前端流式输出文字”的封装层。只做一个聊天页面时,这种理解勉强够用:传入模型和 Prompt,拿到文本流,再把字符一点点显示出来。
但生产系统里的 AI 调用很少停留在文本生成。
一个真实请求通常会经过:
用户消息
↓
模型判断是否调用工具
↓
Tool Call
↓
Tool Result
↓
模型继续生成
↓
最终回复
再复杂一点,还会出现:
Tool 失败
↓
Retry
↓
用户取消
↓
恢复执行
↓
另一个 Tool
↓
人工审批
到了这个阶段,SDK 不再只是 UI 辅助库,它事实上参与了 Agent Runtime 的控制流。它返回的 Step、Tool Call、Tool Result、Message 和 Finish Reason,会直接进入数据库、日志、审计系统和故障恢复逻辑。
AI SDK 7 值得关注,主要不是因为几个名称更统一,而是它让“整次 Run”与“最后一步”的边界变得更明确,并加入更直接的超时配置。对新项目而言,这些设计更适合构建可观测的多步工作流。
问题也正出在这里:
当语义变得更明确时,旧代码里那些没有被明确写出来的假设就会暴露。
例如,v6 代码可能长期这样写:
const calls = result.toolCalls;
开发者心里默认它是“最后一步的 Tool Call”。迁移到 v7 后,顶层字段更接近整次 Run 的聚合。如果业务逻辑还按旧理解判断“最终步骤有没有工具”,结果就会错。
同样,很多项目把:
result.response.messages
直接保存为完整历史。只要它能恢复一次对话,就认为这个结构稳定。我们的实验显示,同一个两步任务在 v6 和 v7 中,原始响应消息数量分别是 3 和 1。最终文本相同,Tool 也相同,但消息结构并不相同。
这意味着,AI SDK 7 的迁移价值和迁移风险来自同一个地方:它逼着项目重新定义自己的运行时边界。
二、v6 的真实问题,不是“旧”,而是应用层太容易偷懒
AI SDK 6 并不是不能用于生产。事实上,很多系统已经基于它稳定运行。问题是 v6 的使用方式很容易让应用把 SDK 结果对象当成自己的领域模型。
1. 把 SDK Step 当成数据库 Step
SDK 的 Step 是一次模型调用或工具往返过程的结果表示。数据库里的 Step 则承担恢复、审计、幂等和状态追踪。
两者看起来相似,但职责不同。
SDK Step 可以随版本升级改变字段聚合方式。数据库 Step 不能因为升级一个 npm 包就改变主键、状态机和历史解释。
如果项目直接序列化整个 Step:
await db.steps.insert({
runId,
payload: JSON.stringify(step),
});
短期很方便,长期会遇到三个问题:
- 查询困难:无法直接筛选失败 Tool、超时 Step 或特定 Tool Name;
- 迁移困难:SDK 字段变化会污染历史数据解释;
- 恢复困难:你不知道哪些字段是业务事实,哪些只是当时 SDK 的展示形式。
2. 把 response.messages 当成完整会话
很多示例会建议把响应消息追加回下一轮输入。这在内存 Demo 里没有问题,但生产持久化不能只考虑“下一次能不能继续问”,还要考虑:
- 这条消息是否已经完整;
- Tool 是否产生外部副作用;
- Tool Result 是否已经提交;
- 用户是否在最终回复前取消;
- 恢复后是否会重复执行 Tool;
- 不同 SDK 版本能否读取旧数据。
本次实验中,v6 的成功任务返回 3 条原始响应消息:
assistant(tool-call)
tool(tool-result)
assistant(final-text)
这个形状非常直观,因此开发者很容易把它当成天然数据库结构。
但“直观”不代表“稳定契约”。
3. Timeout 由应用自己拼装
v6 可以通过 AbortController 实现超时,但应用需要自己创建 Timer、触发 Abort、清理 Timer,并保证 Provider 和 Tool 真正监听 Signal。
一个常见误区是:
Promise.race([
generateText(...),
timeoutPromise,
]);
这只能让调用方先收到超时错误,并不保证底层模型请求或 Tool 已停止。如果底层工作仍然运行,后续可能出现:
- 请求已经返回 504,Tool 却继续写数据库;
- 用户重新提交后,同一订单被处理两次;
- 计费请求继续消耗 Token;
- 后台出现“幽灵 Run”。
真正的 Timeout 必须是 Cancellation,Cancellation 必须贯穿整个调用链。
4. Retry 经常被混成一个参数
maxRetries 很容易让人产生一种错觉:只要设置为 2,整个 Agent 就获得了可靠性。
实际上至少有三种 Retry:
- Provider 请求因为限流或临时网络错误重试;
- 模型输出不符合 Schema,重新请求模型;
- Tool 调用外部系统失败,重新执行 Tool。
第三种最危险,因为 Tool 可能有副作用。
一次“创建工单”失败,可能只是响应丢失,工单实际上已经创建。如果盲目重试,就会生成重复工单。SDK 不可能替应用知道这个业务事实。
因此,v6 的真实问题不是功能不足,而是它给应用留下了大量可以“先不设计”的空间。当系统从 Demo 长成生产服务,这些被省略的边界会集中变成迁移成本。
三、迁移过程:先隔离,再比较,不在主项目里直接升版本
我没有在 XBSTACK 主站的依赖树里直接把 ai 从 6 升到 7。
原因很简单:主项目同时包含 Astro、React、管理后台、增长系统、内容发布和多个工具页面。直接升级后,即使 TypeScript 报错,也很难判断问题来自 AI SDK 本身、Provider Package、UI Hook,还是主项目已有代码。
更稳的做法是建立两个完全隔离的 Fixture:
v6/
├── package.json
├── package-lock.json
├── tsconfig.json
└── src/experiment.ts
v7/
├── package.json
├── package-lock.json
├── tsconfig.json
└── src/experiment.ts
每个目录有自己的:
node_modules- AI SDK 版本
- TypeScript 配置
- Lockfile
- Test 命令
根目录只负责编排:
npm run setup
npm test
完整测试会依次执行:
v6 TypeScript
↓
v7 TypeScript
↓
v6 Runtime Fixtures
↓
v7 Runtime Fixtures
↓
生成 results.json
↓
生成 comparison.md
这个结构有三个好处。
第一,比较公平。两个版本运行相同业务任务,不会被主项目其他依赖干扰。
第二,结果可复现。任何人 Clone 后不需要 API Key 就能看到相同结构结果。
第三,它可以进入 CI。以后升级 Provider 或 AI SDK Patch Version 时,直接重跑相同场景,判断变化发生在什么位置。
四、代码 Diff:Codemod 能做什么,不能做什么
在处理字段改名之前,先确认两个无法靠 Codemod 绕过的运行时门槛:
- AI SDK 7 要求 Node.js 22 或更高版本;
- AI SDK 7 只支持 ESM 导入,不再支持 CommonJS
require()。
项目、CI、Serverless Runtime 和本地开发环境必须同时满足这两个条件。一个最小的包配置可以写成:
{
"type": "module",
"engines": {
"node": ">=22"
}
}
这不是代码风格选择。Node 版本或模块系统不满足时,后面的类型修复、Tool Calling 和 Persistence 迁移都没有意义。Vercel 在 AI SDK 7 官方发布与迁移说明 中把 Node.js 22 和 ESM 列为升级前的两个破坏性要求。
最明显的 API Diff 是:
const result = await generateText({
model,
- system: 'Use tools before answering.',
+ instructions: 'Use tools before answering.',
prompt,
- onFinish({ steps }) {
+ onEnd({ steps }) {
saveRun(steps);
},
+ timeout: { totalMs: 30_000, stepMs: 15_000 },
});
流式结果:
const result = streamText({ model, prompt });
-for await (const part of result.fullStream) {
+for await (const part of result.stream) {
persistPart(part);
}
顶层 Tool Call 的使用方式需要从“默认理解”改成“明确选择”:
-const finalToolCalls = result.toolCalls;
+const allRunToolCalls = result.toolCalls;
+const finalToolCalls = result.finalStep.toolCalls;
我也运行了官方 Codemod 的隔离样本。观察到它会修改:
system→instructionsfullStream→stream
但在这个样本里,onFinish 没有被自动改成 onEnd。由于固定版本仍接受兼容别名,转换后的代码可以通过 TypeScript 检查。
这恰恰说明:
TypeScript 通过不等于迁移完成。
兼容别名可以让代码编译,但不会替你验证生命周期回调在成功、失败、Abort 和 Timeout 路径中是否仍符合预期。
Codemod 适合处理机械改名,不适合处理语义迁移。生产迁移至少要做四类人工审计:
- 顶层聚合字段的含义;
- 最终 Step 的读取方式;
- 持久化结构;
- Abort、Retry 和 Timeout 的状态机。
五、Tool Call 变化:同一个任务,顶层结果不再是同一种理解
实验任务很简单。
用户问:
What is the status of order A-100?
模型第一步返回:
lookupOrder({ orderId: "A-100" })
Tool 返回:
{
"orderId": "A-100",
"status": "ready_for_pickup"
}
模型第二步返回:
Order A-100 is ready for pickup.
两个版本最终文本完全一致,Step 数量都是 2。
差异出现在 Tool Call 聚合。
AI SDK 6 结果
result.toolCalls.length = 0
result.toolResults.length = 0
result.steps[0].toolCalls.length = 1
result.steps[0].toolResults.length = 1
AI SDK 7 结果
result.toolCalls.length = 1
result.toolResults.length = 1
result.finalStep.toolCalls.length = 0
result.finalStep.toolResults.length = 0
从 v7 的角度看,顶层 toolCalls 表示整次 Run 里出现过的 Tool Call;finalStep 表示最后一步,而最后一步只输出文本,所以 Tool Call 是 0。
这个设计更清晰,但它会破坏旧代码中的隐含假设。
例如,一个项目可能用下面逻辑决定是否把回复标记为“工具回复”:
const usedTool = result.toolCalls.length > 0;
在 v6 Fixture 中,这个值是 false;在 v7 中是 true。业务输出相同,标签却不同。
再例如,项目可能用:
if (result.toolResults.length === 0) {
markRunAsNoToolResult();
}
在 v6 中会被误标记,在 v7 中不会。
迁移时不能简单问“字段还在不在”,要问:
- 它现在聚合的是哪一个范围;
- 旧代码当初期待的是哪一个范围;
- 业务判断应该基于整次 Run,还是最终 Step。
我的建议是直接在代码变量名里写出语义:
const runToolCalls = result.toolCalls;
const finalStepToolCalls = result.finalStep.toolCalls;
不要再使用含糊的:
const toolCalls = result.toolCalls;
变量名不能解决所有问题,但可以阻止后续开发者再次把不同层级混在一起。
六、Persistence:不要把 SDK 返回对象当成数据库
这是整个迁移中最重要的一部分。
同一套成功任务,实验观察到:
AI SDK 6 response.messages = 3
AI SDK 7 response.messages = 1
v6 的三条消息分别是:
assistant(tool-call)
tool(tool-result)
assistant(final-text)
v7 的一条原始响应消息是:
assistant(final-text)
Tool Call 和 Tool Result 并没有消失,而是通过整次 Run 的聚合字段和 Step 数据表达。
这说明一个关键事实:
response.messages 是 SDK 为当前版本提供的响应表示,不是应用的永久事件日志。
如果数据库直接存储原始 Message Array,会出现两个选择:
- 升级后把新结构继续写入旧字段,导致同一列里混合两种语义;
- 做一次历史数据迁移,把旧消息转换为新结构。
两种都不理想。
更稳的办法是在 SDK 之外建立版本无关的事件模型:
conversation
↓
message
↓
run
↓
step
↓
tool_call
↓
tool_attempt
↓
tool_result
↓
final_response
conversations
保存长期会话边界:
id
user_id
created_at
metadata
messages
保存用户和助手可见消息:
id
conversation_id
role
status
content_json
sdk_payload_json(optional)
status 不应该只有 completed,还要包含:
pending
partial
completed
failed
aborted
runs
一次用户消息可能触发一次或多次 Run:
id
message_id
status
sdk_version
abort_reason
idempotency_key
started_at
finished_at
Run 状态建议至少包含:
queued
running
succeeded
failed
aborted
timed_out
steps
Step 是 Run 内部的顺序执行单元:
id
run_id
sequence
status
finish_reason
tool_calls 与 tool_attempts
必须把“逻辑 Tool Call”和“执行尝试”分开。
一个 Tool Call 代表模型要求完成一件业务操作:
创建工单
查询订单
发送邮件
更新 CRM
一个 Tool Attempt 代表这件事的第几次执行。
tool_call: create_ticket #123
├── attempt 1: timeout
└── attempt 2: success
如果把每次 Attempt 都保存成新的 Tool Call,恢复逻辑会误以为模型要求创建两个工单。
统一模型建立后,SDK 适配器只负责转换:
function normalizeV6Result(result) {
// v6 Step / response messages → application events
}
function normalizeV7Result(result) {
// v7 full-run aggregation / finalStep → application events
}
数据库不需要知道 fullStream 还是 stream,也不需要知道顶层 Tool Call 是最终 Step 还是整次 Run。
原始 SDK Payload 仍然有价值,可以保留在:
sdk_payload_json
但它的定位应是:
- 调试证据;
- 迁移审计;
- 故障复盘;
- Provider 问题定位。
它不应该成为核心业务查询依赖。
七、Abort:页面关闭不是“什么都没发生”
我们补了三个 Abort 场景:
page_closed
network_disconnected
manual_cancel
每个场景都执行相同流程:
用户消息已保存
↓
模型发出 Tool Call
↓
Tool 执行完成
↓
Tool Result 已保存
↓
第二步模型生成等待中
↓
触发 Abort
最终验证四个不变量:
partialMessagesSaved = true
finalResponseSaved = false
toolExecutions = 1
duplicateToolExecution = false
这组测试的价值在于,它纠正了一个常见误解:
用户断开连接,不代表后端没有完成任何工作。
假设 Tool 是“向 CRM 创建一条销售线索”。用户在模型准备总结时关闭页面,CRM 里的记录已经创建。此时如果应用因为没有最终回复,就删除整个 Run,下一次恢复可能重新执行同一 Tool,产生重复线索。
正确处理方式是:
- 用户消息先于模型执行持久化;
- Tool Call 在执行前记录;
- Tool Result 完成后立即提交;
- Abort 到达时,Run 标记为 aborted;
- 不写入 completed 的 final_response;
- 恢复时读取 Tool Result,从下一步继续,而不是重放整次请求。
Tool 必须有幂等键
对有副作用的 Tool,建议使用:
idempotency_key = conversation_id + run_id + tool_call_id
外部系统支持幂等键时,直接传递。
外部系统不支持时,应用自己保存执行锁:
pending → running → succeeded
恢复前先查:
const existing = await toolExecutions.findByIdempotencyKey(key);
if (existing?.status === 'succeeded') {
return existing.result;
}
Abort 不是回滚,更不是删除历史。它只是说明:这次 Run 没有完成到最终回复。
八、真实 HTTP/SSE 断线:请求绑定、后台继续和幂等恢复
前面的 Abort Fixture 能稳定验证状态机,但它仍然是进程内的确定性测试。为了确认真实连接关闭时应用层到底看到什么,我又增加了一组 Node.js HTTP/SSE 补测。
测试环境保持不变:
- Node.js:
22.18.0; - AI SDK:
[email protected]; - 客户端:Node 原生
fetch; - 服务端:Node 原生 HTTP Server;
- 传输:真实 localhost TCP + SSE;
- Tool:写入内存账本的确定性订单查询;
- 外部模型:未调用;
- 公网:未使用。
这里的“真实”仅指客户端与服务端通过实际 HTTP 连接通信,并真的关闭响应流,不代表真实模型供应商、Serverless 平台或公网网络质量。
第一组:执行生命周期绑定到请求
服务端流程如下:
客户端建立 SSE
↓
Tool Call 写入 planned / executing
↓
Tool 完成,Tool Result 立即持久化
↓
客户端收到 tool_result 后断开
↓
request close 触发 AbortSignal
↓
Run 标记 aborted,不保存 final_response
断开后的状态为:
| 字段 | 实际结果 |
|---|---|
run.status | aborted |
toolExecutions | 1 |
toolResultSaved | true |
finalResponseSaved | false |
disconnectObserved | true |
这说明只要 Tool Result 在完成时独立提交,客户端断开不会抹掉已经发生的业务结果。真正危险的是把 Tool Result 和最终助手消息放在同一次事务末尾保存:最终流没结束,前面的副作用也会失去可恢复证据。
恢复执行:Tool 总执行次数仍然是 1
随后我用同一个 run_id 和同一个幂等键调用恢复接口:
idempotency_key = direct-run:lookupOrder:A-100
恢复流程先查询 Tool Ledger。它发现此前结果已经完成,因此写入一条 reused 事件,把既有 Tool Result 重新交给下一步,而不是再次执行 Tool。
恢复后的断言是:
tool executions before resume = 1
tool executions after resume = 1
duplicate executions = 0
final response saved = true
这也是 consumeStream() 不能替代幂等控制的原因。AI SDK 的消息持久化文档说明,consumeStream() 可以在客户端断开后继续在服务端消费流并触发完成回调;文档同时要求生产系统保存请求的进行中/完成状态,并在更复杂场景增加可恢复流。它解决的是流消费和回调生命周期,不知道“创建订单”“发邮件”或“写 CRM”是否已经产生副作用。
第二组:执行与响应生命周期解耦
另一条路由没有把 Run 的取消信号绑定到客户端连接。客户端仍然在收到 Tool Result 后关闭连接,但后台任务继续运行,最终状态是:
| 字段 | 实际结果 |
|---|---|
disconnectObserved | true |
run.status | completed |
toolExecutions | 1 |
finalResponseSaved | true |
这证明“连接断了,但任务继续”在控制流上可行;它并不证明一个普通的 void promise 已经具备生产耐久性。进程重启、部署切换、实例回收和长时间人工审批仍然需要 Queue、Worker 或持久工作流来拥有任务。
AI SDK 7 官方发布说明把 WorkflowAgent 定位为可跨部署、进程重启、中断和延迟审批保存执行状态的耐久 Agent。但即使使用 WorkflowAgent,Tool 的业务幂等键、外部副作用对账和权限边界仍然属于应用层。
第三组:Cloudflare 524 与 chunkMs 不是同一层
我还建立了一个本地反向代理,把读取静默预算压缩到 70ms:
- 源站在预算内没有返回首字节:代理返回模拟
524; - 源站每
35ms返回一次 heartbeat:请求最终返回200。
测试结果:
slow first byte → 524
periodic heartbeat → 200
这组结果只验证代理层与应用状态之间的关系。它不是一次真实 Cloudflare 边缘节点测试,没有公网域名、Cloudflare Ray ID 或线上截图。
Cloudflare 官方文档说明,524 表示 Cloudflare 已连接到源站,但源站没有在默认 120 秒 Proxy Read Timeout 内及时返回 HTTP 响应;对长时间 HTTP 任务,官方建议使用状态轮询等方式。这里需要把四层超时彻底分开:
| 控制层 | 负责的问题 | 不能替代什么 |
|---|---|---|
| 浏览器/Request | 用户关闭页面、网络断开 | 不能判断 Tool 是否已完成 |
AI SDK chunkMs | 流中相邻 Chunk 静默过久 | 不能修改 Cloudflare 代理预算 |
AI SDK toolMs | 单个 Tool 执行超时 | 不能自动完成副作用对账 |
| Cloudflare 524 | 代理等待源站响应超时 | 不能替应用保存 Run/Step 状态 |
这组补测暴露出的四个失败路径
第一种失败,是把请求取消和任务取消当成同一个状态。请求绑定模式中,客户端关闭 SSE 后,服务端收到了连接关闭事件,Run 被标记为 aborted。如果应用同时把数据库事务也绑定在最终响应回调上,那么 Tool 已经完成,事务却因为最终文本没有生成而整体回滚。用户重新进入页面时,数据库看不到 Tool Result,只能从头重放请求。对查询类 Tool,这只是重复消耗;对创建订单、发送邮件、发布内容或更新 CRM,这会直接制造重复副作用。
正确的提交顺序必须拆开:用户消息先提交,Tool Call 在执行前提交为 planned,执行开始后改为 running,Tool Result 返回后立即提交为 completed。最终助手消息属于后续步骤,它可以失败、被取消或超时,但不能反向抹掉已经完成的 Tool。换句话说,Tool Result 与 final response 不应共用一个“全部成功才提交”的大事务。
第二种失败,是看到 Detached 模式在本地完成,就把一个脱离请求的 Promise 当成后台任务系统。本次实验中,客户端断开后,进程仍然存活,所以任务能够继续写入最终结果。但如果此时发生应用重新部署、Node 进程退出、Serverless 实例回收或容器被重启,这个 Promise 没有任何外部所有者,也没有 Worker 能够重新领取任务。测试证明的是“连接生命周期可以解耦”,不是“任务已经耐久”。生产环境仍需保存 job_id、当前 Step、租约、重试次数和最后心跳,并由 Queue、Worker 或 WorkflowAgent 负责重新调度。
第三种失败,是恢复接口虽然读取了旧状态,却仍然先执行 Tool、再检查结果。幂等检查必须发生在副作用之前,而不是异常之后。最小顺序应是:根据 conversation_id + run_id + tool_call_id 生成稳定键,查询是否已有 succeeded 记录;有则直接复用,没有才尝试抢占执行锁。若记录处于 running 且租约尚未过期,恢复请求应等待或返回处理中;若状态是 unknown,应先向外部系统对账,而不是默认再次执行。
第四种失败,是把 heartbeat 当作所有长任务的解决方案。本地代理测试中,持续发送 heartbeat 确实让连接越过了 70ms 的读取预算,但这只说明代理看到数据,并不代表业务有进展。无意义地长期发送空 Chunk 会占用连接、隐藏卡死的 Tool,还可能让前端误以为任务正常。更稳的做法是让 heartbeat 同时携带可审计的 run_id、step_id、状态和更新时间,并设置总时限;超过总 SLA 后转入后台任务或失败恢复,而不是无限保持 HTTP 连接。
这四类失败共同说明,流式 Agent 的可靠性不能由一个 consumeStream()、一个 AbortSignal 或一个 Timeout 参数负责。请求层负责连接,运行层负责状态,Tool 层负责副作用,代理层负责传输预算,持久化层负责恢复证据。只有把这些状态分别记录,故障发生后才能判断应该继续、复用、重试、对账还是人工介入。
如果任务可能超过 HTTP 代理预算,最稳的结构不是盲目增加一个 Timeout 数字,而是:
POST 创建任务 → 立即返回 job_id
↓
Queue / WorkflowAgent
↓
按 Step 保存状态与 Tool Result
↓
前端轮询或订阅任务状态
这次补测的完整事件记录在实验仓库的:
benchmarks/http-interruption.json
它保留了断线前后 Run 状态、Tool 执行次数、幂等复用事件和代理模拟边界,便于直接复核。
九、Timeout:v7 改善了配置,但不会替你完成取消设计
v6 Fixture 使用:
AbortController + setTimeout
20ms 后由应用主动 Abort。
v7 Fixture 使用:
timeout: {
totalMs: 20,
}
Mock Provider 正确监听 AbortSignal 后,实验捕获到:
TimeoutError
v7 进一步允许区分:
timeout: {
totalMs: 30_000,
stepMs: 15_000,
chunkMs: 5_000,
}
Vercel 的 AI SDK 7 发布说明还列出了默认 Tool 和单 Tool 的超时预算。本文固定在 [email protected] 的可复现实验只把 totalMs 纳入运行时断言,并依据公开 API 参考说明 stepMs 与 chunkMs;没有把尚未进入本实验断言的 Tool 超时写成“已实测结论”。生产项目采用 toolMs 或单 Tool 超时前,应先核对当前锁定版本的类型定义并增加对应 Fixture。
这三个已核验的时间预算解决的是不同问题。
totalMs
整次 Run 的总 SLA。
适合防止多步 Agent 不断继续调用 Tool,最终占用请求太久。
stepMs
单个 Step 的最大时间。
适合识别某次 Provider 调用或 Tool 往返异常缓慢,而不是等到总时间耗尽。
chunkMs
流式输出中两次 Chunk 之间允许的最大静默时间。
适合处理连接仍存在但 Provider 已经停止产出数据的情况。
但配置 Timeout 仍然不等于生产可靠性完成。
必须确认:
- Provider Adapter 是否监听 AbortSignal;
- HTTP 请求是否真的取消;
- Tool 是否接收 Signal;
- 长查询是否可中断;
- Run 是否标记 timed_out;
- 已完成 Tool Result 是否保留;
- 前端是否区分用户取消和系统超时。
一个 Mock 如果只是:
await sleep(1000);
却不监听 Signal,那么 timeout 可能只影响外层等待,无法模拟真实取消。我们的 v7 Fixture 专门保留一个活跃长任务 Timer,并在 Abort 时清理 Timer、拒绝 Promise,用来验证 Signal 的真实传播。
十、Retry:Tool 第一次失败、第二次成功,应该保存几条记录
Retry Fixture 模拟:
JOB-7
↓
attempt 1: transient failure
↓
attempt 2: success
↓
final response
最终状态是:
runStatus = succeeded
toolStatus = succeeded_after_retry
step 1 = completed
step 2 = completed
Attempt 记录为:
attempt 1 running
attempt 1 failed
attempt 2 running
attempt 2 succeeded
这里最重要的不是“第二次成功”,而是状态层级。
Run 状态
表示用户这一轮任务最终是否完成。
Step 状态
表示某次模型/工具编排步骤是否完成。
Tool Call 状态
表示模型要求的逻辑操作是否完成。
Tool Attempt 状态
表示工具实际执行了几次,每次为什么失败或成功。
如果只保存最终:
tool = succeeded
运维系统看不到它曾经失败,无法统计重试率,也无法发现某个外部依赖已经不稳定。
如果只保存:
tool = failed
tool = succeeded
又会误以为模型调用了两个 Tool。
因此,推荐结构是:
tool_call 1
├── tool_attempt 1 failed
└── tool_attempt 2 succeeded
└── tool_result 1
哪些 Tool 可以自动 Retry
适合自动 Retry:
- 幂等查询;
- 带幂等键的写操作;
- 明确返回临时错误码;
- 可安全重复的缓存刷新。
不应默认自动 Retry:
- 支付;
- 发邮件;
- 创建订单;
- 发布内容;
- 删除资源;
- 任何无法判断第一次是否成功的写操作。
对后者,更稳的策略不是立即重试,而是进入:
unknown → reconciliation
先向外部系统查询操作是否已经完成,再决定是否补偿或重试。
AI SDK 7 可以让 Run 和 Timeout 更清楚,但它不会替应用知道某个 Tool 的业务幂等性。
十一、生产建议:迁移前先补四层防线
第一层:测试防线
至少要有:
- Tool Call 成功;
- Tool 抛异常;
- Provider Timeout;
- Page Close Abort;
- Network Disconnect Abort;
- Manual Cancel;
- 真实 HTTP/SSE Client Disconnect;
- Request-bound 与 Detached Execution 对照;
- 恢复后 Tool 幂等复用;
- Tool Retry;
- Proxy Read Timeout / Heartbeat 对照;
- Persistence Snapshot;
- Result Shape Snapshot。
不要只测试最终文本。最终文本相同,结构仍然可能变化。
本次实验就是典型例子:
finalText: same
tool aggregation: different
response message count: different
第二层:持久化防线
在 v6 阶段先建立版本无关模型,再升级 v7。
不要一边切 SDK,一边改数据库主结构。两个变量同时变化,出问题时很难回滚。
第三层:副作用防线
所有会写外部系统的 Tool 都需要:
- 幂等键;
- Attempt 记录;
- 明确可重试错误;
- 超时后的对账流程;
- 人工审批边界。
第四层:灰度防线
推荐迁移顺序:
内部无副作用查询
↓
低风险 Tool
↓
少量真实用户
↓
观察 Abort / Retry / Timeout
↓
扩大流量
↓
高风险 Tool
保留 v6 Adapter 一段时间:
const adapter = featureFlag.aiSdk7
? createV7Adapter()
: createV6Adapter();
回滚不是重新安装 npm 包,而是切换 Adapter 和流量开关。
十二、是否升级:我的判断
建议升级的项目
如果项目已经具备:
- TypeScript 严格检查;
- 自动化 Fixture;
- 版本无关 Persistence;
- AbortSignal 贯穿调用链;
- Tool 幂等;
- 灰度发布;
- Run/Step/Tool 可观测性;
那么 AI SDK 7 的语义更适合继续扩展生产 Agent。
特别是下面这些项目:
- 多步 Tool Calling;
- 长时间流式任务;
- 需要总超时和 Step 超时;
- 需要审计整次 Run;
- 需要恢复执行;
- 需要区分最终 Step 和完整历史。
不应马上升级的项目
如果项目当前:
- 没有自动化测试;
- 直接保存
response.messages; - Tool 没有幂等键;
- 用户断开后后端仍然无状态运行;
- 只靠
maxRetries处理所有失败; - 没有灰度开关;
那么最优先的工作不是升级,而是补运行时边界。
否则 v7 不会自动让系统更可靠,只会把原来隐藏的问题更快暴露出来。
最终结论
我会升级,但不会采用“一次性替换依赖”的方式。
XBSTACK 的迁移顺序是:
隔离 Fixture
↓
结构对比
↓
Persistence Adapter
↓
Abort / Retry / Timeout 测试
↓
低风险工作流灰度
↓
生产观测
↓
扩大范围
AI SDK 7 最值得升级的地方,不是它让代码少写几行,而是它给了我们一个机会:把长期被 SDK 返回对象掩盖的运行时边界重新设计清楚。
真正的迁移完成标准也不是:
npm install 成功
TypeScript 通过
页面能输出文字
而是:
同一 Tool 不重复执行
Abort 后状态可以解释
Retry 过程可以审计
Timeout 能真正取消
历史数据不依赖 SDK 版本
需要时可以回滚
当这些条件都成立,AI SDK 7 才从一个新版本,变成可以进入生产系统的基础设施。
实验仓库与后续更新
完整 Fixture、Migration Diff、原始 JSON 结果和架构文档:
- XBSTACK AI SDK 7 Migration Demo
- Vercel:AI SDK 7 官方发布与迁移说明
- AI SDK:Tools、Tool Calling 与 response.messages
- AI SDK:Timeout 与 AbortSignal 配置
继续处理生产 Agent 的状态、恢复和框架边界,可以参考:
- AI Agent 生产治理:权限、审计、成本与回滚
- LangGraph 失败恢复:Tool Error、Timeout 与重试策略
- AI Agent 框架选型:AI SDK 7、LangGraph、ADK 与 Microsoft Agent Framework
后续与 AI SDK、MCP、LangGraph 和 Agent Reliability 相关的真实实验,会整理进:
XBSTACK AI Engineering Weekly
它不会做一周新闻摘要,而会固定记录:
- What changed
- What broke
- What I tested
- Tools worth trying
- XBSTACK updates
技术站点负责沉淀完整结论,GitHub 提供可运行证据,Newsletter 负责把值得保存的变化送到开发者手里。三者不是三份内容,而是一套从实验到分发的技术资产闭环。
把迁移结论继续追到可复现实验
AI Tools Lab 会统一承接 Migration Diff、Tool Call、Persistence、Abort、Retry、Timeout 和 Failure 实验,避免只看版本发布说明。
下一步阅读
返回专题入口 →ChatGPT Work、Chat、Codex 有什么区别?我用一个真实网站任务跑了一遍
ChatGPT Work、Chat 和 Codex 到底怎么分工?我用 XBSTACK 三天未更新后的真实运营任务跑完整条工作流,并复盘第一版只有1499字、构建通过却仍然不合格的原因。
AI Agent Memory Retrieval 实战:混合检索、重排、时效性与冲突消解
系统拆解 AI Agent 记忆召回链路,覆盖身份与作用域过滤、结构化查询、向量召回、混合检索、多信号重排、时效性、冲突消解、Prompt 预算、可观测性与回归测试。
CRM Automation AI Agent 选型指南 2026:线索评分、销售跟进与数据治理
CRM automation AI agent 怎么选?本文按 Lead Scoring、Sales Follow-up、Email Routing、Meeting Summary、Customer Success 和 CRM Data Hygiene 拆解能力边界,并给出 SaaS、自建 Agent、权限控制和上线顺序。
AutoGen 实战教程:多智能体对话协作、工具调用与生产化边界
系统拆解 AutoGen 在多智能体对话协作中的实战用法与生产化边界,覆盖 AgentChat、GroupChat、Planner / Executor / Critic 模式、工具调用、Human-in-the-loop、对话轮次控制、评估指标、成本监控和 Microsoft Agent Framework 迁移风险。

小白
Full-Stack AI Engineer
小白,全栈 AI 工程师,持续构建生产级 Agent 系统、产品工具与独立软件资产。
了解小白与 XBSTACK →