使用 Vercel AI SDK 构建你的第一个 Generative UI
使用流式组件创建 AI 驱动界面的分步教程。
前置条件
开始之前,请确保你具备以下条件:
- Node.js 18+ 已安装
- 使用 App Router 的 Next.js 14+ 项目
- OpenAI API 密钥(或 Anthropic——SDK 两者都支持)
- 对 React Server Components 有基本了解
如果你不熟悉 RSC,先花 15 分钟看一下 Next.js 的 Server Components 文档。Vercel AI SDK 的 streamUI 函数依赖 RSC,理解这个模型之后代码会清晰很多。
我们要构建什么
我们将构建一个简单的 AI 驱动助手,它能根据用户提示生成交互式 UI。完成本教程后,你将拥有一个可用的 Generative UI 功能,它能:
- 接受用户的文字提示
- 从服务端流式传输 React 组件
- 根据 AI 的决策渲染交互式卡片和图表
示例领域是一个可以显示股票价格和天气数据的金融助手——足够简单易懂,也足够复杂来演示真实模式。
⚠️ AI SDK RSC 和
streamUI被 Vercel 标记为实验性功能。 对于生产项目,Vercel 推荐 AI SDK UI(来自@ai-sdk/react的useChat)。本文展示一个可用的 RSC 流式传输模式,适用于原型、演示和受控环境;对于生产环境,请权衡取舍,并参见《何时不应使用 Vercel AI SDK》。RSC → UI 迁移指南。
第一步:安装依赖
npm install ai@^4 @ai-sdk/openai@^1 zod
固定 v4——这是在 parameters: 格式和 ai/rsc 导入方式下包含 RSC API 的最后一个系列。对于 v5+,请参见文末的差异说明。
ai 包是 Vercel AI SDK 核心。@ai-sdk/openai 是 OpenAI 提供商(如果偏好 Claude,换成 @ai-sdk/anthropic)。zod 处理工具参数校验——这是你定义 AI 可以向每个组件传递哪些参数的方式。
将 API 密钥添加到 .env.local:
OPENAI_API_KEY=sk-...
第二步:创建组件库
定义 AI 可以生成的组件。这些都是普通的 React 组件——没有任何 AI 特定的内容。关键设计原则:构建可以独立使用的组件,这样它们就能被 AI 组合调用。
// components/weather-card.tsx
interface WeatherCardProps {
city: string;
temperature: number;
conditions: string;
humidity: number;
}
export function WeatherCard({ city, temperature, conditions, humidity }: WeatherCardProps) {
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<h3 className="text-lg font-semibold">{city}</h3>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-4xl font-bold">{temperature}°C</span>
<span className="text-muted-foreground">{conditions}</span>
</div>
<p className="mt-2 text-sm text-muted-foreground">
湿度:{humidity}%
</p>
</div>
);
}
// components/stock-ticker.tsx
interface StockTickerProps {
symbol: string;
price: number;
change: number;
changePercent: number;
}
export function StockTicker({ symbol, price, change, changePercent }: StockTickerProps) {
const isPositive = change >= 0;
const sign = isPositive ? '+' : '';
const color = isPositive ? 'text-green-600' : 'text-red-600';
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold">{symbol}</h3>
<span className={`text-sm font-medium ${color}`}>
{sign}{changePercent.toFixed(2)}%
</span>
</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-bold">${price.toFixed(2)}</span>
<span className={`text-sm ${color}`}>
{sign}{change.toFixed(2)} 今日
</span>
</div>
</div>
);
}
// components/loading-skeleton.tsx
export function CardSkeleton({ height = 'h-32' }: { height?: string }) {
return (
<div className={`animate-pulse rounded-lg bg-muted ${height} w-full`} />
);
}
第三步:定义 AI 工具(Server Action)
这是 Generative UI 的核心。创建一个 Server Action,将你的组件作为"工具"连接到 AI——即模型可以决定调用的函数:
// app/actions.tsx
'use server';
export const runtime = 'edge';
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { WeatherCard } from '@/components/weather-card';
import { StockTicker } from '@/components/stock-ticker';
import { CardSkeleton } from '@/components/loading-skeleton';
export async function generateUI(prompt: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: `You are a helpful financial and information assistant.
Use the available tools to display information visually
whenever possible. Prefer showing components over text responses.
When asked about weather or stocks, always use the appropriate tool.`,
prompt,
tools: {
showWeather: {
description: 'Display current weather conditions for a city. Use this when the user asks about weather, temperature, or climate.',
parameters: z.object({
city: z.string().describe('The city name, e.g. "Paris" or "New York"'),
temperature: z.number().describe('Current temperature in Celsius'),
conditions: z.string().describe('Weather description, e.g. "Partly cloudy"'),
humidity: z.number().min(0).max(100).describe('Relative humidity percentage'),
}),
generate: async function* (params) {
// 立即 yield 骨架屏,同时"加载"数据
yield <CardSkeleton height="h-36" />;
// 在真实应用中,这里应该获取实时天气数据
return <WeatherCard {...params} />;
},
},
showStock: {
description: 'Display a stock price and daily change. Use this when the user asks about stock prices, market data, or a company\'s shares.',
parameters: z.object({
symbol: z.string().describe('Stock ticker symbol, e.g. "AAPL" or "TSLA"'),
price: z.number().describe('Current stock price in USD'),
change: z.number().describe('Price change today in USD'),
changePercent: z.number().describe('Percentage price change today'),
}),
generate: async function* (params) {
yield <CardSkeleton height="h-32" />;
return <StockTicker {...params} />;
},
},
},
});
return result.value;
}
关于这段代码,有三点值得理解:
generate 函数是异步生成器。 yield 关键字在 AI 完成参数解析之前立即发送骨架屏。return 发送最终组件。这就是流式 Generative UI 的工作方式。
工具描述是给 AI 的指令。 description 字段是模型读取以决定调用哪个工具的内容。写清楚,包括何时应该和不应该使用该工具。
Zod schema 强制执行契约。 如果你定义了严格的 Zod schema,AI 就无法传入无效参数。校验失败会在组件渲染之前被捕获。
第四步:构建 UI
// app/page.tsx
'use client';
import { useState } from 'react';
import { generateUI } from './actions';
const EXAMPLE_PROMPTS = [
"东京现在天气怎么样?",
"显示苹果公司当前股价",
"对比伦敦和纽约的天气",
"特斯拉股票表现如何?",
];
export default function Home() {
const [prompt, setPrompt] = useState('');
const [messages, setMessages] = useState<Array<{ prompt: string; ui: React.ReactNode }>>([]);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
setLoading(false);
}
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="text-3xl font-bold">Generative UI 演示</h1>
<p className="mt-2 text-muted-foreground">
询问天气或股票——看 AI 生成正确的界面。
</p>
{/* 示例提示词 */}
<div className="mt-4 flex flex-wrap gap-2">
{EXAMPLE_PROMPTS.map(p => (
<button
key={p}
onClick={() => setPrompt(p)}
className="rounded-full border px-3 py-1 text-sm hover:bg-muted"
>
{p}
</button>
))}
</div>
{/* 提示词输入 */}
<form onSubmit={handleSubmit} className="mt-6 flex gap-2">
<input
value={prompt}
onChange={e => setPrompt(e.target.value)}
placeholder="问任何问题..."
className="flex-1 rounded-md border bg-background px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<button
type="submit"
disabled={loading || !prompt.trim()}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50"
>
{loading ? '生成中...' : '提问'}
</button>
</form>
{/* 生成的 UI 输出 */}
<div className="mt-8 space-y-6">
{messages.map((msg, i) => (
<div key={i}>
<p className="mb-2 text-sm font-medium text-muted-foreground">
"{msg.prompt}"
</p>
{msg.ui}
</div>
))}
</div>
</main>
);
}
第五步:运行和测试
npm run dev
按顺序尝试以下提示词,观察不同的行为:
- "巴黎的天气怎么样?" — 单个 WeatherCard
- "显示苹果股票" — 单个 StockTicker
- "对比伦敦和纽约的天气" — AI 调用
showWeather两次,并排生成两张卡片 - "特斯拉表现如何,旧金山天气怎么样?" — AI 调用两个工具,生成混合类型的组件
第三个提示词是关键演示:不需要任何额外代码,模型就能组合多个组件来回答一个多部分问题。
幕后发生了什么
当你提交提示词时:
- 客户端调用
generateUIServer Action streamUI将提示词 + 工具定义发送到 OpenAI API- 模型选择调用哪些工具以及传入什么参数
- 每个工具的
generate函数立即 yield 骨架屏 - AI 完成参数解析,最终组件被返回
- React 用组件替换骨架屏
RSC 流式传输协议使这一切成为可能。服务端序列化 React 组件树并以增量方式流式传输到客户端。这与 JSON API 不同——客户端接收的是渲染好的组件,而不是原始数据。
错误处理
生成的组件可能以手工编码的组件不会出现的方式失败。但有一个值得注意的细节:React 错误边界只捕获渲染时错误。流式传输失败(网络中断、OpenAI 超时、服务端工具错误)不会被错误边界捕获——你需要在 handleSubmit 中显式处理。
两层防御——流的 try/catch,加上渲染 UI 的错误边界:
// components/genui-error-boundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean; error: Error | null }
export class GenUIErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="rounded-lg border border-destructive/50 bg-destructive/5 p-4">
<p className="text-sm text-destructive">
此组件无法渲染。AI 可能传入了意外数据。
</p>
</div>
);
}
return this.props.children;
}
}
在客户端捕获流错误:
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
try {
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
} catch (err) {
// 网络错误、OpenAI 超时、Server Action 失败——都在这里捕获
setMessages(prev => [...prev, {
prompt: currentPrompt,
ui: <div className="text-sm text-destructive">流式传输失败,请重试。</div>,
}]);
} finally {
setLoading(false);
}
}
用 GenUIErrorBoundary 包裹页面上生成的 UI——它处理渲染错误,try/catch 处理其他所有情况。
何时不应使用 Vercel AI SDK
SDK 很可靠,但不是万能的。以下情况请跳过它:
- SDK 被标记为实验性 — 已记录的限制:流式传输无法从 Server Actions 中止,组件在
.done()时重新挂载(闪烁),过多的<Suspense>边界可能导致页面崩溃,createStreamableUI产生二次方增长的传输量。生产环境 Vercel 推荐 AI SDK UI。 - 你不在 Next.js 上。
streamUI基于 React Server Components,需要 Next.js App Router(或 Waku 等支持 RSC 的框架)。对于 Vite SPA、不带 RSC 的 Remix、Vue、Svelte 或 Angular——见下方替代方案。 - 你需要固定 UI + 动态数据。 如果界面是预先确定的,只有 LLM 来填充数据,使用普通的
generateObject+ 你的静态 React。只有当 AI 决定显示哪些组件时,Generative UI 才有价值。 - 严格隐私或本地部署。 SDK 假设使用托管提供商(OpenAI、Anthropic)。对于自托管 LLM,在
vLLM/Ollama之上加一层薄封装加上你自己的组件注册表会更简单。 - 实时协作或多人场景。 RSC 流是单向的。对于用户间的双向 UI 同步,你需要基于 WebSocket 的方案,而不是 RSC。
- Token 预算很紧张。 每次渲染都是一次 LLM 调用。在没有激进缓存的情况下,MAU > 10k 时,gpt-4o 的成本可能超过 $1,000/月。
非 Next.js 项目的替代方案
| 工具 | 技术栈 | 适用场景 |
|---|---|---|
| Thesys C1 API | 任意(HTTP API) | 通过 JSON schema 返回即开即用 UI 块的 SaaS。适合没有 RSC 专业知识的团队。 |
| CopilotKit | React(Next.js + Vite + Remix) | 带状态和动作的应用内副驾驶。通过 useCopilotAction 支持 Generative UI。 |
| Tambo | React(通用) | 以组件目录为核心概念。支持 Vite,不需要 RSC。 |
| A2UI | 任意(Google) | Google 面向智能体的声明式 JSON UI 格式。与渲染器无关,可在任何前端上渲染。 |
| assistant-ui | React | 支持工具 UI 的聊天优先库。适合任何 React 应用的副驾驶的坚实基础。 |
| 自己动手 | 任意 | 如果你只需要 2–3 种组件类型且控制权更重要——注册表 + generateObject + 客户端 switch 语句大约 150 行。 |
截至 2026 年 5 月,对于 Vue / Svelte / Angular,还没有与 Vercel AI SDK 相当的生产级替代方案。大多数团队向返回 JSON 组件描述的 API 发送请求,然后自己渲染。
在低成本平台上部署
Vercel 是 Next.js 的首选,但不是唯一选择。如果预算有限或想避免供应商锁定:
- Fly.io — 爱好者计划 $0–5/月。通过 Dockerfile 部署 Next.js,全球边缘区域。免费套餐限制为 3 台机器 × 256MB。
- Render — 免费 web 服务在 15 分钟无活动后休眠(休眠后第一次请求约需 30 秒)。适合演示和个人项目,不适合生产。
- Railway — 爱好者计划每月 $5 积分。简单的 GitHub 驱动部署,出色的开发体验,但扩展后比 Fly.io 贵。
- Cloudflare Pages + Workers — 每天最多 100k 请求免费。需要
nodejs_compat运行时,RSC 流式传输需要注意一些限制。 - 自托管 VPS + Coolify / Dokploy — 从 $5/月起(Hetzner、Contabo)。完全控制权,你负责更新、SSL、监控。
对于大多数个人项目,Fly.io 是最佳平衡:免费套餐起步,有真实的生产路径,边缘区域,无供应商锁定。
团队技能要求
在将 Vercel AI SDK 引入生产之前,评估团队和技术栈的准备情况:
必备技能:
- React Server Components — 没有这个,
streamUI在第一个 bug 时就是黑盒。 - TypeScript — 没有类型,Zod schema 和工具参数会变成一团泥。
- 异步生成器(
async function*,yield)——不是每个中级 React 工程师都用过这些。 - 提示词工程 — 工具描述和系统提示决定了组件选择的质量。这是一个独立的技能。
加分技能:
- LLM API 经验(速率限制、重试策略、token 计费)。
- 边缘运行时及其限制(无 Node.js API,bundle 大小上限)。
- 可观测性——工具调用的结构化日志、请求追踪。
团队规模与 TCO(粗略估算,2026 年 5 月):
| 规模 | 工程时间 | LLM 成本(MAU 1k) | LLM 成本(MAU 10k) | 现实可行? |
|---|---|---|---|---|
| 独立(1 人) | 2–3 周到 MVP | ~$50/月(gpt-4o-mini) | ~$500/月 | 是,副业项目甜蜜点 |
| 小团队(2–4 人) | 4–6 周到 v1 | ~$150/月(gpt-4o 混合) | ~$1,500/月 | 是,主要使用场景 |
| 中团队(5–15 人) | 2–3 个月到完整集成 | ~$300/月 | ~$3,000–5,000/月 | 是,如果有平台支撑 |
| 大团队(15+ 人) | 4–6 个月 + 平台团队 | 预算协商 | $10,000+/月 | 值得评估自托管 LLM |
LLM 数字假设"每次会话 1 次请求,gpt-4o 用于工具选择,gpt-4o-mini 用于参数填充"。实际成本很大程度上取决于提示词长度、重试率和缓存策略。
TCO 方法论: 数据基于以下假设计算——平均提示词约 800 输入 + 约 300 输出 token(gpt-4o)或约 $0.001(gpt-4o-mini),每次会话 1 次请求,OpenAI 2026-05 定价,MAU ≈ DAU × 30%。请根据你自己的工作负载进行调整。
部署提示
环境变量: OPENAI_API_KEY 必须在生产环境中可用。在 Vercel 上,在项目设置的环境变量中添加它。
边缘运行时: streamUI 函数在边缘运行时上工作,可以显著减少冷启动时间。在你的 Server Action 文件中添加 export const runtime = 'edge'。
速率限制: 没有速率限制的话,单个用户可能生成数千次 AI 请求。在 streamUI 调用前添加速率限制器。@upstash/ratelimit 包与 Next.js 集成良好。
模型选择: gpt-4o 产生最好的组件选择效果但成本更高。gpt-4o-mini 在输入/输出上便宜约 15 倍(openai.com/api/pricing,2026-05),对于简单的组件集效果也很好。用你具体的工具定义对两者都进行测试。
下一步
本教程涵盖了基础知识。对于生产级 Generative UI:
- 添加更多工具 — 每添加一个组件到注册表,就扩展了 AI 能够回答的问题范围
- 实现工具结果缓存 — 缓存常见查询以降低延迟和成本
- 在 UI 组件旁边添加流式文字,让 AI 能够解释它正在展示什么
- 使用结构化输出以获得更可靠的参数生成
- 设置可观测性 — 记录每次工具调用、其参数和用户交互
Vercel AI SDK 文档深入涵盖了所有这些模式,示例仓库中有值得学习的生产级启动模板。
关于 AI SDK v5/v6
如果你在使用更新版本的 SDK,与本文代码的主要差异:
- 工具定义中的
parameters:→inputSchema: import { streamUI } from 'ai/rsc'→import { streamUI } from '@ai-sdk/rsc'- RSC 仍然被 Vercel 标记为实验性——生产环境推荐 AI SDK UI(
useChat)。
想在你的产品中实现 Generative UI?聊聊你的用例——根据我们的咨询经验,GenUI 技术栈非常适合仪表板和内部工具;对于受监管的界面和高流量公开页面,权衡下来通常不合算。
免责声明: 外部产品链接(Thesys、CopilotKit、Tambo、Vercel、Fly.io、Render、Railway、Cloudflare、Upstash)均为有机推荐;无附属计划,无付费推广。价格准确截至 2026-05-11。
Alex
Generative UI Engineer & Consultant
专注于 AI 界面与 Generative UI 系统的资深工程师。帮助产品团队用正确的 GenUI 技术栈更快交付。
相关文章
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
掌握 Generative UI 前沿动态
每周文章、框架更新与实用实现指南——直达你的邮箱。
需要帮助实现你刚读到的内容?
预约免费咨询