第一次做 AI 应用,最容易卡住的地方通常不是模型本身,而是一堆新名词同时扑过来:Provider、stream、tool calling、message parts、Route Handler、model messages、UI messages。像刚走进后厨,灶台、锅、出餐口全在响,你只是想先煮一碗能吃的面。
Vercel AI SDK 解决的正是这个问题。它把「怎么调模型」和「怎么把模型输出接到前端」这两件事拆开,又用一套比较统一的接口把它们接回去。你不用一开始就手写 SSE,也不用自己维护一堆模型供应商的适配逻辑。先让一个聊天页跑起来,再加工具调用、结构化输出、鉴权和监控,会稳很多。
Vercel AI SDK 是一套 TypeScript AI 应用工具箱。ai 包负责模型调用和流式输出,@ai-sdk/react 负责前端聊天状态,Provider 包或 Vercel AI Gateway 负责连接具体模型服务。
可以先把它想成三层,后面的代码基本都在这三层之间传递消息:
- AI SDK Core:模型层。
streamText、generateText、tool这些 API 都在这里,负责把 prompt 或 messages 送进模型,再拿回文本、对象或流。 - AI SDK UI:界面层。
useChat负责输入框、消息列表、发送状态和流式更新,少写很多重复的 React 状态代码。 - Provider / Gateway:连接层。
@ai-sdk/openai、@ai-sdk/anthropic或 Vercel AI Gateway 把统一调用翻译成具体模型服务能听懂的请求。
你会做出什么
这篇不追求一口气讲完整个 AI SDK。我们只做一条最小但完整的链路:用户在页面输入一句话,后端调用模型,前端把回复一点点渲染出来。
能跑通这条链路,比先看十页概念更重要。因为一旦你知道消息怎么从浏览器走到模型、又怎么回来,后面加工具、加 RAG、加日志,都只是沿着这条路继续铺。
项目结构大概是这样:
my-ai-app/ ├─ app/ │ ├─ page.tsx │ └─ api/chat/route.ts ├─ .env.local └─ package.json
| 文件 | 作用 |
|---|---|
app/page.tsx | 前端聊天界面,使用 useChat 管输入、发送和消息渲染 |
app/api/chat/route.ts | 后端 Route Handler,使用 streamText 调模型 |
.env.local | 放 API Key,不能提交到 Git |
package.json | 安装 ai、@ai-sdk/react、Provider 包等依赖 |
我重新查了一下 npm:现在 ai 是 7.0.18,@ai-sdk/react 是 4.0.19,@ai-sdk/openai 是 4.0.9。AI SDK 更新很快,复制代码前最好顺手看一眼官方文档。本文按 AI SDK 7 的写法整理。
第一步:创建 Next.js 项目
先准备 Node.js 22+ 和 pnpm。官方 quickstart 也建议用 pnpm,这里就不折腾别的包管理器了。
pnpm create next-app@latest my-ai-app cd my-ai-app
创建项目时,建议这样选:
- App Router:选 yes。本文用的就是
app/api/chat/route.ts。 - TypeScript:选 yes。AI SDK 的类型提示能帮你少踩不少坑。
- Tailwind CSS:选 yes。不是必须,但写一个舒服点的聊天页会快很多。
- src 目录:看个人习惯。选不选都不影响 AI SDK。
然后安装 AI SDK 相关依赖:
pnpm add ai @ai-sdk/react zod
如果你要直接调用 OpenAI,再装 Provider 包:
pnpm add @ai-sdk/openai
这里有两条常见路线:
| 路线 | 安装 | 环境变量 | 适合谁 |
|---|---|---|---|
| Vercel AI Gateway | ai @ai-sdk/react zod | AI_GATEWAY_API_KEY | 想用一个入口管理多个模型 |
| 直接 Provider | 再加 @ai-sdk/openai 等 | OPENAI_API_KEY 等 | 已经确定只接某家模型服务 |
新手我建议先选一条路。先跑通,再抽象。很多奇怪 bug 都来自「我两个方案都配了一半」。
第二步:写环境变量
在项目根目录创建 .env.local:
cp /dev/null .env.local
如果走 Vercel AI Gateway:
AI_GATEWAY_API_KEY=你的_key
如果直接走 OpenAI Provider:
OPENAI_API_KEY=你的_key
.env.local 不要提交到 Git。前端组件也不要直接读取模型 Key。浏览器里出现的东西,用户都能看到;把 Key 放进去,基本等于把门禁卡贴在楼下。
第三步:写后端 Route Handler
先看 Gateway 写法。新建 app/api/chat/route.ts:
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
type UIMessage,
} from 'ai'
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json()
const result = streamText({
model: 'xai/grok-build-0.1',
messages: await convertToModelMessages(messages),
})
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
})
}
这段代码不长,但里面有几个动作很关键:
- 1. 接收 UI messages
前端发来的
messages不只是纯文本。它们还带着角色、分段内容和 UI 侧需要的元数据,类型是UIMessage[]。- 2. 转成 model messages
模型不需要前端元数据,所以用
convertToModelMessages转成模型能处理的消息格式。- 3. 调用 streamText
streamText会发起模型调用,并返回一个可以继续往外传的文本流。聊天场景优先用它,因为用户能更早看到内容开始出现。- 4. 返回 UIMessage Stream
createUIMessageStreamResponse把模型输出包装成useChat能读取的响应格式。前端不用关心底层流协议,只管更新消息。
如果你走 OpenAI Provider,Route Handler 可以改成这样:
import { openai } from '@ai-sdk/openai'
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
type UIMessage,
} from 'ai'
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json()
const result = streamText({
model: openai('gpt-5.1'),
messages: await convertToModelMessages(messages),
})
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
})
}
streamText 更偏模型层,useChat 需要的是 UI 消息流。中间这一层转换有点像后厨把菜做好,再按外卖盒的格式装好:菜是同一道菜,但前端需要按它认识的格式拆开。
实际项目里,模型名最好抽到环境变量里。教程为了少一点干扰,先写死在代码里;等跑通后再改成 MODEL_ID 会更稳。
第四步:写前端聊天页
把 app/page.tsx 改成下面这样:
'use client'
import { useChat } from '@ai-sdk/react'
import { useState } from 'react'
export default function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage } = useChat()
return (
<main className="mx-auto flex min-h-screen w-full max-w-2xl flex-col gap-4 px-4 py-10">
<h1 className="text-3xl font-semibold tracking-tight">
My AI Chat
</h1>
<div className="flex-1 space-y-4 rounded-lg border p-4">
{messages.map(message => (
<div key={message.id} className="whitespace-pre-wrap">
<div className="mb-1 text-sm text-zinc-500">
{message.role === 'user' ? 'User' : 'AI'}
</div>
{message.parts.map((part, index) => {
if (part.type === 'text') {
return (
<div key={`${message.id}-${index}`}>
{part.text}
</div>
)
}
return null
})}
</div>
))}
</div>
<form
className="flex gap-2"
onSubmit={(event) => {
event.preventDefault()
if (!input.trim())
return
sendMessage({ text: input })
setInput('')
}}
>
<input
className="flex-1 rounded-md border px-3 py-2 outline-none focus:ring-2 focus:ring-black/20"
value={input}
placeholder="问点什么,比如:用一句话解释 AI SDK"
onChange={event => setInput(event.currentTarget.value)}
/>
<button className="rounded-md bg-black px-4 py-2 text-white">
Send
</button>
</form>
</main>
)
}
这里最值得注意的是两处:
messages:当前聊天记录。AI SDK 7 的消息内容通过message.parts渲染,不要再假设每条消息只有一个content字符串。sendMessage:把用户输入发给/api/chat。默认路径就是刚才写的 Route Handler。
message.parts 乍看有点绕,但它是合理的。模型以后不一定只返回纯文本,还可能返回 reasoning、tool call、file、source 等片段。parts 就像一排托盘格子,不同类型的输出按顺序放进去,前端再决定每一格怎么展示。
完整链路可以这样读:
用户输入 ↓ useChat 发送 POST /api/chat ↓ Route Handler 读取 messages ↓ convertToModelMessages 转成模型消息 ↓ streamText 调用模型 ↓ UIMessage Stream 持续返回 ↓ message.parts 逐段渲染到页面
第五步:跑起来
启动开发服务器:
pnpm dev
打开:
http://localhost:3000
如果配置没问题,你输入一句话后,回复会一点点冒出来。这个体验和一次性等完整结果不太一样:总耗时可能差不多,但页面开始动起来以后,人会明显更安心。
如果它没跑起来,先看这几个地方
.env.local里有没有 Key,变量名是不是写对了。- 改完
.env.local后有没有重启pnpm dev。 - Route Handler 里的模型名是否在当前 Provider 或 Gateway 里可用。
- 浏览器 Network 里
/api/chat是否返回 200。 - 终端有没有鉴权、配额、区域网络之类的错误。
第一轮调 AI 应用,很多问题都不神秘:Key 写错、模型名不可用、环境变量没重启。先查这些,别急着怀疑框架。
generateText 和 streamText 怎么选
AI SDK Core 里最常见的是 generateText 和 streamText。名字很直白,但使用场景不一样。
| 函数 | 适合场景 | 返回方式 |
|---|---|---|
generateText | 标题、摘要、分类、短文本改写 | 等模型完整生成后一次性返回 |
streamText | 聊天、长文本、需要实时显示的输出 | 边生成边返回 |
比如你只是要生成一个标题:
import { generateText } from 'ai'
const { text } = await generateText({
model: 'xai/grok-build-0.1',
prompt: '给一篇 Vercel AI SDK 入门教程起一个中文标题',
})
console.log(text)
聊天页更适合 streamText。用户发出消息后,系统马上开始吐字,比盯着空白区域等最终答案舒服很多。流式输出不一定让模型更快,但会让等待更可控。
加一点系统提示词
现在模型还在自由发挥。你可以在 streamText 里加 system,让它更像你的产品,而不是一个没有边界的万能聊天框:
const result = streamText({
model: 'xai/grok-build-0.1',
system: '你是一个简洁、耐心的中文编程助手。回答要短,但关键步骤不能省。',
messages: await convertToModelMessages(messages),
})
system 不要写成一份公司制度。太长的提示词会互相打架,模型也未必抓得住重点。先写三五句,跑一段真实对话,再根据输出慢慢修。
你可以在 system 里写「不要泄露密钥」,但真正的安全要靠代码:Key 不进前端,工具不随便读 .env.local,用户输入不直接拼进危险命令。Prompt 是方向盘,不是保险柜。
下一步可以加什么
跑通聊天只是第一关。AI SDK 后面能接的东西很多,但别一口气把工具、RAG、图片、语音、agent 全倒进去。先让主链路稳定,再一层一层加。
- 第一阶段:纯聊天
先掌握
useChat、streamText、messages和message.parts。这一步看似基础,其实决定了后面所有功能怎么接。- 第二阶段:结构化输出
用 schema 让模型返回固定结构,比如分类、表单字段、JSON 数据。适合做后台任务和半自动流程。
- 第三阶段:工具调用
让模型在需要时调用函数,比如查数据库、读文档、创建工单。这里开始要认真处理权限、日志和失败重试。
- 第四阶段:产品化
补上鉴权、限流、成本统计、错误提示、模型切换和评估。能 demo 不等于能上线,这一步最容易被低估。
新手可以先做几个小练习:
1. 把 system prompt 改成「苏格拉底式老师」,让它少直接给答案,多追问。 2. 给页面加一个「清空对话」按钮。 3. 把模型名抽到环境变量里,比如 MODEL_ID=xai/grok-build-0.1。 4. 在 Route Handler 里捕获错误,给前端返回更友好的提示。
常见坑
| 坑 | 症状 | 处理 |
|---|---|---|
忘了 'use client' | useChat 报错或页面无法交互 | 在 app/page.tsx 顶部加上 |
| Key 写错 | /api/chat 返回 401/403 | 检查 .env.local 和 Provider 文档 |
| 改 env 不重启 | 明明改了 Key,还是报旧错误 | 停掉 pnpm dev 重新启动 |
| 消息渲染空白 | 后端有返回,页面没显示 | 改成遍历 message.parts |
| API 路径不一致 | 前端发不到后端 | 默认用 /api/chat,自定义时配置 useChat transport |
| 模型不支持 | Provider 报 model not found | 换可用模型,或确认 Gateway 权限 |
这张表可以先留着。第一次接 AI SDK,很多调试时间都花在这些小地方;它们不高级,但足够烦人。
总结
Vercel AI SDK 入门的重点不是记住所有 API,而是先把主链路跑通。
浏览器里,useChat 管输入、发送和消息渲染;Next.js Route Handler 里,streamText 调模型;Provider 或 Gateway 把请求送到真正的模型服务;UIMessage Stream 再把结果一点点送回页面。
这条路通了,你再加工具调用、结构化输出、RAG 或 agent 编排,心里会有一张地图。否则一开始就把所有能力堆上去,调试时很容易分不清到底是前端状态、后端格式、模型权限,还是流式协议出了问题。
先做一条能稳定来回的消息链路。它看起来朴素,但这是所有 AI 应用的地基。
评论区
评论加载中...