assistant-ui

assistant-ui (assistant-ui)

assistant-ui는 AI 채팅을 위한 헤드리스(headless) React UI 프레임워크예요. 스레드 관리, 메시지 분기, 첨부 파일 처리 같은 완전한 런타임 레이어를 제공하며, useExternalStoreRuntime 어댑터를 통해 LangChain의 useStream과 연결됩니다. 즉 assistant-ui의 강력한 런타임 기능을 그대로 둔 채, 백엔드와의 통신은 LangChain 에이전트가 담당하는 구조죠.

참고: 전체 assistant-ui 예제를 langgraphjs 예제 저장소에서 클론해서 실행해보면, useExternalStoreRuntime으로 LangChain 에이전트에 연결된 Claude 스타일 채팅 인터페이스를 직접 볼 수 있어요.

출처: LangChain 공식 문서 — assistant-ui

동작 방식 (How it works)

  1. useExternalStoreRuntime으로 어댑트BaseMessage[]ThreadMessageLike[]로 변환해 stream.messages를 assistant-ui의 런타임 형식에 브리징해요.
  2. 런타임 제공 — UI를 AssistantRuntimeProvider로 감싸고 아무 assistant-ui 스레드 컴포넌트를 렌더링합니다.

useStream 연결하기 (Wiring useStream)

useExternalStoreRuntime이 assistant-ui와 LangChain 스트림을 이어주는 핵심이에요. 입력 제출과 메시지 변환을 모두 여기서 처리합니다.

      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>({
    // ...
  });

메시지 변환 (Converting messages)

toThreadMessages는 LangChain BaseMessage[]를 assistant-ui가 기대하는 ThreadMessageLike[] 형식으로 매핑해요. human·AI·tool 각 메시지 타입을 처리하고, 콘텐츠 블록, 도구 호출, 추론 토큰도 변환합니다.

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 ?? "",
          // ...
        });
      }
      // ...
    }
  }
  return result;
}

모범 사례 (Best practices)

  • 스레드 영속성onThreadIdthreadId를 영속화하고, 페이지 로드 시 useStream에 다시 넘겨서 assistant-ui가 같은 스레드에 다시 연결되게 하세요.
  • 변환은 한 번에useMemo로 메시지 변환 결과를 기억해 불필요한 재변환을 피하세요.
  • 타입 분기는 isInstancegetType()보다 HumanMessage.isInstance / AIMessage.isInstance를 써서 TypeScript 내로잉을 제대로 활용하세요.

더 알아보기 (Learn more)