开启左侧

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

[复制链接]
AI小编 发表于 昨天 22:43 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
作者:彬彬侠
如何使用 LangGraph 实现生成用户界面(How to implement Generative User Interfaces with LangGraph)

Prerequisites
    LangGraph PlatformLangGraph ServeruseStream() React Hook
生成用户界面(Generative UI)允许代理超越文本,生成丰富的用户界面。这使得创建更具交互性和上下文感知的应用成为可能,其中 UI 根据对话流程和 AI 响应动态适应。
【LangGraph】Implement generative UI with LangGraph:用 LangGraph 实现生成用户界面-1.png


LangGraph 平台支持将你的 React 组件与图代码共同托管。这允许你专注于为图构建特定的 UI 组件,同时轻松接入现有聊天界面(如 Agent Chat),并仅在需要时加载代码。
教程

1. 定义和配置 UI 组件

首先,创建你的第一个 UI 组件。每个组件需要提供一个唯一标识符,用于在图代码中引用该组件。
src/agent/ui.tsx
  1. const WeatherComponent = (props: { city: string }) => {
  2.   return <div>Weather for {props.city}</div>;
  3. };
  4. export default {
  5.   weather: WeatherComponent,
  6. };
复制代码
接下来,在你的 langgraph.json 配置文件中定义 UI 组件:
  1. {"node_version":"20","graphs":{"agent":"./src/agent/index.ts:graph"},"ui":{"agent":"./src/agent/ui.tsx"}}
复制代码
ui 部分指向将由图使用的 UI 组件。默认情况下,我们建议使用与图名称相同的键,但你可以根据需要拆分组件,详情参见自定义 UI 组件命名空间。
LangGraph 平台会自动打包你的 UI 组件代码和样式,并将其作为外部资产提供,可通过 LoadExternalComponent 组件加载。某些依赖(如 react 和 react-dom)会自动从打包中排除。
CSS 和 Tailwind 4.x 也受开箱支持,因此你可以在 UI 组件中自由使用 Tailwind 类以及 shadcn/ui。

  • src/agent/ui.tsx(带样式)
    1. import "./styles.css";
    2. const WeatherComponent = (props: { city: string }) => {
    3.   return <div className="bg-red-500">Weather for {props.city}</div>;
    4. };
    5. export default {
    6.   weather: WeatherComponent,
    7. };
    复制代码
    styles.css
    1. @import"tailwindcss";
    复制代码
2. 在图中发送 UI 组件

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

  • (Python)src/agent.py
    1. import uuid
    2. from typing import Annotated, Sequence, TypedDict
    3. from langchain_core.messages import AIMessage, BaseMessage
    4. from langchain_openai import ChatOpenAI
    5. from langgraph.graph import StateGraph
    6. from langgraph.graph.message import add_messages
    7. from langgraph.graph.ui import AnyUIMessage, ui_message_reducer, push_ui_message
    8. classAgentState(TypedDict):# noqa: D101
    9.     messages: Annotated[Sequence[BaseMessage], add_messages]
    10.     ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]asyncdefweather(state: AgentState):classWeatherOutput(TypedDict):
    11.         city:str
    12.     weather: WeatherOutput =(await ChatOpenAI(model="gpt-4o-mini").with_structured_output(WeatherOutput).with_config({"tags":["nostream"]}).ainvoke(state["messages"]))
    13.     message = AIMessage(id=str(uuid.uuid4()),
    14.         content=f"Here's the weather for {weather['city']}",)# 发出与消息关联的 UI 元素
    15.     push_ui_message("weather", weather, message=message)return{"messages":[message]}
    16. workflow = StateGraph(AgentState)
    17. workflow.add_node(weather)
    18. workflow.add_edge("__start__","weather")
    19. graph = workflow.compile()
    复制代码
