在开发 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 钩子。我们需要处理两件事:
- 渲染持久化数据:遍历
message.parts。 - 处理瞬态数据:使用
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.write 和 message.parts,我们可以构建出交互性更强、信息密度更高的 AI 应用。
核心优势:
- ✨ 类型安全:通过泛型确保前后端数据结构一致。
- 🔄 自动协调 (Reconciliation) :通过 ID 自动更新 UI 状态,无需手动管理繁琐的 Loading 变量。
- 🚀 灵活:既支持持久化存储(如搜索结果),也支持临时通知(如 Toast)。