如何使用 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
组件加载。某些依赖(如 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
@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.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 组件。
- Python:使用
- 在 React 应用中处理 UI 元素:
- 使用
useStream()
钩子连接 LangGraph 部署,获取线程状态和 UI 消息。 - 使用
LoadExternalComponent
渲染 UI 组件,通过影子 DOM 确保样式隔离。
- 使用
- 定义和配置 UI 组件:
- 操作指南:
- 客户端自定义组件:通过
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 消息和图运行,分析性能。
- UI 设计:利用 Tailwind 和
- 注意事项:
- 依赖
@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
)。
- 依赖
参考资料: