外观
与 AI 智能体的对话很少是线性的。你可能想要改写一个问题、重新生成一个你不满意的回复,或者在不丢失检查点历史的情况下探索一条不同的对话路径。分支对话将 LangGraph 检查点用作分叉点:每次编辑或重新生成都会从所选消息的父检查点提交一次新的运行。
import { PatternEmbed } from "/snippets/pattern-embed.jsx"
INFO
This feature requires the LangGraph Agent Server. Run your agent locally with langgraph dev or deploy it to LangSmith to use this pattern.
什么是分支对话?
分支对话将对话视为一条带检查点的时间线,而不是一个扁平列表。每条消息都有指向该消息创建之前检查点的元数据。编辑消息或重新生成回复会从该检查点提交一次新的运行。
关键能力:
- 编辑任意用户消息: 改写之前的提示词,并从该点重新运行智能体
- 重新生成任意 AI 回复: 让智能体针对相同输入产生不同的答案
- 检查历史: 当你需要分支时间线时,使用 LangGraph 客户端加载检查点
设置流元数据
为消息使用根流,然后在渲染每条消息的组件中读取每条消息的检查点元数据。元数据中包含要从中分叉的父检查点 ID。
INFO
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.
tsx
import { useStream } from "@langchain/react";
const AGENT_URL = "http://localhost:2024";
export function Chat() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
return (
{stream.messages.map((msg) => (
<MessageWithForkControls key={msg.id} stream={stream} message={msg} />
))}
);
}vue
<script setup lang="ts">
import { useStream } from "@langchain/vue";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
</script>
<template>
<MessageWithForkControls
v-for="msg in stream.messages.value"
:key="msg.id"
:stream="stream"
:message="msg"
/>
</template>svelte
<script lang="ts">
import { useStream } from "@langchain/svelte";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
</script>
{#each stream.messages as msg (msg.id)}
<Message
message={msg}
{stream}
/>
{/each}ts
import { Component } from "@angular/core";
import { injectStream } from "@langchain/angular";
const AGENT_URL = "http://localhost:2024";
@Component({
selector: "app-chat",
template: `
@for (msg of stream.messages(); track msg.id) {
<app-message
[message]="msg"
[stream]="stream"
/>
}
`,
})
export class ChatComponent {
stream = injectStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
}理解消息元数据
useMessageMetadata(stream, messageId) 帮助函数会为一条消息返回 MessageMetadata。在渲染每条消息的组件中使用它,这样元数据就会保持限定在该消息 ID 的范围内:
tsx
import type { BaseMessage } from "langchain";
import { useState } from "react";
import { useMessageMetadata, useStream } from "@langchain/react";
function Chat() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
return stream.messages.map((message) => (
<MessageWithForkControls
key={message.id}
stream={stream}
message={message}
/>
));
}
function MessageWithForkControls({
stream,
message,
}: {
stream: ReturnType<typeof useStream>;
message: BaseMessage;
}) {
const metadata = useMessageMetadata(stream, message.id);
const checkpointId = metadata?.parentCheckpointId;
const [editedText, setEditedText] = useState(message.text);
return (
<form
onSubmit={(event) => {
event.preventDefault();
if (!checkpointId) return;
stream.submit(
{ messages: [{ type: "human", content: editedText }] },
{ forkFrom: { checkpointId } }
);
}}
>
<textarea
value={editedText}
onChange={(event) => setEditedText(event.target.value)}
/>
<button disabled={!checkpointId || editedText === message.text}>
Submit edited branch
</button>
</form>
);
}parentCheckpointId 是消息之前的那一个检查点。将其用作编辑和重新生成的分叉点。
编辑消息
要编辑用户消息并分叉对话:
- 从消息的元数据中获取
parentCheckpointId - 使用
forkFrom: { checkpointId }提交编辑后的消息 - 智能体会从该点重新运行
ts
function handleEdit(
stream: ReturnType<typeof useStream>,
originalMsg: HumanMessage,
metadata: MessageMetadata | undefined,
newText: string
) {
if (!metadata?.parentCheckpointId) return;
stream.submit(
{
messages: [{ type: "human", content: newText }],
},
{ forkFrom: { checkpointId: metadata.parentCheckpointId } }
);
}编辑之后:
- 智能体会使用更新后的消息从分叉点重新运行
- 原始路径仍保留在线程历史中
重新生成回复
要在不改变输入的情况下重新生成 AI 回复:
- 从 AI 消息的元数据中获取
parent_checkpoint - 使用空输入和
forkFrom: { checkpointId }提交 - 智能体会从该点产生一条新的回复
ts
function handleRegenerate(
stream: ReturnType<typeof useStream>,
metadata: MessageMetadata | undefined
) {
if (!metadata?.parentCheckpointId) return;
stream.submit(undefined, {
forkFrom: { checkpointId: metadata.parentCheckpointId },
});
}每次重新生成都会在该位置为 AI 消息创建一条新路径。
TIP
重新生成对非确定性的智能体很有用。由于 LLM 输出会随 temperature 变化,重新生成相同的提示词通常会产生意义不同的回复。
分支在底层是如何工作的
LangGraph 会将每次状态转换持久化为一个检查点。当你使用 forkFrom 提交时,后端会从该点开始一条新的执行路径,而不是追加到当前对话中。结果是一个树形结构:
User: "What is React?"
└─ AI: "React is a JavaScript library..." (branch A)
└─ AI: "React is a UI framework..." (branch B, regenerated)
User: "Tell me about hooks" (branch A)
└─ AI: "Hooks are functions..."
User: "Tell me about JSX" (edited from branch A)
└─ AI: "JSX is a syntax extension..."每条路径都会持久化到检查点存储中。当你想要构建一个跨越检查点的独立时间线视图时,请使用 stream.client.threads.getHistory(threadId)。
最佳实践
- 在消息附近读取元数据:在渲染消息控件的组件中调用
useMessageMetadata。 - 悬停时显示分叉控件:编辑和重新生成按钮应在悬停时出现,以保持界面整洁。
- 按需刷新历史:仅在渲染时间线时或分叉落定后调用
client.threads.getHistory()。 - 流式传输期间禁用控件:当智能体正在流式输出回复时,不允许编辑或重新生成。在启用这些操作前检查
stream.isLoading。 - 取消时保留编辑文本:如果用户开始编辑然后又取消,请将 textarea 重置为原始消息内容。
- 使用深层的检查点树进行测试:经常编辑和重新生成的用户可能会创建许多路径。确保时间线渲染保持高性能。