assistant-ui 是一个用于 AI 聊天的 headless React UI 框架。它提供完整运行时层,包括线程管理、消息分支和附件处理,并通过 useExternalStoreRuntime 适配器连接到 useStream
克隆并运行完整 assistant-ui 示例,查看如何用 useExternalStoreRuntime 将 Claude 风格聊天界面连接到 LangChain 代理。

工作方式

  1. 使用 useStream 流式传输:连接到代理,并获取响应式消息、加载状态以及提交/取消回调
  2. 使用 useExternalStoreRuntime 适配:将 BaseMessage[] 转换为 ThreadMessageLike[],从而把 stream.messages 桥接到 assistant-ui 的运行时格式
  3. 提供运行时:用 AssistantRuntimeProvider 包裹 UI,并渲染任何 assistant-ui 线程组件

安装

bun add @assistant-ui/react @assistant-ui/react-markdown

连接 useStream

useExternalStoreRuntime 适配器会将 stream.messages 桥接到 assistant-ui 运行时。将它传给 AssistantRuntimeProvider,然后渲染任何线程组件:
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],
  );

  // Convert LangChain messages to assistant-ui's ThreadMessageLike format
  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[] 格式。处理每种消息类型,包括 human、AI 和 tool,并转换内容块、工具调用和推理 token:
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"] = [];

      // Reasoning tokens
      const reasoning = msg.contentBlocks.find((block) => block.type === "reasoning")?.reasoning;
      if (reasoning) parts.push({ type: "reasoning", text: reasoning });

      // Tool calls
      for (const tc of msg.tool_calls ?? []) {
        parts.push({
          type: "tool-call",
          toolCallId: tc.id ?? "",
          toolName: tc.name,
          args: tc.args,
        });
      }

      // Text response
      const text = msg.text;
      if (text) parts.push({ type: "text", text });

      result.push({ role: "assistant", content: parts });
    } else if (ToolMessage.isInstance(msg)) {
      // Attach tool results to the preceding assistant message
      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,包括消息列表、composer 和滚动管理。通过覆盖组件 slots 可以自定义各个部分:
import { Thread, ThreadMessages, Composer } from "@assistant-ui/react";

function CustomThread() {
  return (
    <Thread.Root>
      <ThreadMessages
        components={{
          UserMessage: MyUserMessage,
          AssistantMessage: MyAssistantMessage,
          ToolFallback: MyToolCard,
        }}
      />
      <Composer />
    </Thread.Root>
  );
}

最佳实践

  • 记忆化消息转换:用 useMemo 包裹 toThreadMessages(stream.messages),避免每次渲染都重新运行转换
  • 处理附件:使用 CompositeAttachmentAdapter 搭配 SimpleImageAttachmentAdapter 处理图片上传;如需文件支持,可用自定义适配器扩展
  • 使用分支:assistant-ui 通过 MessageBranch 内置支持消息分支;当你需要 LangGraph checkpoint fork 时,将编辑与 useMessageMetadataforkFrom 搭配使用
  • 线程持久化:使用 onThreadId 持久化 threadId,并在页面加载时将其传回 useStream,让 assistant-ui 重新连接到同一线程