外观
assistant-ui 是一个用于 AI 聊天的无头(headless)React UI 框架。它提供完整的运行时层——线程管理、消息分支、附件处理——通过 useExternalStoreRuntime 适配器连接到 useStream。
import { ExampleEmbed } from "/snippets/example-embed.jsx"
TIP
克隆并运行完整的 assistant-ui 示例,以查看通过 useExternalStoreRuntime 连接到 LangChain 智能体的 Claude 风格聊天界面。
工作原理
- 使用
useStream进行流式输出 — 连接到你的智能体并获取响应式消息、加载状态以及提交/取消回调 - 使用
useExternalStoreRuntime进行适配 — 通过将BaseMessage[]转换为ThreadMessageLike[],把stream.messages桥接到 assistant-ui 的运行时格式 - 提供运行时 — 将你的 UI 包裹在
AssistantRuntimeProvider中,并渲染任意 assistant-ui 线程组件
安装
bash
bun add @assistant-ui/react @assistant-ui/react-markdown连接 useStream
useExternalStoreRuntime 适配器将 stream.messages 桥接到 assistant-ui 运行时。将它传给 AssistantRuntimeProvider 并渲染任意线程组件:
tsx
import { useCallback, useMemo } from "react";
import {
AssistantRuntimeProvider,
useExternalStoreRuntime,
type AppendMessage,
type ThreadMessageLike,
} from "@assistant-ui/react";
import { useStream } from "@langchain/react";
import { Thread } from "@assistant-ui/react";
export function Chat() {
const stream = useStream({
apiUrl: "http://localhost:2024",
assistantId: "claude",
});
const onNew = useCallback(
async (message: AppendMessage) => {
const text = message.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("");
await stream.submit({ messages: [{ type: "human", content: text }] });
},
[stream],
);
// 将 LangChain 消息转换为 assistant-ui 的 ThreadMessageLike 格式
const messages = useMemo(
() => toThreadMessages(stream.messages),
[stream.messages],
);
const runtime = useExternalStoreRuntime<ThreadMessageLike>({
messages,
onNew,
onCancel: () => stream.stop(),
convertMessage: (m) => m,
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
</AssistantRuntimeProvider>
);
}转换消息
toThreadMessages 将 LangChain BaseMessage[] 映射为 assistant-ui 所期望的 ThreadMessageLike[] 格式。处理每种消息类型——人类、AI 和工具——并转换内容块、工具调用和推理 token:
tsx
import { AIMessage, HumanMessage, ToolMessage, type BaseMessage } from "langchain";
import type { ThreadMessageLike } from "@assistant-ui/react";
export function toThreadMessages(messages: BaseMessage[]): ThreadMessageLike[] {
const result: ThreadMessageLike[] = [];
for (const msg of messages) {
if (HumanMessage.isInstance(msg)) {
result.push({
role: "user",
content: [{ type: "text", text: msg.text }],
});
} else if (AIMessage.isInstance(msg)) {
const parts: ThreadMessageLike["content"] = [];
// 推理 token
const reasoning = msg.contentBlocks.find((block) => block.type === "reasoning")?.reasoning;
if (reasoning) parts.push({ type: "reasoning", text: reasoning });
// 工具调用
for (const tc of msg.tool_calls ?? []) {
parts.push({
type: "tool-call",
toolCallId: tc.id ?? "",
toolName: tc.name,
args: tc.args,
});
}
// 文本响应
const text = msg.text;
if (text) parts.push({ type: "text", text });
result.push({ role: "assistant", content: parts });
} else if (ToolMessage.isInstance(msg)) {
// 将工具结果附加到前面的助手消息上
const last = result[result.length - 1];
if (last?.role === "assistant") {
for (const part of last.content) {
if (
part.type === "tool-call" &&
part.toolCallId === msg.tool_call_id
) {
(part as { result?: string }).result = msg.text;
}
}
}
}
}
return result;
}自定义线程 UI
<Thread /> 自带完整的默认线程 UI,包括消息列表、输入框和滚动管理。通过覆盖组件插槽来自定义各个部分:
tsx
import { Thread, ThreadMessages, Composer } from "@assistant-ui/react";
function CustomThread() {
return (
<Thread.Root>
<ThreadMessages
components={{
UserMessage: MyUserMessage,
AssistantMessage: MyAssistantMessage,
ToolFallback: MyToolCard,
}}
/>
<Composer />
</Thread.Root>
);
}最佳实践
- 记忆化消息转换: 将
toThreadMessages(stream.messages)包裹在useMemo中,以避免在每次渲染时重新执行转换 - 处理附件: 对图片上传使用
CompositeAttachmentAdapter搭配SimpleImageAttachmentAdapter;对文件使用自定义适配器进行扩展 - 使用分支: assistant-ui 通过
MessageBranch内置了消息分支支持;当你需要 LangGraph 检查点分支时,可将编辑与useMessageMetadata和forkFrom配合使用 - 线程持久化: 使用
onThreadId持久化threadId,并在页面加载时将其传回useStream,以便 assistant-ui 重新连接到同一线程