Skip to content

如何使用 LangGraph 实现生成式用户界面

生成式用户界面(Generative UI)允许智能体超越文本,生成丰富的用户界面。这使得能够创建更具交互性和上下文感知的应用程序,其中用户界面会根据对话流程和人工智能响应进行自适应调整。

生成式用户界面示例

LangGraph 平台支持将你的 React 组件与图代码放在一起。这使你可以专注于为你的图构建特定的用户界面组件,同时轻松接入现有的聊天界面,如 智能体聊天,并仅在实际需要时加载代码。

教程

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 组件的命名空间

LangGraph 平台将自动打包你的 UI 组件代码和样式,并将它们作为外部资源提供,这些资源可以由 LoadExternalComponent 组件加载。一些依赖项(如 reactreact-dom)将自动从打包文件中排除。

CSS 和 Tailwind 4.x 也支持开箱即用,因此你可以在 UI 组件中自由使用 Tailwind 类以及 shadcn/ui

import "./styles.css";

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

export default {
  weather: WeatherComponent,
};
@import "tailwindcss";

2. 在图形中发送 UI 组件

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

from langchain_core.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()

使用 typedUi 工具从你的代理节点发出 UI 元素:

src/agent/index.ts
import {
  typedUi,
  uiMessageReducer,
} from "@langchain/langgraph-sdk/react-ui/server";

import { ChatOpenAI } from "@langchain/openai";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";

import type ComponentMap from "./ui.js";

import {
  Annotation,
  MessagesAnnotation,
  StateGraph,
  type LangGraphRunnableConfig,
} from "@langchain/langgraph";

const AgentState = Annotation.Root({
  ...MessagesAnnotation.spec,
  ui: Annotation({ reducer: uiMessageReducer, default: () => [] }),
});

export const graph = new StateGraph(AgentState)
  .addNode("weather", async (state, config) => {
    // 提供组件映射的类型以确保
    // `ui.push()` 调用的类型安全,以及
    // 将消息推送到 `ui` 并发送自定义事件。
    const ui = typedUi<typeof ComponentMap>(config);

    const weather = await new ChatOpenAI({ model: "gpt-4o-mini" })
      .withStructuredOutput(z.object({ city: z.string() }))
      .withConfig({ tags: ["nostream"] })
      .invoke(state.messages);

    const response = {
      id: uuidv4(),
      type: "ai",
      content: `Here's the weather for ${weather.city}`,
    };

    // 发出与 AI 消息关联的 UI 元素
    ui.push({ name: "weather", props: weather }, { message: response });

    return { messages: [response] };
  })
  .addEdge("__start__", "weather")
  .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 将从 LangGraph 平台获取 UI 组件的 JS 和 CSS,并在影子 DOM 中渲染它们,从而确保与应用程序的其余部分实现样式隔离。

操作指南

在组件加载时显示加载界面

你可以提供一个备用界面,在组件加载时进行渲染。

<LoadExternalComponent
  stream={thread}
  message={ui}
  fallback={<div>Loading...</div>}
/>

在客户端提供自定义组件

如果你已经在客户端应用程序中加载了组件,可以提供一个此类组件的映射,这样就无需从 LangGraph 平台获取 UI 代码,直接进行渲染。

const clientComponents = {
  weather: WeatherComponent,
};

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

自定义 UI 组件的命名空间

默认情况下,LoadExternalComponent 会使用 useStream() 钩子中的 assistantId 来获取 UI 组件的代码。你可以通过为 LoadExternalComponent 组件提供 namespace 属性来进行自定义。

<LoadExternalComponent
  stream={thread}
  message={ui}
  namespace="custom-namespace"
/>
{
  "ui": {
    "custom-namespace": "./src/agent/ui.tsx"
  }
}

从 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 属性,向客户端组件传递额外的上下文。

<LoadExternalComponent stream={thread} message={ui} meta={{ userId: "123" }} />

然后,你可以在 UI 组件中使用 useStreamContext 钩子来访问 meta 属性。

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。

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 消息

类似于通过追加 RemoveMessage 从状态中移除消息,你可以通过调用 remove_ui_message / ui.delete 并传入 UI 消息的 ID,从状态中移除 UI 消息。

from langgraph.graph.ui import push_ui_message, delete_ui_message

# push message
message = push_ui_message("weather", {"city": "London"})

# remove said message
delete_ui_message(message["id"])
// push message
const message = ui.push({ name: "weather", props: { city: "London" } });

// remove said message
ui.delete(message.id);

了解更多

Comments