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):
| 视图 | 类型 | 给谁 |
|---|---|---|
textStream | AsyncIterableStream<string> | 只想要纯文本增量(:306) |
fullStream | AsyncIterableStream<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 ← 整体生命周期
怎么读: fullStream 把 02 那个 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()直接产出可返回的 HTTPResponse。
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/processUIMessageStream把streamText的事件转成 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注释:toModelOutput在convertToModelMessages端被调用),否则工具结果的模型表示会不一致。- 协议是 AI SDK 自有格式(非某个标准),前端要用配套的处理器消费;裸 SSE 客户端需自行实现折叠逻辑。
4.7 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 流式入口 | packages/ai/src/generate-text/stream-text.ts | streamText |
| 流视图与事件类型 | packages/ai/src/generate-text/stream-text-result.ts | StreamTextResult, TextStreamPart |
| UI 块 schema | packages/ai/src/ui-message-stream/ui-message-chunks.ts | uiMessageChunkSchema |
| 转 UI 流 | packages/ai/src/ui-message-stream/to-ui-message-stream.ts | toUIMessageStream |
| SSE 封帧 | packages/ai/src/ui-message-stream/json-to-sse-transform-stream.ts | (transform stream) |
| 前端状态机基类 | packages/ai/src/ui/chat.ts | AbstractChat |
| 框架具体类 | packages/react/src/chat.react.ts | Chat(继承 AbstractChat) |
| 块→消息折叠 | packages/ai/src/ui/process-ui-message-stream.ts | processUIMessageStream |
| UI→模型翻译 | packages/ai/src/ui/convert-to-model-messages.ts | convertToModelMessages |
下一章:05 · 结构化输出。