Wave Terminal 中的 AI SDK UIMessage 类型体系:从泛型定义到消息渲染的完整实践
2026/9/13 13:57:09 网站建设 项目流程

Wave Terminal 中的 AI SDK UIMessage 类型体系:从泛型定义到消息渲染的完整实践

【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm

UIMessage是 Vercel AI SDK 中用于描述应用状态的消息模型:它承载完整的消息历史、元数据、数据部件(data parts)与工具调用上下文,是前端渲染useChat会话消息的"唯一事实来源"(source of truth)。本文以 aisdk-uimessage-type.md 为骨架,结合 Wave Terminal(AI 集成跨平台终端)的 Wave AI 面板源码,系统讲解UIMessage的三个泛型参数、全部UIMessagePart类型定义、如何构造自定义类型的UIMessage,以及这些类型如何在前端渲染、工具审批、流式更新中被真实消费。读完本文,你将能够为任意 AI 聊天应用设计出类型安全的UIMessage模型,并理解 AI SDK 流式协议与UIMessage部件之间的映射关系。

一、UIMessage 与 ModelMessage:两种消息模型的职责划分

在 AI SDK 中,UIMessageModelMessage是两套面向不同消费者的消息模型:

  • ModelMessage:表示传给模型的状态或上下文,是模型推理的输入(例如转换为 OpenAI Chat Completions、Anthropic Messages 格式的历史消息)。
  • UIMessage:表示应用的完整状态,包含渲染 UI 所需的全部信息——消息 ID、角色、元数据,以及结构化的parts数组。它同时被客户端功能(重试、编辑、分支、工具审批等)所消费。

Wave Terminal 的实现恰好体现了这一分工:Go 后端在 uctypes.go 中定义了与 AI SDK 对齐的UIMessage/UIMessagePartJSON 结构(UseChatRequest.MessagesUIChat.Messages均以[]UIMessage传输),并将这些消息转换为各提供商(Anthropic / OpenAI / Gemini)的GenAIMessage原生格式;而前端 aitypes.ts 直接从"ai"包导入UIMessage作为 Wave AI 面板的消息基类。前后端通过同一套 JSON 字段契约(如toolCallIdinputoutputstateproviderMetadata)保持类型一致。

二、类型安全:UIMessage 的三个泛型参数

UIMessage被设计为完全类型安全,接受三个泛型参数:

  1. METADATA:消息级自定义元数据类型,用于附加额外信息(默认unknown)。
  2. DATA_PARTS:自定义数据部件(data part)类型,用于结构化数据组件(默认UIDataTypes)。
  3. TOOLS:工具定义集合,用于类型安全的工具交互(默认UITools)。

泛型参数的约束关系体现在接口签名上:

interface UIMessage<METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools> { id: string; role: "system" | "user" | "assistant"; metadata?: METADATA; parts: Array<UIMessagePart<DATA_PARTS, TOOLS>>; }

从声明可以看出:DATA_PARTSTOOLS会沿着parts向下传播到每一个UIMessagePart,因此一旦你在应用层定义了自定义数据类型和工具集合,parts数组中的data-*tool-*部件就能获得精确到字段级别的类型推导,而不是退化为any

实战:构造你自己的 UIMessage 类型

文档给出如下示例,用于创建带自定义元数据、数据部件与工具集合的UIMessage

import { InferUITools, ToolSet, UIMessage, tool } from "ai"; import z from "zod"; const metadataSchema = z.object({ someMetadata: z.string().datetime(), }); type MyMetadata = z.infer<typeof metadataSchema>; const dataPartSchema = z.object({ someDataPart: z.object({}), anotherDataPart: z.object({}), }); type MyDataPart = z.infer<typeof dataPartSchema>; const tools = { someTool: tool({}), } satisfies ToolSet; type MyTools = InferUITools<typeof tools>; export type MyUIMessage = UIMessage<MyMetadata, MyDataPart, MyTools>;