在 TypeScript 中,使用 typedUi 工具确保 UI 元素推送的类型安全:

  • (TypeScript)src/agent/index.ts
    1. import{
    2.   typedUi,
    3.   uiMessageReducer,}from"@langchain/langgraph-sdk/react-ui/server";import{ ChatOpenAI }from"@langchain/openai";import{ v4 as uuidv4 }from"uuid";import{ z }from"zod";importtypeComponentMapfrom"./ui.js";import{
    4.   Annotation,
    5.   MessagesAnnotation,
    6.   StateGraph,typeLangGraphRunnableConfig,}from"@langchain/langgraph";const AgentState = Annotation.Root({...MessagesAnnotation.spec,
    7.   ui:Annotation({ reducer: uiMessageReducer,default:()=>[]}),});exportconst graph =newStateGraph(AgentState).addNode("weather",async(state, config)=>{// 提供组件映射的类型以确保// `ui.push()` 调用的类型安全,以及// 将消息推送到 `ui` 并发送自定义事件const ui =typedUi<typeof ComponentMap>(config);const weather =awaitnewChatOpenAI({ model:"gpt-4o-mini"}).withStructuredOutput(z.object({ city: z.string()})).withConfig({ tags:["nostream"]}).invoke(state.messages);const response ={
    8.       id:uuidv4(),
    9.       type:"ai",
    10.       content:`Here's the weather for ${weather.city}`,};// 发出与 AI 消息关联的 UI 元素
    11.     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
  1. "use client";
  2. import { useStream } from "@langchain/langgraph-sdk/react";
  3. import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
  4. export default function Page() {
  5.   const { thread, values } = useStream({
  6.     apiUrl: "http://localhost:2024",
  7.     assistantId: "agent",
  8.   });
  9.   return (
  10.     <div>
  11.       {thread.messages.map((message) => (
  12.         <div key={message.id}>
  13.           {message.content}
  14.           {values.ui
  15.             ?.filter((ui) => ui.metadata?.message_id === message.id)
  16.             .map((ui) => (
  17.               <LoadExternalComponent key={ui.id} stream={thread} message={ui} />
  18.             ))}
  19.         </div>
  20.       ))}
  21.     </div>
  22.   );
  23. }
复制代码
在后台,LoadExternalComponent 会从 LangGraph 平台获取 UI 组件的 JavaScript 和 CSS 代码,并在一个影子 DOM 中渲染它们,从而确保与应用其余部分的样式隔离。
操作指南

在客户端提供自定义组件

如果你已经在客户端应用中加载了组件,可以提供一个组件映射,直接渲染而无需从 LangGraph 平台获取 UI 代码。
  1. const clientComponents = {
  2.   weather: WeatherComponent,
  3. };
  4. <LoadExternalComponent
  5.   stream={thread}
  6.   message={ui}
  7.   components={clientComponents}
  8. />;
复制代码
在组件加载时显示加载 UI

你可以提供一个回退 UI,在组件加载时渲染。
  1. <LoadExternalComponent
  2.   stream={thread}
  3.   message={ui}
  4.   fallback={<div>Loading...</div>}
  5. />
复制代码
自定义 UI 组件的命名空间

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

  • src/app/page.tsx
    1. <LoadExternalComponent
    2.   stream={thread}
    3.   message={ui}
    4.   namespace="custom-namespace"
    5. />
    复制代码
    langgraph.json
    1. {"ui":{"custom-namespace":"./src/agent/ui.tsx"}}
    复制代码
从 UI 组件访问和交互线程状态

你可以通过 useStreamContext 钩子在 UI 组件内访问线程状态。
  1. import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";
  2. const WeatherComponent = (props: { city: string }) => {
  3.   const { thread, submit } = useStreamContext();
  4.   return (
  5.     <>
  6.       <div>Weather for {props.city}</div>
  7.       <button
  8.         onClick={() => {
  9.           const newMessage = {
  10.             type: "human",
  11.             content: `What's the weather in ${props.city}?`,
  12.           };
  13.           submit({ messages: [newMessage] });
  14.         }}
  15.       >
  16.         Retry
  17.       </button>
  18.     </>
  19.   );
  20. };
复制代码
向客户端组件传递额外上下文

你可以通过为 LoadExternalComponent 组件提供 meta 属性向客户端组件传递额外上下文。
  1. <LoadExternalComponent stream={thread} message={ui} meta={{ userId: "123" }} />
复制代码
然后,你可以通过 useStreamContext 钩子在 UI 组件中访问 meta 属性。
  1. import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";
  2. const WeatherComponent = (props: { city: string }) => {
  3.   const { meta } = useStreamContext<
  4.     { city: string },
  5.     { MetaType: { userId?: string } }
  6.   >();
  7.   return (
  8.     <div>
  9.       Weather for {props.city} (user: {meta?.userId})
  10.     </div>
  11.   );
  12. };
复制代码
从服务器流式传输 UI 消息

你可以通过 useStream() 钩子的 onCustomEvent 回调在节点执行完成之前流式传输 UI 消息。这在 LLM 生成响应时更新 UI 组件特别有用。
  1. import { uiMessageReducer } from "@langchain/langgraph-sdk/react-ui";
  2. const { thread, submit } = useStream({
  3.   apiUrl: "http://localhost:2024",
  4.   assistantId: "agent",
  5.   onCustomEvent: (event, options) => {
  6.     options.mutate((prev) => {
  7.       const ui = uiMessageReducer(prev.ui ?? [], event);
  8.       return { ...prev, ui };
  9.     });
  10.   },
  11. });
复制代码
然后,你可以通过调用 ui.push()(TypeScript)或 push_ui_message()(Python)并使用与要更新的 UI 消息相同的 ID 来推送 UI 组件的更新。
Python 示例
  1. from typing import Annotated, Sequence, TypedDict
  2. from langchain_anthropic import ChatAnthropic
  3. from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
  4. from langgraph.graph import StateGraph
  5. from langgraph.graph.message import add_messages
  6. from langgraph.graph.ui import AnyUIMessage, push_ui_message, ui_message_reducer
  7. classAgentState(TypedDict):# noqa: D101
  8.     messages: Annotated[Sequence[BaseMessage], add_messages]
  9.     ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]classCreateTextDocument(TypedDict):"""为用户准备文档标题。"""
  10.     title:strasyncdefwriter_node(state: AgentState):
  11.     model = ChatAnthropic(model="claude-3-5-sonnet-latest")
  12.     message: AIMessage =await model.bind_tools(
  13.         tools=[CreateTextDocument],
  14.         tool_choice={"type":"tool","name":"CreateTextDocument"},).ainvoke(state["messages"])
  15.     tool_call =next((x["args"]for x in message.tool_calls if x["name"]=="CreateTextDocument"),None,)if tool_call:
  16.         ui_message = push_ui_message("writer", tool_call, message=message)
  17.         ui_message_id = ui_message["id"]# 我们已经通过 UI 消息将 LLM 响应流式传输到客户端# 因此无需再次将其流式传输到 `messages` 流模式。
  18.         content_stream = model.with_config({"tags":["nostream"]}).astream(f"Create a document with the title: {tool_call['title']}")
  19.         content: AIMessageChunk |None=Noneasyncfor chunk in content_stream:
  20.             content = content + chunk if content else chunk
  21.             push_ui_message("writer",{"content": content.text()},id=ui_message_id,
  22.                 message=message,# 使用 `merge=True` 将属性与现有 UI 消息合并
  23.                 merge=True,)return{"messages":[message]}
