【LangGraph】Implement generative UI with LangGraph:用 LangGraph 实现生成用户界面

如何使用 LangGraph 实现生成用户界面(How to implement Generative User Interfaces with LangGraph)

Prerequisites

生成用户界面(Generative UI)允许代理超越文本,生成丰富的用户界面。这使得创建更具交互性和上下文感知的应用成为可能,其中 UI 根据对话流程和 AI 响应动态适应。

在这里插入图片描述

LangGraph 平台支持将你的 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 组件命名空间

LangGraph 平台会自动打包你的 UI 组件代码和样式,并将其作为外部资产提供,可通过 LoadExternalComponent 组件加载。某些依赖(如 reactreact-dom)会自动从打包中排除。

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

  • src/agent/ui.tsx(带样式)

    import "./styles.css";
    
    const WeatherComponent = (props: { city: string }) => {
      return <div className="bg-red-500">Weather for {props.city}</div>;
    };
    
    export default {
      weather: WeatherComponent,
    };
    
  • styles.css

    @import "tailwindcss";
    

2. 在图中发送 UI 组件

在图代码中,使用 push_ui_message(Python)或 ui.push(TypeScript)将 UI 组件与消息关联发送。

  • (Python)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()
    

在 TypeScript 中,使用 typedUi 工具确保 UI 元素推送的类型安全:

  • (TypeScript)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 组件的 JavaScript 和 CSS 代码,并在一个影子 DOM 中渲染它们,从而确保与应用其余部分的样式隔离。

操作指南

在客户端提供自定义组件

如果你已经在客户端应用中加载了组件,可以提供一个组件映射,直接渲染而无需从 LangGraph 平台获取 UI 代码。

const clientComponents = {
  weather: WeatherComponent,
};

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

在组件加载时显示加载 UI

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

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

自定义 UI 组件的命名空间

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

  • src/app/page.tsx
    <LoadExternalComponent
      stream={thread}
      message={ui}
      namespace="custom-namespace"
    />
    
  • langgraph.json
    {
      "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" }} />

然后,你可以通过 useStreamContext 钩子在 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() 钩子的 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.push()(TypeScript)或 push_ui_message()(Python)并使用与要更新的 UI 消息相同的 ID 来推送 UI 组件的更新。

Python 示例

from typing import Annotated, Sequence, TypedDict

from langchain_anthropic import ChatAnthropic
from langchain_core.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 消息合并
                merge=True,
            )

    return {"messages": [message]}

TypeScript 示例

import {
  Annotation,
  MessagesAnnotation,
  type LangGraphRunnableConfig,
} from "@langchain/langgraph";
import { z } from "zod";
import { ChatAnthropic } from "@langchain/anthropic";
import {
  typedUi,
  uiMessageReducer,
} from "@langchain/langgraph-sdk/react-ui/server";
import type { AIMessageChunk } from "@langchain/core/messages";

import type ComponentMap from "./ui";

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

async function writerNode(
  state: typeof AgentState.State,
  config: LangGraphRunnableConfig
): Promise<typeof AgentState.Update> {
  const ui = typedUi<typeof ComponentMap>(config);

  const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" });
  const message = await model
    .bindTools(
      [
        {
          name: "create_text_document",
          description: "Prepare a document heading for the user.",
          schema: z.object({ title: z.string() }),
        },
      ],
      { tool_choice: { type: "tool", name: "create_text_document" } }
    )
    .invoke(state.messages);

  type ToolCall = { name: "create_text_document"; args: { title: string } };
  const toolCall = message.tool_calls?.find(
    (tool): tool is ToolCall => tool.name === "create_text_document"
  );

  if (toolCall) {
    const { id, name } = ui.push(
      { name: "writer", props: { title: toolCall.args.title } },
      { message }
    );

    const contentStream = await model
      // 我们已经通过 UI 消息将 LLM 响应流式传输到客户端
      // 因此无需再次将其流式传输到 `messages` 流模式。
      .withConfig({ tags: ["nostream"] })
      .stream(`Create a short poem with the topic: ${message.text}`);

    let content: AIMessageChunk | undefined;
    for await (const chunk of contentStream) {
      content = content?.concat(chunk) ?? chunk;

      ui.push(
        { id, name, props: { content: content?.text } },
        // 使用 `merge: true` 将属性与现有 UI 消息合并
        { message, merge: true }
      );
    }
  }

  return { messages: [message] };
}

UI 组件示例

function WriterComponent(props: { title: string; content?: string }) {
  return (
    <article>
      <h2>{props.title}</h2>
      <p style={{ whiteSpace: "pre-wrap" }}>{props.content}</p>
    </article>
  );
}

export default {
  writer: WriterComponent,
};

从状态中移除 UI 消息

与通过追加 RemoveMessage 从状态中移除消息类似,你可以通过调用 delete_ui_message(Python)或 ui.delete(TypeScript)并提供 UI 消息的 ID 来从状态中移除 UI 消息。

Python 示例

from langgraph.graph.ui import push_ui_message, delete_ui_message

# 推送消息
message = push_ui_message("weather", {"city": "London"})

# 移除该消息
delete_ui_message(message["id"])

TypeScript 示例

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

// 移除该消息
ui.delete(message.id);

Learn more