要点拆解:

  • 元数据与数据部件用zodschema 定义后,通过z.infer提取为 TypeScript 类型,保证运行时校验与静态类型同源;
  • tool({})定义工具,satisfies ToolSet让对象字面量保持精确类型而非被拓宽;
  • InferUITools<typeof tools>ToolSet推导出 UI 侧工具类型(含input/output结构);
  • 最终UIMessage<MyMetadata, MyDataPart, MyTools>即为整个应用共享的消息类型。

Wave Terminal 的真实泛型实例

Wave Terminal 在 aitypes.ts 中定义了自己的数据类型并实例化:

import { ChatRequestOptions, FileUIPart, UIMessage, UIMessagePart } from "ai"; type WaveUIDataTypes = { userfile: { filename: string; size: number; mimetype: string; previewurl?: string; }; tooluse: { toolcallid: string; toolname: string; tooldesc: string; status: "pending" | "error" | "completed"; runts?: number; errormessage?: string; approval?: "needs-approval" | "user-approved" | "user-denied" | "auto-approved" | "timeout"; blockid?: string; writebackupfilename?: string; inputfilename?: string; }; toolprogress: { toolcallid: string; toolname: string; statuslines: string[]; }; }; export type WaveUIMessage = UIMessage<unknown, WaveUIDataTypes, any>; export type WaveUIMessagePart = UIMessagePart<WaveUIDataTypes, any>;

这里WaveUIDataTypes就是第二个泛型参数DATA_PARTS的实例——它声明了三种data-*部件:data-userfile(用户上传文件)、data-tooluse(工具调用状态与审批信息)、data-toolprogress(工具执行进度)。这些类型与 Go 后端的UIMessageDataUserFileUIMessageDataToolUseUIMessageDataToolProgress一一对应,代码注释明确标注"when updating this struct, also modify frontend/app/aipanel/aitypes.ts",实现了跨语言的双向契约同步。

三、UIMessage 接口核心字段

UIMessage接口本身十分精简,四个字段各司其职:

字段类型说明
idstring消息的唯一标识,用于 React key、重试、编辑与分支定位
role"system" \| "user" \| "assistant"消息角色,决定渲染方向与样式
metadata?METADATA消息级自定义元数据,可选
partsArray<UIMessagePart<DATA_PARTS, TOOLS>>消息的全部结构化内容,是 UI 渲染的主要依据

在 aipanelmessages.tsx 中可以看到roleid的典型用法:messages.map((message) => <AIMessage key={message.id} ... />),并用message.role === "assistant"判断是否为最后一条流式消息;system角色则用于注入系统提示(参见 usechat-backend-design.md 中initialMessages注入系统上下文的示例)。

四、UIMessagePart:八种部件类型全解析

parts数组是UIMessage的灵魂。AI SDK 用**可判别联合类型(discriminated union)**将不同内容形态统一在UIMessagePart之下,每种部件通过type字段区分。下面逐一展开文档给出的全部类型。

1. TextUIPart — 文本部件

type TextUIPart = { type: "text"; text: string; state?: "streaming" | "done"; };

state字段标注文本块是否仍在流式传输:流式过程中为"streaming",完成(收到text-end)后为"done"。前端可据此决定是否启用"不完整 Markdown 增量解析"。

2. ReasoningUIPart — 推理部件

type ReasoningUIPart = { type: "reasoning"; text: string; state?: "streaming" | "done"; providerMetadata?: Record<string, any>; };

推理部件承载思考模型(thinking model)的推理过程文本,providerMetadata可透传提供商额外信息(如 token 用量、推理模式等)。Wave Terminal 在 aimessage.tsx 的getThinkingMessage中判断lastPart?.type === "reasoning",在流式期间展示 "AI is thinking..." 并滚动显示推理文本,一旦text部件出现即切换为正文展示——这正是state与部件顺序共同驱动 UI 状态机的实例。

3. ToolUIPart — 工具调用部件

工具部件是状态机最复杂的部件,其type为模板字面量类型`tool-${NAME}`(即工具名someTool对应tool-someTool),并且按state划分四个互斥子状态:

