如何将LangGraph集成到您的React应用程序中¶
先决条件
useStream()
React钩子提供了一种无缝集成LangGraph到您的React应用程序中的方法。它处理了流式传输、状态管理和分支逻辑的所有复杂性,让您能够专注于构建出色的聊天体验。
主要特性:
- 消息流处理:处理消息片段流以形成完整消息
- 自动消息、中断、加载状态和错误的状态管理
- 对话分支:从聊天历史中的任何点创建替代对话路径
- 与UI无关的设计:自带组件和样式
让我们探索如何在React应用程序中使用useStream()
。
useStream()
为创建定制聊天体验提供了坚实的基础。对于预构建的聊天组件和界面,我们还推荐查看CopilotKit和assistant-ui。
安装¶
示例¶
"use client";
import { useStream } from "@langchain/langgraph-sdk/react";
import type { Message } from "@langchain/langgraph-sdk";
export default function App() {
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
<div>
{thread.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const message = new FormData(form).get("message") as string;
form.reset();
thread.submit({ messages: [{ type: "human", content: message }] });
}}
>
<input type="text" name="message" />
{thread.isLoading ? (
<button key="stop" type="button" onClick={() => thread.stop()}>
停止
</button>
) : (
<button keytype="submit">发送</button>
)}
</form>
</div>
);
}
自定义您的用户界面¶
useStream()
钩子处理了所有复杂的后台状态管理,为您提供简单易用的接口来构建您的用户界面。以下是您开箱即得的功能:
- 线程状态管理
- 加载和错误状态
- 中断
- 消息处理和更新
- 分支支持
这里有一些关于如何有效使用这些功能的例子:
加载状态¶
isLoading
属性告诉您当流处于活动状态时,您可以:
- 显示加载指示器
- 在处理期间禁用输入字段
- 显示取消按钮
export default function App() {
const { isLoading, stop } = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<form>
{isLoading && (
<button key="stop" type="button" onClick={() => stop()}>
停止
</button>
)}
</form>
);
}
线程管理¶
使用内置的线程管理功能来跟踪对话。您可以访问当前的线程 ID,并在创建新线程时收到通知:
const [threadId, setThreadId] = useState<string | null>(null);
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
threadId: threadId,
onThreadId: setThreadId,
});
我们建议将 threadId
存储在 URL 的查询参数中,以便用户在页面刷新后可以继续对话。
消息处理¶
useStream()
钩子会跟踪从服务器接收的消息块,并将它们连接起来形成完整的消息。完成的消息块可以通过 messages
属性获取。
默认情况下,messagesKey
设置为 messages
,它将新消息块追加到 values["messages"]
。如果您将消息存储在不同的键中,可以更改 messagesKey
的值。
import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
export default function HomePage() {
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
{thread.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
);
}
在后台,useStream()
钩子将使用 streamMode: "messages-key"
来接收消息流(即单个 LLM 令牌)从任何 LangChain 聊天模型调用中。更多关于消息流的信息可以在 如何从您的图中流式传输消息 指南中找到。
中断¶
useStream()
钩子暴露了 interrupt
属性,该属性将填充线程中的最后一个中断。您可以使用中断来:
- 渲染确认界面,在执行节点之前
- 等待人类输入,允许代理向用户提出澄清问题
更多关于中断的信息可以在 如何处理中断 指南中找到。
const thread = useStream<
{ messages: Message[] },
{ InterruptType: string }
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
if (thread.interrupt) {
return (
<div>
中断!{thread.interrupt.value}
<button
type="button"
onClick={() => {
// `resume` 可以是代理接受的任何值
thread.submit(undefined, { command: { resume: true } });
}}
>
继续
</button>
</div>
);
}
分支¶
对于每个消息,您可以使用 getMessagesMetadata()
获取消息首次被看到的第一个检查点。然后,您可以从先于第一个看到的检查点的检查点创建一个新的运行,从而在线程中创建一个新的分支。
分支可以通过以下方式创建:
- 编辑之前的用户消息。
- 请求重新生成之前的助手消息。
"use client";
import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
import {
Annotation,
MessagesAnnotation,
type StateType,
type UpdateType,
} from "@langchain/langgraph/web";
import { useState } from "react";
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
});
function BranchSwitcher({
branch,
branchOptions,
onSelect,
}: {
branch: string | undefined;
branchOptions: string[] | undefined;
onSelect: (branch: string) => void;
}) {
if (!branchOptions || !branch) return null;
const index = branchOptions.indexOf(branch);
return (
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
const prevBranch = branchOptions[index - 1];
if (!prevBranch) return;
onSelect(prevBranch);
}}
>
上一个
</button>
<span>
{index + 1} / {branchOptions.length}
</span>
<button
type="button"
onClick={() => {
const nextBranch = branchOptions[index + 1];
if (!nextBranch) return;
onSelect(nextBranch);
}}
>
下一个
</button>
</div>
);
}
function EditMessage({
message,
onEdit,
}: {
message: Message;
onEdit: (message: Message) => void;
}) {
const [editing, setEditing] = useState(false);
if (!editing) {
return (
<button type="button" onClick={() => setEditing(true)}>
编辑
</button>
);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const content = new FormData(form).get("content") as string;
form.reset();
onEdit({ type: "human", content });
setEditing(false);
}}
>
<input name="content" defaultValue={message.content as string} />
<button type="submit">保存</button>
</form>
);
}
export default function App() {
const thread = useStream<
StateType<typeof AgentState.spec>,
{ UpdateType: UpdateType<typeof AgentState.spec> }
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
<div>
{thread.messages.map((message) => {
const meta = thread.getMessagesMetadata(message);
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
return (
<div key={message.id}>
<div>{message.content as string}</div>
{message.type === "human" && (
<EditMessage
message={message}
onEdit={(message) =>
thread.submit(
{ messages: [message] },
{ checkpoint: parentCheckpoint }
)
}
/>
)}
{message.type === "ai" && (
<button
type="button"
onClick={() =>
thread.submit(undefined, { checkpoint: parentCheckpoint })
}
>
<span>重新生成</span>
</button>
)}
<BranchSwitcher
branch={meta?.branch}
branchOptions={meta?.branchOptions}
onSelect={(branch) => thread.setBranch(branch)}
/>
</div>
);
})}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const message = new FormData(form).get("message") as string;
form.reset();
thread.submit({ messages: [message] });
}}
>
<input type="text" name="message" />
{thread.isLoading ? (
<button key="stop" type="button" onClick={() => thread.stop()}>
停止
</button>
) : (
<button key="submit" type="submit">
发送
</button>
)}
</form>
</div>
);
}
对于高级用例,您可以使用 experimental_branchTree
属性来获取线程的树表示,这可以用于渲染非消息基础图的分支控件。
TypeScript¶
useStream()
钩子对使用 TypeScript 编写的应用程序友好,您可以为状态指定类型,以获得更好的类型安全和 IDE 支持。
// 定义您的类型
type State = {
messages: Message[];
context?: Record<string, unknown>;
};
// 使用它们与钩子
const thread = useStream<State>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
您还可以可选地为不同场景指定类型,例如:
ConfigurableType
:config.configurable
属性的类型(默认:Record<string, unknown>
)InterruptType
: 中断值的类型 - 即interrupt(...)
函数的内容(默认:unknown
)CustomEventType
: 自定义事件的类型(默认:unknown
)UpdateType
: 提交函数的类型(默认:Partial<State>
)
const thread = useStream<State, {
UpdateType: {
messages: Message[] | Message;
context?: Record<string, unknown>;
};
InterruptType: string;
CustomEventType: {
type: "progress" | "debug";
payload: unknown;
};
ConfigurableType: {
model: string;
};
}>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
如果您使用 LangGraph.js,还可以重用图的注释类型。但是,请确保只导入注释模式的类型,以避免导入整个 LangGraph.js 运行时(即通过 import type { ... }
指令)。
import {
Annotation,
MessagesAnnotation,
type StateType,
type UpdateType,
} from "@langchain/langgraph/web";
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
context: Annotation<string>(),
});
const thread = useStream<
StateType<typeof AgentState.spec>,
{ UpdateType: UpdateType<typeof AgentState.spec> }
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
事件处理¶
useStream()
钩子提供了几种回调选项,帮助你响应不同的事件:
onError
:当发生错误时被调用。onFinish
:当流结束时被调用。onUpdateEvent
:当收到更新事件时被调用。onCustomEvent
:当收到自定义事件时被调用。请参阅自定义事件,了解如何流式传输自定义事件。onMetadataEvent
:当收到包含运行ID和线程ID的元数据事件时被调用。