Skip to main content
생성형 사용자 인터페이스(Generative UI)는 에이전트가 텍스트를 넘어 풍부한 사용자 인터페이스를 생성할 수 있게 해줍니다. 이를 통해 대화 흐름과 AI 응답에 따라 UI가 적응하는, 더 인터랙티브하고 컨텍스트를 인식하는 애플리케이션을 만들 수 있습니다. 예약/숙박에 대한 프롬프트와 UI 컴포넌트로 인라인 렌더링된 호텔 목록 카드(이미지, 제목, 가격, 위치) 세트를 보여주는 에이전트 채팅 LangSmith는 React 컴포넌트를 그래프 코드와 함께 배치할 수 있도록 지원합니다. 이를 통해 그래프에 특화된 UI 컴포넌트를 구축하는 데 집중하면서, Agent Chat과 같은 기존 채팅 인터페이스에 쉽게 연결하고 실제로 필요할 때만 코드를 로드할 수 있습니다.

튜토리얼

1. UI 컴포넌트 정의 및 구성

먼저 첫 번째 UI 컴포넌트를 생성합니다. 각 컴포넌트에는 그래프 코드에서 컴포넌트를 참조하는 데 사용될 고유 식별자를 제공해야 합니다.
src/agent/ui.tsx
const WeatherComponent = (props: { city: string }) => {
  return <div>Weather for {props.city}</div>;
};

export default {
  weather: WeatherComponent,
};
다음으로, langgraph.json 구성에서 UI 컴포넌트를 정의합니다:
{
  "node_version": "20",
  "graphs": {
    "agent": "./src/agent/index.ts:graph"
  },
  "ui": {
    "agent": "./src/agent/ui.tsx"
  }
}
ui 섹션은 그래프에서 사용할 UI 컴포넌트를 가리킵니다. 기본적으로 그래프 이름과 동일한 키를 사용하는 것을 권장하지만, 원하는 방식으로 컴포넌트를 분할할 수 있습니다. 자세한 내용은 UI 컴포넌트의 네임스페이스 사용자 정의를 참조하세요. LangSmith는 UI 컴포넌트 코드와 스타일을 자동으로 번들링하고 LoadExternalComponent 컴포넌트로 로드할 수 있는 외부 자산으로 제공합니다. reactreact-dom과 같은 일부 종속성은 번들에서 자동으로 제외됩니다. CSS와 Tailwind 4.x도 기본적으로 지원되므로, UI 컴포넌트에서 Tailwind 클래스와 shadcn/ui를 자유롭게 사용할 수 있습니다.
  • src/agent/ui.tsx
  • src/agent/styles.css
import "./styles.css";

const WeatherComponent = (props: { city: string }) => {
  return <div className="bg-red-500">Weather for {props.city}</div>;
};

export default {
  weather: WeatherComponent,
};

2. 그래프에서 UI 컴포넌트 전송

  • Python
  • JS
src/agent.py
import uuid
from typing import Annotated, Sequence, TypedDict

from langchain.messages import AIMessage, BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.graph.ui import AnyUIMessage, ui_message_reducer, push_ui_message


class AgentState(TypedDict):  # noqa: D101
    messages: Annotated[Sequence[BaseMessage], add_messages]
    ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]


async def weather(state: AgentState):
    class WeatherOutput(TypedDict):
        city: str

    weather: WeatherOutput = (
        await ChatOpenAI(model="gpt-4o-mini")
        .with_structured_output(WeatherOutput)
        .with_config({"tags": ["nostream"]})
        .ainvoke(state["messages"])
    )

    message = AIMessage(
        id=str(uuid.uuid4()),
        content=f"Here's the weather for {weather['city']}",
    )

    # 메시지와 연결된 UI 요소 발송
    push_ui_message("weather", weather, message=message)
    return {"messages": [message]}


workflow = StateGraph(AgentState)
workflow.add_node(weather)
workflow.add_edge("__start__", "weather")
graph = workflow.compile()

3. React 애플리케이션에서 UI 요소 처리

클라이언트 측에서 useStream()LoadExternalComponent를 사용하여 UI 요소를 표시할 수 있습니다.
src/app/page.tsx
"use client";

import { useStream } from "@langchain/langgraph-sdk/react";
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";

export default function Page() {
  const { thread, values } = useStream({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
  });

  return (
    <div>
      {thread.messages.map((message) => (
        <div key={message.id}>
          {message.content}
          {values.ui
            ?.filter((ui) => ui.metadata?.message_id === message.id)
            .map((ui) => (
              <LoadExternalComponent key={ui.id} stream={thread} message={ui} />
            ))}
        </div>
      ))}
    </div>
  );
}
내부적으로 LoadExternalComponent는 LangSmith에서 UI 컴포넌트의 JS와 CSS를 가져와 shadow DOM에서 렌더링하여, 애플리케이션의 나머지 부분과 스타일 격리를 보장합니다.

사용 가이드

클라이언트 측에서 커스텀 컴포넌트 제공

이미 클라이언트 애플리케이션에 컴포넌트가 로드되어 있는 경우, LangSmith에서 UI 코드를 가져오지 않고 직접 렌더링할 컴포넌트 맵을 제공할 수 있습니다.
const clientComponents = {
  weather: WeatherComponent,
};

<LoadExternalComponent
  stream={thread}
  message={ui}
  components={clientComponents}
