在 Vercel AI SDK 中,大家最熟悉的可能是在 React 前端使用的 useChat 或 useCompletion 钩子。但在某些场景下——比如开发一个终端聊天工具 (CLI Chatbot) 、在 Node.js 脚本中处理流、或者在 React Server Components (RSC) 中直接消费流——我们需要一种更底层的方式来读取和解析消息流。
这就是 readUIMessageStream 发挥作用的地方。它能将原始的数据流转换为易于处理的 UIMessage 对象流。
本文将带你深入了解如何手动掌控 AI 的响应流。
📦 核心概念:readUIMessageStream
通常,streamText 生成的流是原始的文本或数据块。readUIMessageStream 是一个辅助函数,它能将这些碎片化的 UIMessageChunk 转换成完整的、结构化的 UIMessage 对象流。
这意味着你不需要自己去拼接字符串,SDK 会帮你处理好消息的构建过程。
适用场景
- 🖥️ 终端界面 (Terminal UIs) :在命令行中实时显示 AI 回复。
- ⚙️ 自定义流处理:在客户端以非标准方式展示数据。
- ⚡ React Server Components (RSC) :在服务端组件中直接处理流。
🛠️ 实战一:基础文本流读取
让我们从最简单的例子开始:在一个 Node.js 脚本中调用 AI 并实时打印结果。
我们将使用 streamText 生成流,并使用 result.toUIMessageStream() 将其转换为 UI 消息流。
TypeScript
import { readUIMessageStream, streamText } from 'ai';
import { openai } from '@ai-sdk/openai'; // 假设使用 OpenAI,也可以换成其他 provider
async function main() {
// 1. 发起请求,获取流对象
const result = streamText({
model: openai('gpt-4o'),
prompt: '用一句话讲一个关于程序员的冷笑话。',
});
// 2. 使用 readUIMessageStream 消费流
// result.toUIMessageStream() 将生成结果转换为 UI 消息流
for await (const uiMessage of readUIMessageStream({
stream: result.toUIMessageStream(),
})) {
// 3. 实时打印消息状态
// 注意:uiMessage 代表当前消息的"累积"状态,而不仅是增量
console.clear(); // 模拟终端刷新效果
console.log('正在生成:', uiMessage.content);
}
console.log('\n✅ 生成完成!');
}
main().catch(console.error);
代码解析:
streamText:发起 AI 请求。toUIMessageStream():这是关键一步,将标准流转换为 UI 消息流格式。for await...of:异步迭代器,让我们能像处理数组一样处理流数据。
🔧 实战二:处理工具调用 (Tool Calls)
现代 AI 应用离不开 Tool Calling(工具调用)。当模型决定调用工具时,流的内容会变得复杂(包含文本、工具调用参数、工具执行结果等)。
readUIMessageStream 能够解析这些不同的部分 (parts),让我们能分别处理它们。
TypeScript
import { readUIMessageStream, streamText, tool } from 'ai';
import { z } from 'zod';
import { openai } from '@ai-sdk/openai';
async function handleToolCalls() {
const result = streamText({
model: openai('gpt-4o'),
tools: {
weather: tool({
description: '获取某地的天气',
parameters: z.object({
location: z.string().describe('城市名称'),
}),
execute: async ({ location }) => ({
location,
temperature: 25 + Math.floor(Math.random() * 10), // 模拟数据
condition: 'Sunny',
}),
}),
},
prompt: '东京现在的天气怎么样?',
});
// 遍历消息流
for await (const uiMessage of readUIMessageStream({
stream: result.toUIMessageStream(),
})) {
// uiMessage.parts 是一个数组,包含了消息的各个组成部分
uiMessage.parts.forEach(part => {
switch (part.type) {
case 'text':
// 处理普通文本
process.stdout.write(`\r文本内容: ${part.text}`);
break;
case 'tool-invocation':
// 处理工具调用
// 注意:Vercel AI SDK 新版中通常统称为 tool-invocation,
// 具体可能细分为 'tool-call' (调用中) 和 'tool-result' (调用后)
// 这里的 switch 逻辑取决于具体的 UI Message 结构,标准结构通常如下:
const invocation = part as any;
if ('toolName' in invocation && !('result' in invocation)) {
console.log(`\n🤖 AI 正在调用工具: ${invocation.toolName}`);
console.log(` 参数: ${JSON.stringify(invocation.args)}`);
} else if ('result' in invocation) {
console.log(`\n✅ 工具返回结果: ${JSON.stringify(invocation.result)}`);
}
break;
}
});
}
}
handleToolCalls();
💡 重点提示:
在处理复杂流时,uiMessage.parts 是核心。它将一条消息拆解为多个部分(Part),每个部分都有明确的类型 (type),这使得在 UI 上分别渲染文本和工具卡片变得非常容易。
🔄 实战三:恢复/接续对话 (Resuming Conversations)
如果你需要从某个特定的消息状态继续处理流(例如,网络中断后重连,或者在一个长流程中分步处理),你可以利用 readUIMessageStream 的输入参数来指定初始状态。
TypeScript
import { readUIMessageStream, streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
async function resumeConversation(lastMessageState: any) {
// 假设我们需要基于之前的上下文继续生成
const result = streamText({
model: openai('gpt-4o'),
messages: [
{ role: 'user', content: '继续我们刚才关于量子物理的讨论。' },
],
});
// 从上一次的消息状态开始读取
for await (const uiMessage of readUIMessageStream({
stream: result.toUIMessageStream(),
message: lastMessageState, // 👈 关键:传入之前的消息状态
})) {
console.log('接续生成的内容:', uiMessage.content);
}
}
📝 总结
readUIMessageStream 是 Vercel AI SDK 中一个强大但容易被忽视的工具。它跳出了 React Hook 的限制,让你在任何支持 JavaScript 的地方(CLI、脚本、服务端)都能优雅地处理 AI 的流式响应。
核心要点:
- 转化:使用
result.toUIMessageStream()获取兼容的流。 - 消费:使用
for await...of readUIMessageStream(...)进行迭代。 - 解析:利用
uiMessage.parts精细控制文本与工具调用的展示。
希望这篇教程能帮你更好地掌控 AI 数据流!Happy Coding! 🚀