主题
Vercel AI SDK 上手:前端接大模型的标准姿势
这篇解决一个很具体的问题:你已经在项目里调大模型了,但代码是"每个页面自己拉一根 fetch 解 SSE(Server-Sent Events,服务器推送事件)",对话历史、加载状态、工具调用各写一套,换模型时又要重新适配字段名。Vercel AI SDK(本文核对于 2026-09-27,当时 npm 上 ai 最新为 7.0.116,@ai-sdk/react 为 4.0.119、@ai-sdk/vue 为 4.0.116,要求 Node.js 22+)就是把这堆重复劳动收成一个 TypeScript 库。这个包迭代很快,具体 API 请以官方文档为准。
裸写 fetch,你会重复造哪些轮子
第一是编码。fetch 的 response.body 给你的是 Uint8Array(字节数组)流,一个中文占三字节,chunk 边界正好把某个字劈成两半是常态——必须自己用 TextDecoder 并传 { stream: true } 兜住半个字符,否则页面蹦乱码。
第二是分帧。SSE 的格式是 data: 前缀加空行结尾,最后还有一条 [DONE];一条 JSON 也可能横跨两个 chunk。你得自己存 buffer、按换行切、跳过注释行。
第三是多模型适配。OpenAI 风格、Anthropic 风格、Gemini 风格的系统消息位置、增量字段、工具调用结构各不相同,一个"支持多家"的产品等于维护若干套解析分支。
第四是工具调用回环:模型返回工具意图,你要执行、把结果回填成一条消息、再发一次请求,直到它不再要工具。循环写错就是死循环或者上下文丢失。
第五是 UI 状态:submitted/streaming/ready/error 切得对不对、停止按钮怎么中断、网络断了怎么续、多轮历史的 key 怎么给。
AI SDK 抽象掉了哪几层
核心层是 ai 包:generateText(一次性生成)和 streamText(流式生成)是模型无关的调用面,返回统一的事件流,文本增量、工具调用、结束原因都是固定类型,前端不用再认得厂商字段。
UI 层是两个独立包:React 用 @ai-sdk/react 的 useChat,Vue 用 @ai-sdk/vue 的同名 composable(组合式 API 函数)。它把 SSE 解析、消息列表、状态机收进一个 hook,并且 AI SDK 5 之后 Vue 与 React 已基本对齐功能。
协议层从 5.0 起换成标准 SSE,并明确区分两类消息:UIMessage 是客户端的真相(含 parts、工具结果),ModelMessage 是发给模型的精简格式,中间用 convertToModelMessages 转换,持久化存前者。
工具层用 inputSchema(5.0 起由 parameters 更名,配合 zod 定义)加 execute,多步循环由 SDK 自动跑。
服务端和客户端怎么分工
服务端持密钥、拼提示词、调 streamText,再把流转成 UI 消息流返回;客户端只负责发消息、渲染 parts、显示状态。API Key 绝不能进浏览器。
ts
// app/api/chat/route.ts —— 服务端
import { streamText, UIMessage, convertToModelMessages } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai('gpt-4o'), // 也可直接写模型字符串,走网关
instructions: '你是前端助手,回答尽量给可运行的代码。',
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}tsx
// 客户端:useChat 接住流
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function Chat() {
const { messages, sendMessage, status } = useChat();
const [input, setInput] = useState('');
return (
<>
{messages.map(m => (
<div key={m.id}>
{m.parts.map((part, i) =>
part.type === 'text' ? <span key={i}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={e => {
e.preventDefault();
sendMessage({ text: input }); // 注意:输入状态归你自己管
setInput('');
}}
>
<input value={input} onChange={e => setInput(e.target.value)} disabled={status !== 'ready'} />
</form>
</>
);
}Vue 版本换汤不换药:import { useChat } from '@ai-sdk/vue',返回同样结构,注意 Vue 里别解构丢响应式。
换模型只是换一行
model 既能是 provider 包实例,也能是一个模型字符串(如 openai/gpt-4o,通过 AI Gateway 统一入口),切换时通常只动这一行,加装对应 @ai-sdk/* 包即可。要留意 @ai-sdk/* 的版本必须跟 ai 的主版本配套,混着升会直接类型报错。
代价:多背一份版本债
第一,多几层依赖,客户端 bundle 也跟着涨;第二,抽象是黑盒,流被截断时你得掀开协议层才知道问题在 SSE 还是在转换;第三,版本漂移真实存在——5.0 重写了 useChat(不再托管输入状态),6.0 起 generateObject/streamObject 标记弃用改用 output 参数,升级前最好先读迁移指南并留出改造时间。
常见坑 / 什么时候不该套 SDK
- 密钥别放客户端,
useChat只发消息,模型调用必须在服务端路由里。 - 输入框状态 5.0 以后由你自己维护,别照抄旧教程里的
input/handleInputChange。 - 工具定义字段是
inputSchema,抄老代码用parameters会没反应。 - 渲染优先用
message.parts,不要读已不推荐的content。 - 只调一家模型、只做一次非流式请求,或者要用某个厂商独有的新参数(如特定缓存、思考控制),直接用它官方 SDK 或裸 fetch 弯路更少,别硬套。