type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{ [NAME in keyof TOOLS & string]: { type: `tool-${NAME}`; toolCallId: string; } & ( | { state: "input-streaming"; input: DeepPartial<TOOLS[NAME]["input"]> | undefined; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: "input-available"; input: TOOLS[NAME]["input"]; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: "output-available"; input: TOOLS[NAME]["input"]; output: TOOLS[NAME]["output"]; errorText?: never; providerExecuted?: boolean; } | { state: "output-error"; input: TOOLS[NAME]["input"]; output?: never; errorText: string; providerExecuted?: boolean; } ); }>;

四个状态对应工具调用的生命周期:

  • input-streaming:模型正在生成工具入参,inputDeepPartial(允许部分字段);
  • input-available:入参生成完毕,可执行工具;
  • output-available:工具执行完成,携带output
  • output-error:工具执行失败,携带errorText

providerExecuted?: boolean用于标识该工具是否已由模型提供商侧执行。Wave Terminal 的 aimessage.tsx 中isDisplayPart只把state === "input-available"tool-*部件视为可显示内容,流式生成入参期间(input-streaming)不打断正文展示。

4. SourceUrlUIPart — 来源 URL 部件

type SourceUrlUIPart = { type: "source-url"; sourceId: string; url: string; title?: string; providerMetadata?: Record<string, any>; };

用于标注回答引用的外部 URL(联网搜索或引用来源),sourceId用于去重与关联。

5. SourceDocumentUIPart — 来源文档部件

type SourceDocumentUIPart = { type: "source-document"; sourceId: string; mediaType: string; title: string; filename?: string; providerMetadata?: Record<string, any>; };

用于引用文档类来源,mediaType标识文档 MIME 类型。

6. FileUIPart — 文件部件