/>;

컴포넌트 로딩 중 로딩 UI 표시

컴포넌트가 로드되는 동안 렌더링할 대체 UI를 제공할 수 있습니다.
<LoadExternalComponent
  stream={thread}
  message={ui}
  fallback={<div>Loading...</div>}
/>

UI 컴포넌트의 네임스페이스 사용자 정의

기본적으로 LoadExternalComponentuseStream() 훅의 assistantId를 사용하여 UI 컴포넌트 코드를 가져옵니다. LoadExternalComponent 컴포넌트에 namespace prop을 제공하여 이를 사용자 정의할 수 있습니다.
  • src/app/page.tsx
  • langgraph.json
<LoadExternalComponent
  stream={thread}
  message={ui}
  namespace="custom-namespace"
/>

UI 컴포넌트에서 스레드 상태 액세스 및 상호 작용

useStreamContext 훅을 사용하여 UI 컴포넌트 내부에서 스레드 상태에 액세스할 수 있습니다.
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";

const WeatherComponent = (props: { city: string }) => {
  const { thread, submit } = useStreamContext();
  return (
    <>
      <div>Weather for {props.city}</div>

      <button
        onClick={() => {
          const newMessage = {
            type: "human",
            content: `What's the weather in ${props.city}?`,
          };

          submit({ messages: [newMessage] });
        }}
      >
        Retry
      </button>
    </>
  );
};

클라이언트 컴포넌트에 추가 컨텍스트 전달

LoadExternalComponent 컴포넌트에 meta prop을 제공하여 클라이언트 컴포넌트에 추가 컨텍스트를 전달할 수 있습니다.
<LoadExternalComponent stream={thread} message={ui} meta={{ userId: "123" }} />
그런 다음 useStreamContext 훅을 사용하여 UI 컴포넌트에서 meta prop에 액세스할 수 있습니다.
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";

const WeatherComponent = (props: { city: string }) => {
  const { meta } = useStreamContext<
    { city: string },
    { MetaType: { userId?: string } }
  >();

  return (
    <div>
      Weather for {props.city} (user: {meta?.userId})
    </div>
  );
};

서버에서 UI 메시지 스트리밍

useStream() 훅의 onCustomEvent 콜백을 사용하여 노드 실행이 완료되기 전에 UI 메시지를 스트리밍할 수 있습니다. 이는 LLM이 응답을 생성하는 동안 UI 컴포넌트를 업데이트할 때 특히 유용합니다.
import { uiMessageReducer } from "@langchain/langgraph-sdk/react-ui";

const { thread, submit } = useStream({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  onCustomEvent: (event, options) => {
    options.mutate((prev) => {
      const ui = uiMessageReducer(prev.ui ?? [], event);
      return { ...prev, ui };
    });
  },
});
그런 다음 업데이트하려는 UI 메시지와 동일한 ID로 ui.push() / push_ui_message()를 호출하여 UI 컴포넌트에 업데이트를 푸시할 수 있습니다.
  • Python
  • JS
  • ui.tsx
from typing import Annotated, Sequence, TypedDict

from langchain_anthropic import ChatAnthropic
from langchain.messages import AIMessage, AIMessageChunk, BaseMessage
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.graph.ui import AnyUIMessage, push_ui_message, ui_message_reducer


class AgentState(TypedDict):  # noqa: D101
    messages: Annotated[Sequence[BaseMessage], add_messages]
    ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]


class CreateTextDocument(TypedDict):
    """사용자를 위한 문서 제목을 준비합니다."""

    title: str


async def writer_node(state: AgentState):
    model = ChatAnthropic(model="claude-3-5-sonnet-latest")
    message: AIMessage = await model.bind_tools(
        tools=[CreateTextDocument],
        tool_choice={"type": "tool", "name": "CreateTextDocument"},
    ).ainvoke(state["messages"])

    tool_call = next(
        (x["args"] for x in message.tool_calls if x["name"] == "CreateTextDocument"),
        None,
    )

    if tool_call:
        ui_message = push_ui_message("writer", tool_call, message=message)
        ui_message_id = ui_message["id"]

        # UI 메시지를 통해 이미 LLM 응답을 클라이언트로 스트리밍하고 있으므로
        # `messages` 스트림 모드로 다시 스트리밍할 필요가 없습니다.
        content_stream = model.with_config({"tags": ["nostream"]}).astream(
            f"Create a document with the title: {tool_call['title']}"
        )

        content: AIMessageChunk | None = None
        async for chunk in content_stream:
            content = content + chunk if content else chunk

            push_ui_message(
                "writer",
                {"content": content.text()},
                id=ui_message_id,
                message=message,
                # `merge=True`를 사용하여 기존 UI 메시지와 props를 병합합니다
                merge=True,
            )

    return {"messages": [message]}

상태에서 UI 메시지 제거

RemoveMessage를 추가하여 상태에서 메시지를 제거할 수 있는 것처럼, UI 메시지의 ID로 remove_ui_message / ui.delete를 호출하여 상태에서 UI 메시지를 제거할 수 있습니다.
  • Python
  • JS
from langgraph.graph.ui import push_ui_message, delete_ui_message

# 메시지 푸시
message = push_ui_message("weather", {"city": "London"})

# 해당 메시지 제거
delete_ui_message(message["id"])

더 알아보기


Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I