跳到内容

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

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

Generative UI Sample

LangGraph 平台支持将您的 React 组件与您的图代码并置。这使您可以专注于为您的图构建特定的 UI 组件,同时轻松插入现有的聊天界面(如 Agent Chat),并在实际需要时才加载代码。

仅限 LangGraph.js

目前只有 LangGraph.js 支持生成式 UI。对 Python 的支持即将推出。

教程

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 组件

使用 typedUi 实用程序从您的 agent 节点发出 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) => {
    // Provide the type of the component map to ensure
    // type safety of `ui.push()` calls as well as
    // pushing the messages to the `ui` and sending a custom event as well.
    const ui = typedUi<typeof ComponentMap>(config);

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

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

    // Emit UI elements with associated AI message
    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: "https://127.0.0.1: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,并在 shadow DOM 中渲染它们,从而确保样式与应用程序其余部分隔离。

操作指南

在组件加载时显示加载 UI

您可以提供一个回退 UI,以便在组件加载时渲染。

<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() hook 的 assistantId 来获取 UI 组件的代码。您可以通过为 LoadExternalComponent 组件提供 namespace 属性来自定义此设置。

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

从 UI 组件访问线程状态并与之交互

您可以使用 useStreamContext hook 在 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" }} />

然后,您可以使用 useStreamContext hook 在 UI 组件中访问 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() hook 的 onCustomEvent 回调在节点执行完成之前流式传输 UI 更新。

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

const { thread, submit } = useStream({
  apiUrl: "https://127.0.0.1:2024",
  assistantId: "agent",
  onCustomEvent: (event, options) => {
    options.mutate((prev) => {
      const ui = uiMessageReducer(prev.ui ?? [], event);
      return { ...prev, ui };
    });
  },
});

从状态中移除 UI 消息

类似于如何通过附加 RemoveMessage 从状态中移除消息,您可以通过使用 UI 消息的 ID 调用 ui.delete 从状态中移除 UI 消息。

// pushed message
const message = ui.push({ name: "weather", props: { city: "London" } });

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

// return new state to persist changes
return { ui: ui.items };

了解更多