复制代码
TypeScript 示例
  1. import{
  2.   Annotation,
  3.   MessagesAnnotation,typeLangGraphRunnableConfig,}from"@langchain/langgraph";import{ z }from"zod";import{ ChatAnthropic }from"@langchain/anthropic";import{
  4.   typedUi,
  5.   uiMessageReducer,}from"@langchain/langgraph-sdk/react-ui/server";importtype{ AIMessageChunk }from"@langchain/core/messages";importtypeComponentMapfrom"./ui";const AgentState = Annotation.Root({...MessagesAnnotation.spec,
  6.   ui:Annotation({ reducer: uiMessageReducer,default:()=>[]}),});asyncfunctionwriterNode(
  7.   state:typeof AgentState.State,
  8.   config: LangGraphRunnableConfig
  9. ):Promise<typeof AgentState.Update>{const ui =typedUi<typeof ComponentMap>(config);const model =newChatAnthropic({ model:"claude-3-5-sonnet-latest"});const message =await model
  10.     .bindTools([{
  11.           name:"create_text_document",
  12.           description:"Prepare a document heading for the user.",
  13.           schema: z.object({ title: z.string()}),},],{ tool_choice:{ type:"tool", name:"create_text_document"}}).invoke(state.messages);typeToolCall={ 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
  14.       // 我们已经通过 UI 消息将 LLM 响应流式传输到客户端// 因此无需再次将其流式传输到 `messages` 流模式。.withConfig({ tags:["nostream"]}).stream(`Create a short poem with the topic: ${message.text}`);let content: AIMessageChunk |undefined;forawait(const chunk of contentStream){
  15.       content = content?.concat(chunk)?? chunk;
  16.       ui.push({ id, name, props:{ content: content?.text }},// 使用 `merge: true` 将属性与现有 UI 消息合并{ message, merge:true});}}return{ messages:[message]};}
复制代码
UI 组件示例
  1. function WriterComponent(props: { title: string; content?: string }) {
  2.   return (
  3.     <article>
  4.       <h2>{props.title}</h2>
  5.       <p style={{ whiteSpace: "pre-wrap" }}>{props.content}</p>
  6.     </article>
  7.   );
  8. }
  9. export default {
  10.   writer: WriterComponent,
  11. };
复制代码
从状态中移除 UI 消息

与通过追加 RemoveMessage 从状态中移除消息类似,你可以通过调用 delete_ui_message(Python)或 ui.delete(TypeScript)并提供 UI 消息的 ID 来从状态中移除 UI 消息。
Python 示例
  1. from langgraph.graph.ui import push_ui_message, delete_ui_message
  2. # 推送消息
  3. message = push_ui_message("weather",{"city":"London"})# 移除该消息
  4. delete_ui_message(message["id"])
复制代码
TypeScript 示例
  1. // 推送消息const message = ui.push({ name:"weather", props:{ city:"London"}});// 移除该消息
  2. ui.delete(message.id);
复制代码
Learn more
    JS/TS SDK Reference

总结

    生成 UI 机制:本指南展示了如何使用 LangGraph 平台实现生成用户界面(Generative UI),通过将 React 组件与图代码共同托管,动态生成交互式、上下文感知的 UI。
  • 核心步骤

    • 定义和配置 UI 组件
        在 src/agent/ui.tsx 中创建 React 组件(如 WeatherComponent),以键值对形式导出({ weather: WeatherComponent })。在 langgraph.json 的 ui 部分指定组件路径(如 "agent": "./src/agent/ui.tsx")。支持 CSS 和 Tailwind 4.x,自动排除 react 和 react-dom 依赖。

    • 在图中发送 UI 组件
        Python:使用 push_ui_message 将 UI 元素(如 weather 组件)与消息关联,存储在 AgentState.ui 中。TypeScript:使用 typedUi 和 ui.push 确保类型安全,推送 UI 元素到 ui 状态。示例节点(如 weather)调用 LLM(如 gpt-4o-mini 或 claude-3-5-sonnet)生成结构化输出(如城市名称),并附加 UI 组件。

    • 在 React 应用中处理 UI 元素
        使用 useStream() 钩子连接 LangGraph 部署,获取线程状态和 UI 消息。使用 LoadExternalComponent 渲染 UI 组件,通过影子 DOM 确保样式隔离。


  • 操作指南
      客户端自定义组件:通过 components 属性提供本地组件映射,跳过从平台加载。加载 UI:使用 fallback 属性定义组件加载时的回退 UI(如 <div>Loading...</div>)。命名空间:通过 namespace 属性自定义 UI 组件的加载路径,匹配 langgraph.json 的 ui 配置。线程状态交互:在组件中使用 useStreamContext 访问 thread 和 submit,实现交互(如重试按钮)。额外上下文:通过 meta 属性传递上下文(如 userId),在组件中通过 useStreamContext 访问。
    • 流式 UI 消息
        使用 onCustomEvent 和 uiMessageReducer 处理服务器端的流式 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 正确配置 graphs 和 ui 路径,Node.js 版本需为 20。流式 UI 需要服务器支持(如 onCustomEvent 和 uiMessageReducer),验证 LLM 配置(如 tags: ["nostream"])。示例支持 Python 和 TypeScript,适用于不同技术栈,但需注意语言特定的工具(如 zod 或 pydantic)。


参考资料:
    https://langchain-ai.github.io/langgraph/cloud/how-tos/generative_ui_react/

原文地址:https://blog.csdn.net/u013172930/article/details/148141911
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

发布主题
阅读排行更多+

Powered by Discuz! X3.4© 2001-2013 Discuz Team.( 京ICP备17022993号-3 )