跳到主要内容

04 · 流式与 UI 互通

本章讲什么: streamText 怎么把一个底层 part 流变成多种好用的"视图";后端和前端之间那套 UI Message Stream 线协议长什么样;前端 useChat/Chat 怎么消费它、又怎么把消息转回模型格式发回后端。这是 AI SDK 在"web 互操作"上的精华。

4.1 要解决的小问题

流式输出有两个不同受众:

  • 后端逻辑 想要"结构化的事件流"——不只是文本,还有"工具开始调了""这一步结束了""出错了"。
  • 前端 UI 想要"能直接渲染成聊天气泡的东西"——还得能跨网络传输(SSE)、能在浏览器里增量重建消息。

streamText多视图满足前者,UI Message Stream 协议满足后者。

4.2 streamText 的多种流视图

一次 streamText(...) 返回的结果对象,暴露好几个不同粒度的流(stream-text-result.ts):

视图类型给谁
textStreamAsyncIterableStream<string>只想要纯文本增量(:306)
fullStreamAsyncIterableStream<TextStreamPart>要全部事件(文本/工具/步/错误)(:324)
toUIMessageStream()UI 块流转成前端协议(:370)
toTextStreamResponse()HTTP Response直接当纯文本流响应返回(:423)

fullStream 的事件类型 是个大判别联合 TextStreamPart,覆盖整个生命周期(stream-text-result.ts:427-568):

text-start / text-delta / text-end ← 文本分片
reasoning-start / -delta / -end ← 推理分片
tool-input-start / -delta / -end ← 工具参数在流式拼装
tool-call / tool-result / tool-error ← 工具调用与结果
tool-output-denied ← 工具被审批拒绝
source / file ← 引用源 / 生成的文件
start-step / finish-step ← 对应 02 的 step 边界
start / finish / abort / error / raw ← 整体生命周期

怎么读: fullStream02 那个 step 循环里发生的每件事都"事件化"了。start-step/finish-step 就是循环每一圈的边界;tool-input-delta 让你能在模型还在拼工具参数时就看到进度。

4.3 UI Message Stream:后端→前端的线协议

要把上面的事件跨网络送到浏览器,需要一个稳定、带类型标签、可被 zod 校验的线格式。这就是 UI Message Stream。它的每个块都有一个 type 字段做判别:

// packages/ai/src/ui-message-stream/ui-message-chunks.ts —— uiMessageChunkSchema 节选
z.union([
z.strictObject({ type: z.literal('text-start'), id: z.string(), ... }),
z.strictObject({ type: z.literal('text-delta'), id: z.string(), delta: z.string() }),
z.strictObject({ type: z.literal('text-end'), id: z.string() }),
z.strictObject({ type: z.literal('error'), errorText: z.string() }),
z.strictObject({ type: z.literal('tool-input-start'), toolCallId: z.string(),
toolName: z.string(), providerExecuted: z.boolean().optional(), ... }),
z.strictObject({ type: z.literal('tool-input-delta'), toolCallId: z.string(),
inputTextDelta: z.string() }),
// … 更多块类型
])

uiMessageChunkSchema(packages/ai/src/ui-message-stream/ui-message-chunks.ts:23)。三个关键设计:

  • strictObject + literal 判别。 每个块形状被精确锁死,前端可以安全地 switch(chunk.type)。这是把"流"做成"协议"的核心——有 schema 才能跨端可靠解析。
  • 块是"增量指令"不是"完整消息"。 text-start/text-delta/text-end 是"开一段文本 / 追加 / 收尾",前端据此增量重建一条消息,而不是每次重传全文。
  • 可走 SSE。 json-to-sse-transform-stream.ts 把这些 JSON 块包成 Server-Sent Events 帧;toUIMessageStreamResponse() 直接产出可返回的 HTTP Response

4.4 前端:Chat / useChat 怎么消费

前端聊天状态机的核心是 ai 包里的 AbstractChat(packages/ai/src/ui/chat.ts:237)——框架无关的基类;各框架的具体类(如 React 的 Chat,packages/react/src/chat.react.ts:114)继承它,而 useChat 又是对具体 Chat 的封装。它是个状态机:维护一个 UIMessage[],把收到的块流喂给 processUIMessageStream,后者按 type 把块折叠进当前消息。

端到端全栈数据流:

前端 useChat 后端 route
│ 用户发消息 │
├── HTTP POST {messages} ───────────▶ │
│ │ convertToModelMessages(uiMessages)
│ │ UIMessage[] → ModelMessage[]
│ │ streamText({ model, messages, tools })
│ │ result.toUIMessageStreamResponse()
│ ◀──── SSE: uiMessageChunk 流 ────────┤
│ processUIMessageStream │
│ 按 type 折叠进 UIMessage │
│ React 重渲染气泡 │

两个方向各有一个翻译器:

  • 去程:convertToModelMessages(packages/ai/src/ui/convert-to-model-messages.ts)把前端的 UIMessage(带 UI parts:文本、工具调用、文件…)转回 01 说的中立 ModelMessage(里头会重建 tool-call/tool-result 部分,见 convert-to-model-messages.ts:213,249)。
  • 回程:toUIMessageStream / processUIMessageStreamstreamText 的事件转成 UI 块、再在前端折叠回 UIMessage

精华: 后端和前端各自用对自己方便的格式(后端用 ModelMessage,前端用 UIMessage),中间靠两个纯翻译器和一套带 schema 的块协议解耦。换 UI 框架(React/Vue/Svelte)只换"折叠层",协议不变。

4.5 巧妙之处

  • 同一段生成,三种粒度的视图。 textStream 给只要文本的人,fullStream 给要全事件的人,toUIMessageStream 给前端——同一个底层流,按需投影(stream-text-result.ts)。
  • 协议用 zod strictObject 锁形状。 线协议有运行时 schema(ui-message-chunks.ts:23),跨端解析不靠约定靠校验,出错早暴露。
  • 增量块而非全量消息。 UI 重建靠 start/delta/end 三段式,带宽和重渲染都省。
  • 双向各一个翻译器。 convertToModelMessages(UI→模型)与 processUIMessageStream(块→UI)对称,前后端格式彻底解耦。

4.6 边界与局限

  • convertToModelMessages 需要你两边传同一套 tools(tool.ts:147 注释:toModelOutputconvertToModelMessages 端被调用),否则工具结果的模型表示会不一致。
  • 协议是 AI SDK 自有格式(非某个标准),前端要用配套的处理器消费;裸 SSE 客户端需自行实现折叠逻辑。

4.7 代码地图

主题文件符号
流式入口packages/ai/src/generate-text/stream-text.tsstreamText
流视图与事件类型packages/ai/src/generate-text/stream-text-result.tsStreamTextResult, TextStreamPart
UI 块 schemapackages/ai/src/ui-message-stream/ui-message-chunks.tsuiMessageChunkSchema
转 UI 流packages/ai/src/ui-message-stream/to-ui-message-stream.tstoUIMessageStream
SSE 封帧packages/ai/src/ui-message-stream/json-to-sse-transform-stream.ts(transform stream)
前端状态机基类packages/ai/src/ui/chat.tsAbstractChat
框架具体类packages/react/src/chat.react.tsChat(继承 AbstractChat)
块→消息折叠packages/ai/src/ui/process-ui-message-stream.tsprocessUIMessageStream
UI→模型翻译packages/ai/src/ui/convert-to-model-messages.tsconvertToModelMessages

下一章:05 · 结构化输出