🚀 Vercel AI SDK 使用指南:生成式用户界面 (Generative UI)

8 阅读4分钟

在构建 AI Chatbot 时,我们通常局限于纯文本的交互。用户询问天气,AI 返回一段文字:"旧金山今天是晴天,气温 20 度"。

但在现代应用中,我们希望 AI 能做得更多——直接渲染一个精美的天气卡片,或者一个互动的股市走势图。这就是 Generative UI (生成式用户界面) 的核心理念:让大模型不仅能生成文本,还能"生成"界面。

本文将带你深入理解 Vercel AI SDK 的 Generative UI 机制,并手把手教你实现一个能动态渲染组件的聊天应用。

🤔 什么是 Generative UI?

简单来说,Generative UI 是将 Tool Calling (工具调用) 的结果直接连接到 React 组件 的过程。

工作流程如下:

  1. 用户发送消息(例如:"查询旧金山的天气")。
  2. 模型根据上下文决定调用一个工具(例如:getWeather)。
  3. 工具在服务端执行,返回结构化数据(JSON)。
  4. 客户端接收到数据,不是显示 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>
  );
}

💡 核心要点总结

  1. 强类型工具名:在客户端判断渲染逻辑时,使用 part.type === 'tool-[key]',这里的 [key] 必须对应 ai/tools.ts 中导出对象的键名(即本例中的 displayWeather)。

  2. 状态管理:工具调用有三个主要状态,你可以分别为它们定制 UI:

    • input-available: 工具已被模型调用,正在等待结果(适合展示 Loading 骨架屏)。
    • output-available: 工具执行完成,数据已就绪(展示最终组件)。
    • output-error: 执行出错。
  3. 分离关注点:将数据获取(Server Tool)与数据展示(Client Component)完全解耦,让代码更易维护。

通过这种方式,你可以轻松扩展出查询股票、预订机票、生成图表等丰富的交互功能,让你的 AI 应用从"聊天框"进化为"智能终端"。