🚀 Vercel AI SDK 使用指南:数据流 (Streaming Data)

4 阅读3分钟

在开发 AI 应用时,除了流式传输文本(Text Streaming),我们往往还需要传输额外的数据。比如:

  • RAG 场景:在回答之前,先展示检索到的参考文档(Source)。
  • 状态更新:展示 "正在搜索..."、"正在思考..." 等中间状态。
  • 调试信息:返回 token 使用量、耗时等元数据。

Vercel AI SDK 提供了强大的 Data Stream 机制,允许我们在服务器端将自定义数据(Data Parts)“混入”到流中,并在客户端优雅地渲染出来。

本文将带你通过实战掌握这一核心功能。


1. 核心概念:数据流是如何工作的?

在 Vercel AI SDK (UI) 中,服务器响应不仅仅是一串文本,它实际上是一个混合流。我们可以使用 createUIMessageStream 创建一个流,通过 writer.write() 写入不同类型的数据块:

  • Persistent Data (持久化数据) :会保存到 message.parts 历史记录中(如天气信息、参考链接)。
  • Transient Data (瞬态数据)不会保存到历史记录,只用于即时展示(如 Toast 通知、Loading 状态),客户端通过 onData 回调接收。

2. 定义类型 (Type Safety)

为了在前后端获得完整的 TypeScript 提示,我们首先定义一个扩展的 Message 类型。

创建 ai/types.ts

TypeScript

import { UIMessage } from 'ai';

// 定义你的自定义消息类型
export type MyUIMessage = UIMessage<
  never, // Metadata 类型 (暂时不用)
  {
    // 定义具体的数据部分 (Data Parts)
    weather: {
      city: string;
      temperature: number;
      status: 'loading' | 'success';
    };
    sources: {
      url: string;
      title: string;
    }[];
    notification: {
      message: string;
      level: 'info' | 'error';
    };
  }
>;

3. 服务端实现 (Server-Side)

在 Route Handler 中,我们将创建一个数据流,先写入一些自定义数据,然后将 AI 模型的文本流合并进去。

创建 app/api/chat/route.ts

TypeScript

import { openai } from '@ai-sdk/openai';
import {
  createUIMessageStream,
  createUIMessageStreamResponse,
  streamText,
  convertToModelMessages,
} from 'ai';
import type { MyUIMessage } from '@/ai/types';

export async function POST(req: Request) {
  const { messages } = await req.json();

  // 1. 创建 UI 消息流
  const stream = createUIMessageStream<MyUIMessage>({
    execute: async ({ writer }) => {
      // 演示:写入一个瞬态通知 (不会存入历史)
      writer.write({
        type: 'data-notification',
        data: { message: '正在分析您的请求...', level: 'info' },
        transient: true, // 标记为瞬态
      });

      // 演示:写入初始数据 (Loading 状态)
      writer.write({
        type: 'data-weather',
        id: 'weather-part-1', // 给它一个 ID,方便后续更新 (Reconciliation)
        data: { city: '杭州', temperature: 0, status: 'loading' },
      });

      // 2. 调用大模型
      const result = streamText({
        model: openai('gpt-4o'), // 或者使用兼容的其他模型
        messages: convertToModelMessages(messages),
        async onFinish() {
            // 3. 模型完成后,更新之前的数据 (Reconciliation)
            // 使用相同的 ID 'weather-part-1',客户端会自动合并/更新数据
            writer.write({
                type: 'data-weather',
                id: 'weather-part-1', 
                data: { city: '杭州', temperature: 26, status: 'success' },
            });
            
            // 写入参考来源
            writer.write({
                type: 'data-sources',
                data: [{ title: 'Vercel AI SDK Docs', url: 'https://sdk.vercel.ai' }]
            });
        }
      });

      // 4. 将模型的文本流合并到主流中
      writer.merge(result.toUIMessageStream());
    },
  });

  // 5. 返回响应
  return createUIMessageStreamResponse({ stream });
}

关键点解析:

  • writer.write():发送数据块。
  • transient: true:标记该数据不保存到聊天记录。
  • id 复用(Reconciliation):如果你发送两个 ID 相同的数据块,SDK 会自动用新的覆盖旧的。这非常适合做 "Loading -> Result" 的状态切换。

4. 客户端实现 (Client-Side)

在前端,我们使用 useChat 钩子。我们需要处理两件事:

  1. 渲染持久化数据:遍历 message.parts
  2. 处理瞬态数据:使用 onData 回调。

创建 app/page.tsx

TypeScript

'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import type { MyUIMessage } from '@/ai/types';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat<MyUIMessage>({
    api: '/api/chat',
    // 处理瞬态数据 (Notifications)
    onData: (dataPart) => {
      if (dataPart.type === 'data-notification') {
        console.log(`收到通知: ${dataPart.data.message}`);
        // 这里可以调用 toast.success(dataPart.data.message)
      }
    },
  });

  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map((m) => (
        <div key={m.id} className="whitespace-pre-wrap mb-4">
          <div className="font-bold">{m.role === 'user' ? 'User: ' : 'AI: '}</div>

          {/* 渲染 Message Parts */}
          {m.parts?.map((part, index) => {
            // 1. 渲染文本
            if (part.type === 'text') {
              return <span key={index}>{part.text}</span>;
            }

            // 2. 渲染天气组件
            if (part.type === 'data-weather') {
              return (
                <div key={index} className="p-4 my-2 bg-gray-100 rounded-lg">
                  {part.data.status === 'loading' ? (
                    <span>🔄 正在查询 {part.data.city} 的天气...</span>
                  ) : (
                    <span>☀️ {part.data.city} 当前气温: {part.data.temperature}°C</span>
                  )}
                </div>
              );
            }

            // 3. 渲染参考来源
            if (part.type === 'data-sources') {
              return (
                <div key={index} className="text-sm text-gray-500 mt-2">
                  参考资料:
                  {part.data.map((src, i) => (
                    <a key={i} href={src.url} className="text-blue-500 ml-1 underline" target="_blank">
                      {src.title}
                    </a>
                  ))}
                </div>
              );
            }

            return null;
          })}
        </div>
      ))}

      <form onSubmit={handleSubmit} className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl">
        <input
          className="w-full p-2"
          value={input}
          onChange={handleInputChange}
          placeholder="Say something..."
        />
      </form>
    </div>
  );
}

总结

通过 Vercel AI SDK 的 Data Streaming,我们不再局限于简单的文本对话。利用 writer.writemessage.parts,我们可以构建出交互性更强、信息密度更高的 AI 应用。

核心优势:

  • 类型安全:通过泛型确保前后端数据结构一致。
  • 🔄 自动协调 (Reconciliation) :通过 ID 自动更新 UI 状态,无需手动管理繁琐的 Loading 变量。
  • 🚀 灵活:既支持持久化存储(如搜索结果),也支持临时通知(如 Toast)。