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

在 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

javascript 复制代码
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

javascript 复制代码
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

javascript 复制代码
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! 🚀

相关推荐
stereohomology7 分钟前
大模型的观点谨:StoryTold 「Crafting Apps」四件套 · 深度总览
人工智能·llm
Csvn2 小时前
线上出问题怎么查?一套可复现的排障 SOP(O04)
人工智能·aigc·agent
浮链序4 小时前
怎么证明"你这个模型是偷我的"——把蒸馏变成取证工具
算法·安全·llm
用户938515635076 小时前
从 0 到 1 搭建企业级多模态 RAG 知识库:一个装修公司的 AI 落地实战
人工智能·langchain·llm
我跟你说6 小时前
不改权重的终身学习:AI Agent 的“可塑性”架构与技能自演进深度解构
agent
用户721746588266 小时前
把云端沙箱实例拆成三层看:控制面、运行时、计费面的一次全流程核查
agent·ai编程
用户250458106096 小时前
AI智能体正在改变软件开发方式,但距离完全自主编程还有多远?
aigc·agent
救救孩子把6 小时前
Agent面试题-记忆治理-上下文-多Agent协作与工程落地
面试·agent·多agent·agent记忆·agent面试题·上下文治理
浮生望7 小时前
Agentic RAG(上):别让所有问题都走检索——查询路由与 Web 兜底
agent
deepseek239 小时前
Gemini 4 Argon拆解:单次输出100万Token不是上下文游戏,长周期Agent的经济账怎么算
大模型·llm·谷歌·ai agent·gemini