作者:彬彬侠
如何使用 LangGraph 实现生成用户界面(How to implement Generative User Interfaces with LangGraph)
Prerequisites
LangGraph PlatformLangGraph ServeruseStream() React Hook
生成用户界面(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 组件加载。某些依赖(如 react 和 react-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 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
- classAgentState(TypedDict):# noqa: D101
- messages: Annotated[Sequence[BaseMessage], add_messages]
- ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]asyncdefweather(state: AgentState):classWeatherOutput(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";importtypeComponentMapfrom"./ui.js";import{
- Annotation,
- MessagesAnnotation,
- StateGraph,typeLangGraphRunnableConfig,}from"@langchain/langgraph";const AgentState = Annotation.Root({...MessagesAnnotation.spec,
- 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 ={
- 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
- classAgentState(TypedDict):# noqa: D101
- messages: Annotated[Sequence[BaseMessage], add_messages]
- ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]classCreateTextDocument(TypedDict):"""为用户准备文档标题。"""
- title:strasyncdefwriter_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=Noneasyncfor 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,typeLangGraphRunnableConfig,}from"@langchain/langgraph";import{ z }from"zod";import{ ChatAnthropic }from"@langchain/anthropic";import{
- typedUi,
- uiMessageReducer,}from"@langchain/langgraph-sdk/react-ui/server";importtype{ AIMessageChunk }from"@langchain/core/messages";importtypeComponentMapfrom"./ui";const AgentState = Annotation.Root({...MessagesAnnotation.spec,
- ui:Annotation({ reducer: uiMessageReducer,default:()=>[]}),});asyncfunctionwriterNode(
- state:typeof AgentState.State,
- config: LangGraphRunnableConfig
- ):Promise<typeof AgentState.Update>{const ui =typedUi<typeof ComponentMap>(config);const model =newChatAnthropic({ 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);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
- // 我们已经通过 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){
- 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.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 |