🚀 Vercel AI SDK 使用指南:手动读取 UI 消息流 (Reading UI Message Streams)

8 阅读4分钟

在 Vercel AI SDK 中,大家最熟悉的可能是在 React 前端使用的 useChatuseCompletion 钩子。但在某些场景下——比如开发一个终端聊天工具 (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 的流式响应。

核心要点:

  1. 转化:使用 result.toUIMessageStream() 获取兼容的流。
  2. 消费:使用 for await...of readUIMessageStream(...) 进行迭代。
  3. 解析:利用 uiMessage.parts 精细控制文本与工具调用的展示。

希望这篇教程能帮你更好地掌控 AI 数据流!Happy Coding! 🚀