type FileUIPart = { type: "file"; mediaType: string; // IANA media type filename?: string; url: string; // 托管文件 URL 或 Data URL };

url既可以是远端托管地址,也可以是data:Data URL(前端本地读取文件时常用)。Wave Terminal 前端在 ai-utils.ts 中通过createDataUrl(file)File转为 Data URL,并定义了文件类型白名单(图片、PDF、文本/代码文件)与体积上限(文本 200KB、PDF 5MB、图片 10MB),超出时给出错误提示。

7. DataUIPart — 自定义数据部件

type DataUIPart<DATA_TYPES extends UIDataTypes> = ValueOf<{ [NAME in keyof DATA_TYPES & string]: { type: `data-${NAME}`; id?: string; data: DATA_TYPES[NAME]; }; }>;

data-*是 AI SDK 提供的扩展点:任何自定义结构化数据都可以通过data-${NAME}成为消息的一部分,前端按名处理。本文第二节中WaveUIDataTypesuserfiletoolusetoolprogress即为三个实例。以data-tooluse为例,Go 侧 uctypes.go 定义了其数据结构:

type UIMessageDataToolUse struct { ToolCallId string `json:"toolcallid"` ToolName string `json:"toolname"` ToolDesc string `json:"tooldesc"` Status string `json:"status"` // pending | error | completed RunTs int64 `json:"runts,omitempty"` ErrorMessage string `json:"errormessage,omitempty"` Approval string `json:"approval,omitempty"` // needs-approval | user-approved | ... BlockId string `json:"blockid,omitempty"` WriteBackupFileName string `json:"writebackupfilename,omitempty"` InputFileName string `json:"inputfilename,omitempty"` }

IsApproved()方法定义:Approval == "" || user-approved || auto-approved视为已批准。前端 aitooluse.tsx 则消费这些字段:渲染✓/✗/•状态图标、展示 "Approve / Deny" 审批按钮、为写文件工具提供 "Revert File" 备份还原与 "Show Diff" 差异查看入口,甚至通过blockid在终端中高亮相关 Block——充分体现data-*部件承载"客户端专属功能状态"的设计意图。

8. StepStartUIPart — 步骤边界部件

type StepStartUIPart = { type: "step-start"; };

标记一个 step(后端一次 LLM API 调用)的开始,与流式协议中的start-step/finish-step事件配套,用于区分多步拼接的 assistant 消息。

五、流式协议如何落地为 UIMessage

UIMessage是静态形态,而其内容来自流式协议。配套文档 aisdk-streaming.md 描述了 AI SDK 基于 SSE 的数据流协议(自定义后端需设置x-vercel-ai-ui-message-stream: v1头)。两类文档的映射关系如下:

流式事件(SSEdata:落地的 UIMessage 部件
text-start/text-delta/text-endTextUIPartstatestreamingdone
reasoning-start/reasoning-delta/reasoning-endReasoningUIPart
source-urlSourceUrlUIPart
source-documentSourceDocumentUIPart
fileFileUIPart
data-*DataUIPart
tool-input-start/tool-input-delta/tool-input-available/tool-output-availableToolUIPart(状态机流转)
start-step/finish-stepStepStartUIPart与 step 边界
errorerrorText形式附加到消息
finish/[DONE]消息完成标记

Wave Terminal 的 Go 后端在 uctypes.go 中定义了与之一一对应的UseChatStreamPart结构(text-starttext-deltatool-input-availabletool-output-availablefinish-stepfinish等),注释中完整列举了协议支持的 type 常量。流式部件在 aimessage.tsx 中被消费:text部件流式期间交给WaveStreamdown做增量 Markdown 解析(parseIncompleteMarkdown={isStreaming});消息渲染完成且非流式时显示反馈按钮(AIFeedbackButtons)。这一"流式协议 → UIMessage 部件 → React 渲染"的链路正是 AI SDK 聊天架构的标准范式。

六、在应用中完整落地:从类型定义到渲染管线

综合以上内容,在 Wave Terminal(或其他 AI 应用)中落地UIMessage类型体系的完整链路为:

  1. 定义类型:用zod定义METADATADATA_PARTS,用tool()定义TOOLS,组合成应用级MyUIMessage(见第二节示例);
  2. 传输契约:前端UIMessage的 JSON 形态与后端结构体对齐(如uctypes.goUIMessage/UIMessagePart),保证消息持久化(UIChat.Messages)与跨请求传输一致;
  3. 流式接收useChat通过 SSE 协议(aisdk-streaming.md)增量构建消息的parts,由DATA_PARTSTOOLS泛型保证每个部件的类型安全;
  4. 按类型渲染:渲染器对parts做判别式分发——text走 Markdown 渲染、reasoning走思考展示、tool-*data-tooluse/data-toolprogress走工具卡片(含审批、进度、diff 等交互)、data-userfile走文件缩略图列表(见 aimessage.tsx 的AIMessagePartUserMessageFilesAIToolUseGroup);
  5. 客户端功能:基于id实现重试/编辑,基于state实现流式状态展示,基于data-*approval字段实现工具审批交互。

usechat-backend-design.md 还展示了更宏观的视角:Wave Terminal 的目标架构是"前端useChat→ HTTP/SSE → Go 后端 → AI 提供商",前端由单个useChathook 管理全部消息状态,后端解析配置后流式返回标准 AI SDK 格式——而UIMessage正是这条链路上前后端共享的"消息唯一事实来源",也是上述全部能力得以类型安全地串联起来的基础。

总结

UIMessage通过三个泛型参数(METADATADATA_PARTSTOOLS)将元数据、自定义数据部件与工具类型注入到统一的消息结构中;八种UIMessagePart(文本、推理、工具、来源 URL、来源文档、文件、自定义数据、步骤边界)以可判别联合覆盖了现代 AI 聊天的全部内容形态。Wave Terminal 的实践表明:前端UIMessage类型(aitypes.ts)与 Go 后端结构体(uctypes.go)双端对齐、渲染组件(aimessage.tsx、aitooluse.tsx)按部件判别分发、流式协议(aisdk-streaming.md)增量驱动状态机,三者共同构成了可复用、可扩展、类型安全的 AI 聊天消息体系。

【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询