把 batch-2 的 chatbot 升级到 Vercel AI SDK 6
一句话定义:用
npx @ai-sdk/codemod upgrade v6自动跑 4.x → 6.x 迁移,验证streamText/useChat没坏,然后用 v6 新增的ToolLoopAgent重写并加 tool approval 让用户审批敏感工具调用。预计耗时:30 分钟(自动迁移 + 验证) | 难度:进阶
适用场景
- 已经用 Vercel AI SDK 4.x / 5.x 写过 chatbot,想升 6.x 拿 agent 抽象、tool approval、多模态
- 看到 v6 release notes,想知道值不值得升、坑在哪
- 跟着
add-vercel-ai-sdk-streaming-chatbot路径 C 跑过 demo,现在想升级到 v6 - 后端跑在 CloudBase 云托管 / Web 云函数,前端是 Next.js / Vue / React
不适用:
- 从未用过 Vercel AI SDK 的项目,直接看 v6 官方文档新建即可,不必走升级路径
- 用 OpenAI Node SDK 直连、根本没接 Vercel AI SDK 的项目,本篇不相关
- 微信小程序栈,参考 batch-2 篇 08 的路径 A(
cloudbase-agent-ui)
v6 vs v5 / v4 关键差异
| 维度 | v4.x | v5.x | v6.x |
|---|---|---|---|
| 流式协议 | data stream v1 | data stream v2(破坏性) | 兼容 v5 协议 |
| 前端 hook | ai/react | @ai-sdk/react | @ai-sdk/react |
| Agent 抽象 | 无 | 实验性 | ToolLoopAgent(stable) |
| Tool approval | 无 | 无 | needsApproval + UI 协议 |
| MCP 集成 | 无 | experimental | @ai-sdk/mcp stable |
| 调试工具 | 无 | 无 | @ai-sdk/devtools |
| 升级路径 | — | 手工迁移多 | npx @ai-sdk/codemod upgrade v6 |
v6 官方表态是"不期望大破坏性变化"——大部分 4.x → 6.x 的工作 codemod 能扫掉,比当年 v4 → v5 那次温和。但 codemod 不是万能的,下面会标出几个 codemod 改不到的地方。
环境要求
| 依赖 | 版本(撰写时 npm latest) |
|---|---|
| Node.js | ≥ 20(v6 已停止支持 Node 18) |
ai | ^6.0(撰写时 6.0.x) |
@ai-sdk/codemod | ^3.0(撰写时 3.0.x) |
@ai-sdk/openai / @ai-sdk/anthropic | ^3.0 |
@ai-sdk/react | ^3.0 |
@ai-sdk/devtools(可选) | ^0.0(公测期) |
@cloudbase/cli(tcb) | latest |
具体 patch 版本请以 npm view ai version 拉到的为准,下面命令里写 ^6.0 / latest 即可,不要锁死单个 patch。
升级前置:把 batch-2 篇 08 的项目跑起来
本篇假设你手上有一个跟着 add-vercel-ai-sdk-streaming-chatbot 路径 C 跑通的项目结构:
chatbot/
├── app/
│ ├── api/
│ │ └── chat/
│ │ └── route.ts # streamText + toDataStreamResponse
│ └── chat/
│ └── page.tsx # useChat hook
├── package.json # ai@^4 + @ai-sdk/openai@^1
└── .env.local
升级前先跑一遍 npm run dev 确认能聊天、能看到流式输出,再开始改。不要在挂着的项目上直接升——升完出问题分不清是迁移没干净还是原来就坏了。
第一步:运行 codemod 自动迁移
# 切到项目根目录
cd chatbot
# 备份当前分支
git checkout -b upgrade-ai-sdk-v6
# 跑官方 codemod,自动改 import 路径、API 调用形态、类型签名
npx @ai-sdk/codemod upgrade v6
codemod 会扫整个 src/ / app/ / pages/(含 .ts .tsx .js .jsx),主要做这几件事:
- import 路径:
from 'ai'里被废弃的导出改名或迁出(比如老的experimental_*前缀去掉) streamText/generateText选项重命名:v5 时改过一次的messages/prompt字段在 v6 又收紧了toDataStreamResponse→toUIMessageStreamResponse:v6 把流式响应方法名做了统一- provider 包装:
createOpenAI({ baseURL })这类工厂函数签名几乎不动,但内部协议层 codemod 会改
跑完看一眼 git diff:
git diff --stat
正常的 diff 应该只有几个文件、几十行变动。如果一上来就是几百行——大概率你之前混用了实验性 API,需要先回到 stable 表面再升。
注意:codemod 不会装新依赖,也不会改 package.json 的版本号。下一步手动升包。
第二步:升级依赖 + 跑通现有 chatbot
# 升级核心包到 v6
npm install ai@^6.0
# 升级 React 前端 hook
npm install @ai-sdk/react@^3.0
# 升级 provider(OpenAI / Anthropic 都可,按你篇 08 用的来)
npm install @ai-sdk/openai@latest
# 可选:装调试工具
npm install -D @ai-sdk/devtools
装完跑:
npm run dev
打开 http://localhost:3000/chat,发一句话,确认还能流式输出。这一步主要验:
streamText({ model, messages, system })还能正常返回 streamuseChat({ api: '/api/chat' })hook 还能渲染消息- 错误处理(
error字段)、停止(stop函数)行为没变
如果跑不起来,常见三种坏法:
| 症状 | 原因 | 修法 |
|---|---|---|
Cannot find module 'ai/react' | v6 把 React 入口拆到独立包了 | import { useChat } from '@ai-sdk/react' |
route handler 报 toDataStreamResponse is not a function | codemod 漏改 | 手动改成 result.toUIMessageStreamResponse() |
| 前端收不到 chunk,network 看到 200 但 body 空 | data stream 协议版本不一致 | 后端 result.toUIMessageStreamResponse() + 前端 @ai-sdk/react 都升到 v6 才一致 |
到这一步,你已经把 4.x / 5.x 的 chatbot 升到 v6 了。如果业务上够用,下面三步是可选优化,按需做。
第三步:用 ToolLoopAgent 重写(更简洁)
v6 新增的 ToolLoopAgent 把"声明 tools、跑 LLM 循环、累计消息"这一坨封装成一个对象。原来在 route handler 里手写 streamText + tools 数组的代码,可以缩成几行。
app/api/chat/route.ts:
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';
import { createOpenAI } from '@ai-sdk/openai';
export const runtime = 'nodejs';
export const maxDuration = 60;
const llm = createOpenAI({
baseURL: process.env.LLM_PROXY_URL,
apiKey: process.env.LLM_PROXY_TOKEN,
});
const weatherTool = tool({
description: '查询某个城市的天气',
inputSchema: z.object({ city: z.string().describe('城市名,例如:北京、上海') }),
execute: async ({ city }) => {
// 真 实场景调云函数 / 第三方 API,这里先 mock
return { city, temperature: 22, condition: 'sunny' };
},
});
const chatAgent = new ToolLoopAgent({
model: llm('gpt-4o-mini'),
instructions: '你是一个有帮助的助手,用简体中文回答;涉及天气问题时调用 weather 工具。',
tools: { weather: weatherTool },
});
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await chatAgent.stream({ messages });
return result.toUIMessageStreamResponse();
}
差异:
- 原来
streamText({ model, system, messages, tools })的散装写法,现在收成chatAgent.stream({ messages }) instructions取代system(语义一致,命名更接近 OpenAI Assistants / Anthropic 的 system prompt 概念)- 多轮 tool calling 循环 SDK 内部跑完,不用自己 while-loop
前端 useChat 完全不用改,因为对外协议没变。
ToolLoopAgent 还支持 prepareCall 在每次调用时动态注入配置(比如根据当前用户的账户类型改 instructions),这部分是高阶能力,本篇不展开,参考官方迁移指南。
第四步:启用 tool approval(敏感工具二次确认)
v6 的 tool() 工厂新增了 needsApproval 字段。返回 true 时 SDK 会暂停 tool 执行,把"等待审批"事件流给前端,前端弹 UI 让用户点"批准 / 拒绝",然后再回写。
适合有副作用的 tool:写数据库、发邮件、调付款、删文件——这类操作不能让 LLM 自己拍板。
后端定义带审批的 tool:
const sendEmailTool = tool({
description: '给指定邮箱发邮件',
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
// 全部邮件都要审批;也可以写成 async ({ to }) => to.endsWith('@external.com') 只审外部邮件
needsApproval: async () => true,
execute: async ({ to, subject, body }) => {
// 真实落地调用 connect-resend-email-cloud-function 那一套
return { sent: true, to };
},
});
前端要新增审批 UI。@ai-sdk/react 的 useChat 会在 message 里塞出 state: 'approval-requested' 的 tool invocation:
'use client';
import { useChat } from '@ai-sdk/react';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, addToolApprovalResponse } = useChat({
api: '/api/chat',
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role === 'user' ? '你' : '助手'}:</strong>
{m.parts?.map((part, i) => {
if (part.type === 'text') return <span key={i}>{part.text}</span>;
if (part.type === 'tool-invocation' && part.toolInvocation.state === 'approval-requested') {
const { toolName, args, approval } = part.toolInvocation;
return (
<div key={i} style={{ border: '1px solid #ddd', padding: 12, marginTop: 8 }}>
<div>助手要调用工具 <code>{toolName}</code>,参数:</div>
<pre>{JSON.stringify(args, null, 2)}</pre>
<button onClick={() => addToolApprovalResponse({ id: approval.id, approved: true })}>
批准
</button>
<button onClick={() => addToolApprovalResponse({ id: approval.id, approved: false })}>
拒绝
</button>
</div>
);
}
return null;
})}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">发送</button>
</form>
</div>
);
}
字段名和具体协议请以 v6 当前文档为准——tool approval 是 v6 新加的,前几个 patch 版本里有微调过。
第五步(可选):用 Output.object 拿结构化输出
v6 的 Output.object 让 agent 在跑 tool calling 的同时强制最终输出符合一个 zod schema。原来要把 structured output 和 tool calling 拆两次调用,现在一次跑完。
import { ToolLoopAgent, Output } from 'ai';
import { z } from 'zod';
const reportAgent = new ToolLoopAgent({
model: llm('gpt-4o-mini'),
instructions: '查询天气然后输出结构化日报',
tools: { weather: weatherTool },
output: Output.object({
schema: z.object({
city: z.string(),
temperature: z.number(),
summary: z.string(),
suggestion: z.string(),
}),
}),
});
const result = await reportAgent.generate({ prompt: '帮我看看上海今天的天气' });
console.log(result.output);
// { city: '上海', temperature: 18, summary: '多云转晴', suggestion: '适合出门' }
这个能力在做"agent 跑完产物落库"的场景特别有用——直接拿 typed object 写数据库,不用再让前端解析自然语言。
部署到 CloudBase
整体路径和 deploy-nextjs-to-cloudbase-run 一致:
tcb cloudrun deploy --port 3000
需要在云托管「服务设置 → 环境变量」里加:
LLM_PROXY_URL(必须以/v1结尾,@ai-sdk/openai内部会拼/chat/completions)LLM_PROXY_TOKEN
Web 云函数版的部署一致:
tcb fn deploy chat-backend --httpFn -e your-env-id
控制台「云函数」补环境变量。如果之前篇 08 已经部过了,本次 redeploy 就行——v6 在 runtime 层面对 Node 20 有要求,云托管 Dockerfile 里如果还在 FROM node:18 改成 FROM node:20。
运行验证
升级流程跑通的判定:
| 验证项 | 命令 / 操作 | 期望结果 |
|---|---|---|
| 依赖装对了 | npm ls ai @ai-sdk/react @ai-sdk/openai | 三个包主版本分别是 6 / 3 / 3 |
| codemod 干净 | git diff --stat | 只动 import 路径 + 几个 API 调用 |
| 老 chatbot 还能聊 | npm run dev → /chat 输入消息 | 流式输出正常 |
| ToolLoopAgent 能调 tool | 问"北京天气怎么样" | 返回里出现 weather tool 的调用结果 |
| Tool approval 弹 UI | 问"给 a@b.com 发邮件" | 前端出现批准/拒绝按钮 |
| 类型检查通过 | npx tsc --noEmit | 无 error |
最后一步必须跑——v6 类型签名比 v5 严,TypeScript 用户经常漏掉 codemod 改不到的地方。