跳到主要内容

第 2 章 · 数据模型与运行时

本章讲什么: 先搞清「一条聊天消息在内存里长什么样」,再看 core 提供的几种运行时怎么选、适配器怎么把你的后端接上来。这是上层 UI 渲染什么、下层逻辑操作什么的共同语言。

2.1 一条消息 = 角色 + 一串「片段」

最容易误解的一点:assistant-ui 里一条消息不是一个字符串。它是一个 ThreadMessage——带角色,内容是一个异构片段数组

packages/core/src/types/message.ts:348-366ThreadAssistantMessage:它有 role: "assistant"content: readonly ThreadAssistantMessagePart[]statusmetadata。重点是 content 里那一串 part

assistant 消息能装哪些片段?见 message.ts:233-241ThreadAssistantMessagePart:

片段类型是什么
text普通文本(TextMessagePart)
reasoning模型的思考过程(ReasoningMessagePart)
tool-call一次工具调用(ToolCallMessagePart)
source引用来源(URL / 文档)
file / image文件 / 图片
data任意结构化数据
generative-ui一棵要渲染的 UI 组件树(见第 5 章)

为什么要拆片段? 因为一条 AI 回复天然是混合的:先来一段文字、中间插一次工具调用卡片、再接一段文字。把内容建模成有序片段数组,UI 就能逐片段、按类型渲染,且支持流式(片段一个个长出来)。

2.2 工具调用片段:agent 能力的载体

ToolCallMessagePart(message.ts:172-224)是这套数据模型里最丰富的一个,值得单独看,因为 agent 的「会用工具」全靠它:

  • toolCallId / toolName / args —— 模型要调哪个工具、传什么参数。注释强调流式期间 args部分解析,字段可能还没到齐(message.ts:181-187)。
  • result / isError —— 工具执行结果。
  • interrupt —— 需要人类输入才能继续(message.ts:202-203)。
  • approval —— 服务端审批门:在 approved 还没定、也没 resolution 之前,可以调 respondToApproval 批/拒(message.ts:204-216)。这是「人在环」的数据基础(见第 5 章)。
  • messages —— 这个工具调用产出的子线程消息,比如一个子 agent 的对话(message.ts:219-223)。

2.3 消息和片段的状态机

流式意味着「未完成」是常态,所以消息和片段都有 status:

  • 消息 MessageStatus(message.ts:270-292):runningrequires-action(等工具结果/人类输入)→ complete / incomplete(带原因:cancelled / length / content-filter / error…)。
  • 片段 MessagePartStatus(message.ts:243-259):running / complete / incomplete

requires-action 这个状态是 agent 工具循环的关键开关——第 3 章会看到 run loop 正是靠它决定「要不要再跑一轮」。

2.4 运行时:state 放哪里,决定你选哪种

core 不止一种运行时(packages/core/src/runtimes/)。AGENTS.md:29 给了选型规则,核心就一个问题:对话状态(消息列表)由谁拥有?

运行时状态归属什么时候用
local(useLocalRuntime)assistant-ui 自己持有消息列表后端管线程状态,只负责「给一轮 messages、回一轮流」。配一个 ChatModelAdapter 即可。
external-store(useExternalStoreRuntime)消息来自外部源,你给它一个数组你已经有自己的 state(LangGraph、Redux…),让 assistant-ui 从外部派生显示。
remote-thread-list在上面两者外再包一层需要多线程(线程列表、切换、历史)。

仓库的适配器编写规范(AGENTS.md:29-31)就是围绕「在 useExternalStoreRuntimeuseLocalRuntime + ChatModelAdapter 之上,再用 useRemoteThreadListRuntime 加多线程」来组织的。

2.5 ChatModelAdapter:接后端的最小契约

用 local runtime 时,你只需实现一个 ChatModelAdapter。它的契约小到只有一个 run 方法(packages/core/src/runtime/utils/chat-model-adapter.ts:59-64):

// 真实源码:chat-model-adapter.ts:59-64
export type ChatModelAdapter = {
run(
options: ChatModelRunOptions,
): Promise<ChatModelRunResult> | AsyncGenerator<ChatModelRunResult, void>;
};

两点要抓:

  1. 入参 ChatModelRunOptions(chat-model-adapter.ts:47-57)给你 messages(当前整条线程)、abortSignal(取消)、context(模型上下文,如可用工具)、还有 unstable_getMessage() 拿正在生成的那条。
  2. 返回值既可以是 Promise 也可以是 AsyncGenerator。返回 generator 就是流式:每 yield 一个 ChatModelRunResult(里面是累积到目前的 content),UI 就更新一次。这就是「逐字打印」从适配器侧看到的样子。

一个最小的流式适配器长这样:

// 示意,非源码:演示流式适配器的形状
const myAdapter: ChatModelAdapter = {
async *run({ messages, abortSignal }) {
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages }),
signal: abortSignal,
});
let text = "";
for await (const token of readTokens(res)) { // 你的流读取逻辑
text += token;
yield { content: [{ type: "text", text }] }; // 每次给「到目前为止」的全文
}
},
};

重点看:adapter 不关心 UI、不关心状态存储——它只把「一轮 messages」翻译成「一串 result」。状态怎么存、UI 怎么渲染,是上面几层的事。框架适配器(react-ai-sdk 等)本质就是替你写好了这个 run

2.6 适配器编写的几条硬规矩(防踩坑)

AGENTS.md 对写 react-* 适配器列了不少纪律,挑教学价值高的:

  • 复用 core 的 runtime cores,别自己造 *ThreadRuntimeCore 状态持有者(AGENTS.md:29AGENTS.md:37)。
  • 跨运行时保持能力对等: 给一个 runtime 加了某能力(如 onResume、动态建议),要么给所有支持该概念的 runtime 都加上,要么写下为什么分叉(AGENTS.md:41)。
  • 转换器要防御式: converter 和片段渲染器不准在字段缺失/undefined 时抛错,也别把浏览器专属 API(FileReader 等)塞进会在 Node / react-ink / RN 跑的路径(AGENTS.md:43)。

2.7 代码地图

主题文件符号
消息与片段数据模型packages/core/src/types/message.tsThreadMessageThreadAssistantMessagePartToolCallMessagePartMessageStatus
后端契约packages/core/src/runtime/utils/chat-model-adapter.tsChatModelAdapterChatModelRunOptionsChatModelRunResult
local 运行时选项packages/core/src/runtimes/local/local-runtime-options.tsLocalRuntimeOptionsBase
external-store 运行时packages/core/src/runtimes/external-store/external-store-runtime-core.tsthread-message-converter.ts
运行时装配入口packages/core/src/react/runtimes/useLocalRuntime.tsuseExternalStoreRuntime.tsuseRemoteThreadListRuntime.ts