如何使用 LangGraph 实现生成式用户界面¶
生成式用户界面(Generative UI)允许智能体超越文本,生成丰富的用户界面。这使得能够创建更具交互性和上下文感知的应用程序,其中用户界面会根据对话流程和人工智能响应进行自适应调整。
LangGraph 平台支持将你的 React 组件与图代码放在一起。这使你可以专注于为你的图构建特定的用户界面组件,同时轻松接入现有的聊天界面,如 智能体聊天,并仅在实际需要时加载代码。
教程¶
1. 定义并配置 UI 组件¶
首先,创建你的第一个 UI 组件。对于每个组件,你需要提供一个唯一标识符,该标识符将用于在图形代码中引用该组件。
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
。
2. 在图形中发送 UI 组件¶
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()
使用 typedUi
工具从你的代理节点发出 UI 元素:
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 元素。
"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 组件的 JS 和 CSS,并在影子 DOM 中渲染它们,从而确保与应用程序的其余部分实现样式隔离。
操作指南¶
在组件加载时显示加载界面¶
你可以提供一个备用界面,在组件加载时进行渲染。
在客户端提供自定义组件¶
如果你已经在客户端应用程序中加载了组件,可以提供一个此类组件的映射,这样就无需从 LangGraph 平台获取 UI 代码,直接进行渲染。
const clientComponents = {
weather: WeatherComponent,
};
<LoadExternalComponent
stream={thread}
message={ui}
components={clientComponents}
/>;
自定义 UI 组件的命名空间¶
默认情况下,LoadExternalComponent
会使用 useStream()
钩子中的 assistantId
来获取 UI 组件的代码。你可以通过为 LoadExternalComponent
组件提供 namespace
属性来进行自定义。
从 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
属性,向客户端组件传递额外的上下文。
然后,你可以在 UI 组件中使用 useStreamContext
钩子来访问 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。
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 消息¶
类似于通过追加 RemoveMessage
从状态中移除消息,你可以通过调用 remove_ui_message
/ ui.delete
并传入 UI 消息的 ID,从状态中移除 UI 消息。