在构建 AI Chatbot 时,我们通常局限于纯文本的交互。用户询问天气,AI 返回一段文字:"旧金山今天是晴天,气温 20 度"。
但在现代应用中,我们希望 AI 能做得更多——直接渲染一个精美的天气卡片,或者一个互动的股市走势图。这就是 Generative UI (生成式用户界面) 的核心理念:让大模型不仅能生成文本,还能"生成"界面。
本文将带你深入理解 Vercel AI SDK 的 Generative UI 机制,并手把手教你实现一个能动态渲染组件的聊天应用。
🤔 什么是 Generative UI?
简单来说,Generative UI 是将 Tool Calling (工具调用) 的结果直接连接到 React 组件 的过程。
工作流程如下:
- 用户发送消息(例如:"查询旧金山的天气")。
- 模型根据上下文决定调用一个工具(例如:
getWeather)。 - 工具在服务端执行,返回结构化数据(JSON)。
- 客户端接收到数据,不是显示 JSON,而是根据数据渲染一个预定义的 React 组件(例如:
<WeatherCard />)。
🛠️ 实战开发
我们将构建一个简单的聊天应用,当用户询问天气时,它会显示一个可视化的天气组件。
第一步:定义工具 (Server-Side)
首先,我们需要在服务端定义一个工具。工具本质上是一个带有 Zod 参数校验的函数。
创建 ai/tools.ts:
TypeScript
import { tool as createTool } from 'ai';
import { z } from 'zod';
// 定义一个天气工具
export const weatherTool = createTool({
description: '显示指定地点的天气信息',
inputSchema: z.object({
location: z.string().describe('需要获取天气的地点'),
}),
// 模拟异步请求,实际项目中这里会调用第三方 API
execute: async function ({ location }) {
await new Promise(resolve => setTimeout(resolve, 2000));
return { weather: 'Sunny', temperature: 24, location };
},
});
// 导出工具集合,键名 'displayWeather' 很重要,后续前端会用到
export const tools = {
displayWeather: weatherTool,
};
第二步:创建 API 路由 (Server-Side)
接下来,创建处理聊天请求的 API 路由。我们需要将定义好的 tools 传递给 streamText 函数。
创建 app/api/chat/route.ts:
TypeScript
import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai';
import { tools } from '@/ai/tools'; // 引入刚才定义的工具
// 这里以 Claude 为例,你也可以替换为 'gpt-4o' 等其他模型
export async function POST(request: Request) {
const { messages }: { messages: UIMessage[] } = await request.json();
const result = streamText({
model: "anthropic/claude-sonnet-4.5",
system: '你是一个友好的助手!',
messages: await convertToModelMessages(messages),
stopWhen: stepCountIs(5), // 防止模型陷入过深的循环
tools, // 注册工具
});
return result.toUIMessageStreamResponse();
}
第三步:创建 UI 组件 (Client-Side)
这是 Generative UI 的可视部分。我们需要一个纯 React 组件来展示工具返回的数据。
创建 components/weather.tsx:
TypeScript
type WeatherProps = {
temperature: number;
weather: string;
location: string;
};
export const Weather = ({ temperature, weather, location }: WeatherProps) => {
return (
<div className="border p-4 rounded-lg bg-blue-50 my-2">
<h2 className="font-bold text-lg">{location} 当前天气</h2>
<p>状况: {weather}</p>
<p>温度: {temperature}°C</p>
</div>
);
};
第四步:整合前端逻辑 (Client-Side)
这是最关键的一步。我们需要在 page.tsx 中根据 message.parts 的类型来动态渲染内容。
注意: 在 AI SDK 5.0+ 中,工具调用的 part 类型会有特定的命名格式:tool-[工具名]。
创建 app/page.tsx:
TypeScript
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import { Weather } from '@/components/weather';
export default function Page() {
const [input, setInput] = useState('');
// 使用 useChat 钩子管理聊天状态
const { messages, sendMessage } = useChat();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
};
return (
<div className="max-w-xl mx-auto p-4">
{messages.map(message => (
<div key={message.id} className="mb-4">
<div className="font-semibold text-sm text-gray-500">
{message.role === 'user' ? 'User: ' : 'AI: '}
</div>
<div>
{/* 遍历消息的各个部分 (parts) */}
{message.parts.map((part, index) => {
// 1. 处理普通文本
if (part.type === 'text') {
return <span key={index}>{part.text}</span>;
}
// 2. 处理特定工具调用:'displayWeather'
// 注意这里的类型匹配字符串:tool-[工具名]
if (part.type === 'tool-displayWeather') {
// 根据工具调用的不同状态渲染不同 UI
switch (part.state) {
case 'input-available':
return <div key={index} className="text-gray-400">正在查询天气...</div>;
case 'output-available':
// 工具执行成功,渲染 Weather 组件
// part.output 包含 execute 函数返回的数据
return (
<div key={index}>
<Weather {...part.output} />
</div>
);
case 'output-error':
return <div key={index} className="text-red-500">Error: {part.errorText}</div>;
default:
return null;
}
}
return null;
})}
</div>
</div>
))}
<form onSubmit={handleSubmit} className="fixed bottom-0 left-0 w-full p-4 bg-white border-t">
<div className="max-w-xl mx-auto flex gap-2">
<input
className="flex-1 p-2 border rounded"
value={input}
onChange={e => setInput(e.target.value)}
placeholder="输入 '查询旧金山天气'..."
/>
<button type="submit" className="px-4 py-2 bg-black text-white rounded">发送</button>
</div>
</form>
</div>
);
}
💡 核心要点总结
-
强类型工具名:在客户端判断渲染逻辑时,使用
part.type === 'tool-[key]',这里的[key]必须对应ai/tools.ts中导出对象的键名(即本例中的displayWeather)。 -
状态管理:工具调用有三个主要状态,你可以分别为它们定制 UI:
input-available: 工具已被模型调用,正在等待结果(适合展示 Loading 骨架屏)。output-available: 工具执行完成,数据已就绪(展示最终组件)。output-error: 执行出错。
-
分离关注点:将数据获取(Server Tool)与数据展示(Client Component)完全解耦,让代码更易维护。
通过这种方式,你可以轻松扩展出查询股票、预订机票、生成图表等丰富的交互功能,让你的 AI 应用从"聊天框"进化为"智能终端"。