跳到主要内容

assistant-ui — 架构与原理

30 秒导读: assistant-ui 是一个 React 库,帮你在自己的应用里搭出「ChatGPT 那种聊天界面」——流式打字、自动滚动、重试、附件、Markdown、工具调用渲染、语音、键盘快捷键全都开箱即用。但它最核心、最值得读的不是这些功能,而是它底层的架构:它把整个聊天运行时写成一套「无头的 React hooks」(可以脱离 React 树跑的响应式资源),再把 UI 拆成一堆能任意拼装的零件。

1. 这是什么(零基础也能懂)

  • 一句话定义: assistant-ui 是一个开源的 TypeScript/React 库,用来在你自己的 web 应用里构建生产级的 AI 聊天界面。

  • 解决什么问题 / 给谁用: 假设你做了一个 AI 后端(接 OpenAI、Anthropic、或自己的 agent),现在要给它配一个前端聊天窗口。你很快会发现「聊天 UI」远不止一个输入框加一串气泡:要处理流式逐字打印、消息还在生成时就要边来边渲染、用户点重试要分支、模型调用工具时要把工具调用画成卡片、长对话要自动滚到底……每一项单独做都不难,凑齐且不互相打架就很烦。assistant-ui 把这些都做好了,给需要做 AI 聊天前端的 React 工程师用。

  • 它能做什么(功能):

    • 流式输出、自动滚动、重试、分支(同一句话的多个回答之间切换)
    • 附件上传、Markdown 渲染、代码高亮、语音听写、键盘快捷键、无障碍
    • 工具调用 / JSON 渲染成 React 组件(生成式 UI)
    • 人在环审批(模型要做某个动作前,先弹给用户批准)
    • 一行换一个后端:Vercel AI SDK、LangGraph、AG-UI、自定义 HTTP
  • 用起来什么样: 最小用法就是「拿一个 runtime + 摆一个 <Thread />」:

// 示意,接近 README 真实用法
"use client";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
import { Thread } from "@/components/assistant-ui/thread";

export function Chat() {
const runtime = useChatRuntime(); // 连到某个后端
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread /> {/* 一整套聊天 UI */}
</AssistantRuntimeProvider>
);
}

useChatRuntime 换成 useLangGraphRuntimeuseDataStreamRuntime 或自己写的 runtime,UI 一行都不用动——这正是它的核心设计哲学(见 README)。

  • 一句话直觉/类比: 把它想成聊天 UI 界的 Radix——不是给你一个黑盒大组件,而是给你一堆没样式的、可组合的零件(ThreadMessageComposerThreadListActionBar…),你把它们拼起来、自己上样式;状态怎么流转、跟后端怎么对接,由它底下的「运行时」负责。

2. 顶层全景(它大概怎么转)

assistant-ui 是一个 monorepo,有 40 多个包(packages/)。但理解它只需要抓住一条主干:从最底层的响应式原语,一层层往上盖到你看到的聊天界面。

2.1 分层骨架(怎么读这张图:从下往上是「地基 → 数据 → 桥接 → 界面」)

你的 React 应用
└─ <AssistantRuntimeProvider runtime={...}><Thread/></...>
│ 摆零件、上样式

┌─────────────────────────────────────────────────┐
│ 第④层 primitives(无头 UI 零件) │ packages/react
│ ThreadPrimitive / MessagePrimitive / Composer… │ + core/src/react
│ 只管「渲染什么」,样式你来 │
└─────────────────────────────────────────────────┘
▲ 用 useAui / useAuiState 读状态
┌─────────────────────────────────────────────────┐
│ 第③层 store(把响应式资源桥接进 React) │ @assistant-ui/store
│ AssistantClient · useAui · useAuiState │
└─────────────────────────────────────────────────┘
▲ 订阅
┌─────────────────────────────────────────────────┐
│ 第②层 core(框架无关的聊天运行时) │ @assistant-ui/core
│ ThreadMessage 数据模型 · run loop · 适配器 │
│ local / external-store / remote-thread-list │
└─────────────────────────────────────────────────┘
▲ 用「无头 hooks」搭出来
┌─────────────────────────────────────────────────┐
│ 第①层 tap(地基:headless React hooks) │ @assistant-ui/tap
│ resource · useState/useEffect 跑在 React 树外 │
└─────────────────────────────────────────────────┘

这套分层在仓库自己的 AGENTS.md:3-13 里写得很清楚。

2.2 五个核心部件,各干什么

部件它是什么 / 干什么在哪个包
tap把 React 的 useState/useEffect 等抽出来,做成能脱离 React 树跑的「响应式资源(resource)」。整个运行时的地基。packages/tap
store把 tap 资源桥接进 React:暴露 AssistantClient(动作)、useAui/useAuiState(读状态)。packages/store
core框架无关的聊天运行时:定义 ThreadMessage 数据模型,跑「发消息 → 流式回复 → 工具循环」的 run loop,提供 local/external-store 等运行时和一堆适配器。packages/core
reactweb 发行版:re-export core,并补上一整套基于 Radix 风格的无头 primitives。用户实际安装的就是它(@assistant-ui/react)。packages/react
react-* 适配器把某个后端 SDK(Vercel AI SDK / LangGraph / AG-UI …)映射到 core 的运行时上。packages/react-ai-sdk