总结

  • 生成 UI 机制:本指南展示了如何使用 LangGraph 平台实现生成用户界面(Generative UI),通过将 React 组件与图代码共同托管,动态生成交互式、上下文感知的 UI。
  • 核心步骤
    • 定义和配置 UI 组件
      • src/agent/ui.tsx 中创建 React 组件(如 WeatherComponent),以键值对形式导出({ weather: WeatherComponent })。
      • langgraph.jsonui 部分指定组件路径(如 "agent": "./src/agent/ui.tsx")。
      • 支持 CSS 和 Tailwind 4.x,自动排除 reactreact-dom 依赖。
    • 在图中发送 UI 组件
      • Python:使用 push_ui_message 将 UI 元素(如 weather 组件)与消息关联,存储在 AgentState.ui 中。
      • TypeScript:使用 typedUiui.push 确保类型安全,推送 UI 元素到 ui 状态。
      • 示例节点(如 weather)调用 LLM(如 gpt-4o-miniclaude-3-5-sonnet)生成结构化输出(如城市名称),并附加 UI 组件。
    • 在 React 应用中处理 UI 元素
      • 使用 useStream() 钩子连接 LangGraph 部署,获取线程状态和 UI 消息。
      • 使用 LoadExternalComponent 渲染 UI 组件,通过影子 DOM 确保样式隔离。
  • 操作指南
    • 客户端自定义组件:通过 components 属性提供本地组件映射,跳过从平台加载。
    • 加载 UI:使用 fallback 属性定义组件加载时的回退 UI(如 <div>Loading...</div>)。
    • 命名空间:通过 namespace 属性自定义 UI 组件的加载路径,匹配 langgraph.jsonui 配置。
    • 线程状态交互:在组件中使用 useStreamContext 访问 threadsubmit,实现交互(如重试按钮)。
    • 额外上下文:通过 meta 属性传递上下文(如 userId),在组件中通过 useStreamContext 访问。
    • 流式 UI 消息
      • 使用 onCustomEventuiMessageReducer 处理服务器端的流式 UI 更新。
      • 通过 merge=True 增量更新 UI 消息(如流式生成文档内容)。
    • 移除 UI 消息:使用 delete_ui_message(Python)或 ui.delete(TypeScript)移除 UI 消息。
  • 使用建议
    • UI 设计:利用 Tailwind 和 shadcn/ui 快速构建样式,结合影子 DOM 避免样式冲突。
    • 类型安全:在 TypeScript 中使用 typedUi 和组件映射类型(如 ComponentMap),确保推送的属性类型正确。
    • 流式优化:通过 nostream 标签避免重复流式传输 LLM 响应,结合 onCustomEvent 实现实时 UI 更新。
    • 生产部署
      • 确保 apiUrl(如 http://localhost:2024)指向正确的 LangGraph 部署,验证 assistantId
      • 使用 UUID 作为消息 ID(通过 uuid.uuid4()),确保唯一性。
      • 参考流式传输指南优化流式体验。
    • 调试:结合 LangSmith(需 LANGSMITH_API_KEY)跟踪 UI 消息和图运行,分析性能。
  • 注意事项
    • 依赖 @langchain/langgraph-sdk 和特定 LLM 包(如 @langchain/openai@langchain/anthropic)。
    • 确保 langgraph.json 正确配置 graphsui 路径,Node.js 版本需为 20。
    • 流式 UI 需要服务器支持(如 onCustomEventuiMessageReducer),验证 LLM 配置(如 tags: ["nostream"])。
    • 示例支持 Python 和 TypeScript,适用于不同技术栈,但需注意语言特定的工具(如 zodpydantic)。

参考资料:

### 正确配置LangGraph API接口 对于LangGraph智能体的API接口配置,主要关注点在于确保智能体能够被成功部署至独立的API服务器环境中并能响应外部请求。此过程涉及几个关键步骤和组件设置。 #### 部署环境准备 为了使LangGraph智能体能够在云端正常运作,需先完成基础环境搭建工作[^1]。这意味着要准备好支持Python应用运行的服务端平台,并安装所有必需的支持库文件来满足LangGraph框架的需求[^3]。通常情况下,这可能涉及到云服务提供商的选择以及相应虚拟机或容器镜像的定制化配置。 #### 构建与编译图模型 在实际操作之前,开发者应该已经设计好了用于处理特定任务逻辑流程的图形结构——即由节点(Node)及其连接关系构成的一个抽象表示形式。当一切就绪之后,则可通过调用`graph_builder.compile()`方法将上述未优化版本转换成高效的执行计划对象(`CompiledGraph`),从而为后续的实际调用做好充分准备[^2]。 ```python # 编译图模型实例代码 compiled_graph = graph_builder.compile() ``` #### 定义状态结构 为了让整个系统可以有效地传递上下文信息,在这里引入了一个名为`GraphState`的数据类定义。该数据类型采用TypedDict的形式描述了在整个交互过程中所关心的关键属性字段集合,比如用户输入的问题文本、基于大语言模型产生的回复内容以及其他辅助性的元数据项等[^4]。 ```python from typing import List, TypedDict class GraphState(TypedDict): question: str generation: str web_search: str documents: List[str] ``` #### 设置API路由映射 最后一步是要把前面提到过的这些要素整合起来形成一套完整的RESTful风格Web Service接口方案。具体来说就是利用诸如Flask/Django这样的轻量级Web框架快速实现HTTP请求解析分发机制;与此同时还要注意安全性和性能方面的考量因素,例如身份验证/授权控制措施的应用以及并发访问压力下的资源调度策略调整等等。 通过以上介绍可以看出,正确配置LangGraph API接口不仅限于简单的技术参数设定层面,更涵盖了从业务需求分析到架构选型等一系列复杂环节的工作内容。只有全面考虑各个方面的细节之处才能最终打造出稳定可靠的在线服务平台。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彬彬侠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值