还有一个旁支地基 assistant-stream(packages/assistant-stream):专门负责「把后端传来的流式 chunk 累积成一条完整消息」。它服务于第②层。

2.3 主线走一遍(高层,不进代码)

以「用户敲一句话、按发送」为例,数据这样流:

用户在 Composer 里打字、点发送
│ Composer primitive 调 aui.composer().send()

core 的运行时把这句话加进 thread,新建一条 assistant 消息(status: running)


进入 run loop:调用 ChatModelAdapter.run({ messages, ... })
│ 适配器(如 react-ai-sdk)真正打后端、拿到流式响应

流式 chunk 一片片回来 → 累积进 assistant 消息的 content
│ 每来一片就通知订阅者

store 收到通知 → useAuiState 选中的那一小块状态变了 → primitive 重渲染


屏幕上文字逐字冒出来;若模型要调工具,run loop 自动跑下一轮

这条主线的两个关键环节——run loop / 工具循环流式累积——在 03-run-loop-and-streaming.md 详讲。

3. 阅读地图(建议顺序)

这个项目复杂度高,拆成了 5 章,由浅入深排好了:

  1. 01-tap-headless-hooks.md —— 先读这章。tap 是整个项目最新颖、最聪明的一招:让 useState/useEffect 在没有 React 树的地方也能跑。读懂它,上面几层才不显得魔法。
  2. 02-data-model-and-runtime.md —— 聊天到底用什么数据结构表示(ThreadMessage 和它的「消息片段」),以及 core 提供的三类运行时怎么选。
  3. 03-run-loop-and-streaming.md —— 一次对话从发送到结束的完整生命周期:run loop、工具调用循环(agent 的核心)、流式增量累积。
  4. 04-store-and-client.md —— 上层 UI 怎么读到下层状态:AssistantClientuseAuiuseAuiState 的设计。
  5. 05-primitives-and-genui.md —— 你最终摆在页面上的那些零件,以及生成式 UI、人在环审批这两个高级能力。

如果你只想抓重点:读第 1 章(地基的巧思)+ 第 3 章(agent 的核心循环),就抓住了这个项目 80% 的精华。

4. 巧妙之处(先剧透,细节在各章)

  • 无头 hooks(tap): 不发明新的状态系统,而是复用 React 的 hooks 心智模型,只是让它在 React 树外也能跑。运行时逻辑因此能用大家都熟的 useState/useEffect/useMemo 写,还能被 oxlint 的 react/exhaustive-deps 检查(AGENTS.md:66-67)。
  • 框架无关的 core: 聊天逻辑全在 @assistant-ui/core,React / React Native / Ink(终端)三个发行版共用同一套核心(AGENTS.md:6-9)。
  • append-only 公共 API: 已发布的导出只增不减,哪怕看着没人用的类型也不能删——因为仓库内审计看不到 npm 上的消费者(AGENTS.md:21)。
  • 组合式而非黑盒: Radix 风格,primitive 不带样式,你「自己控制每一个像素」(README「Customization」一节)。

5. 边界与局限(诚实)

  • 它是前端库:不替你跑模型、不替你管密钥。模型/agent 在你的后端,assistant-ui 通过适配器对接。
  • core 框架无关,但你仍然在 React 生态里(三个发行版都是 React 系:web / native / ink)。
  • 有一个正在进行的迁移:旧的 legacy runtime(packages/react/src/legacy-runtime/)正被新的 tap-only 架构(core/src/react)取代;迁移期间 @assistant-ui/react 的 barrel 同时 re-export 新旧两套(AGENTS.md:23)。读源码时会看到两套并存。

6. 代码地图(导航索引)

想看什么文件关键符号
分层架构权威说明AGENTS.md(Architecture 一节)
无头 hooks 地基packages/tap/src/index.tsresourceuseResourcecreateTapRoot
聊天数据模型packages/core/src/types/message.tsThreadMessageThreadAssistantMessagePartToolCallMessagePart
模型适配器契约packages/core/src/runtime/utils/chat-model-adapter.tsChatModelAdapterChatModelRunResult
核心 run loop / 工具循环packages/core/src/runtimes/local/local-thread-runtime-core.ts_runLoopstartRunshouldContinue
读状态的入口packages/store/src/useAui.tsuseAuiState.tsuseAuiuseAuiStateAssistantClient
UI 零件packages/react/src/primitives/ThreadPrimitiveMessagePrimitiveComposerPrimitive
装载运行时packages/core/src/react/AssistantRuntimeProvider.tsxAssistantRuntimeProvider