分支对话
通过从检查点派生分支,编辑消息并重新生成回答
与 AI Agent 的对话很少是线性的。你可能想要重新组织问题的措辞、重新生成你不满意的回答,或者在不丢失检查点历史记录的情况下探索不同的对话路径。分支对话将 LangGraph 检查点用作分叉点(fork points):每次编辑或重新生成都会从所选消息的父检查点提交一次新的运行(run)。
NOTE
该特性需要 LangGraph Agent Server。请使用 langgraph dev 在本地运行你的 Agent,或将其部署到 LangSmith 来使用此模式。
什么是分支对话?
分支对话将对话视为带有检查点的时间线,而不是扁平列表。每条消息都包含指向创建该消息之前的检查点的元数据。编辑消息或重新生成回答会从该检查点提交一次新的运行。
核心能力:
- 编辑任意用户消息: 重写之前的提示词(prompt),并从该节点重新运行 Agent
- 重新生成任意 AI 回答: 让 Agent 针对相同的输入生成不同的回答
- 检查历史记录: 当需要分支时间线时,使用 LangGraph 客户端加载检查点
配置流元数据
使用根流获取消息,然后在渲染每条消息的组件中读取单条消息的检查点元数据。该元数据包含了用于派生分支的父检查点 ID。
NOTE
代码示例使用 useStream<typeof myAgent> 来实现类型安全的流状态。请参阅 Python 或 JavaScript 后端的类型推断。
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 (
<div>
{stream.messages.map((msg) => (
<MessageWithForkControls key={msg.id} stream={stream} message={msg} />
))}
</div>
);
}<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>
<div>
<MessageWithForkControls
v-for="msg in stream.messages.value"
:key="msg.id"
:stream="stream"
:message="msg"
/>
</div>
</template><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>`
<div>
{#each stream.messages as msg (msg.id)}
<Message
message={msg}
{stream}
/>
{/each}
</div>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 内:
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 }提交编辑后的消息 - Agent 从该节点重新运行
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 } }
);
}编辑之后:
- Agent 携带更新后的消息从分叉点重新运行
- 原始路径在线程(thread)历史记录中依然可用
重新生成回答
要在不更改输入的情况下重新生成 AI 回答:
- 从 AI 消息的元数据中获取
parent_checkpoint - 使用空输入并携带
forkFrom: { checkpointId }进行提交 - Agent 从该节点生成全新的回答
function handleRegenerate(
stream: ReturnType<typeof useStream>,
metadata: MessageMetadata | undefined
) {
if (!metadata?.parentCheckpointId) return;
stream.submit(undefined, {
forkFrom: { checkpointId: metadata.parentCheckpointId },
});
}每次重新生成都会在该位置为 AI 消息创建一条新路径。
TIP
重新生成对于非确定性 Agent 非常有用。由于 LLM 的输出会随温度(temperature)而变化,对同一个提示词重新生成往往会产生明显不同的回答。
分支底层工作原理
LangGraph 将每一次状态转换持久化为一个检查点(checkpoint)。当你携带 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。 - 悬停时显示分叉控件:编辑和重新生成按钮应在悬停时出现,以保持 UI 整洁。
- 按需刷新历史记录:仅在渲染时间线或分支派生完成(settles)后调用
client.threads.getHistory()。 - 在流式传输时禁用控件:当 Agent 正在活跃地流式传输回答时,不允许编辑或重新生成。在启用这些操作之前,先检查
stream.isLoading。 - 取消时保留编辑文本:如果用户开始编辑后又取消,将文本输入框(textarea)重置为原始消息内容。
- 在深层检查点树下进行测试:频繁编辑和重新生成的用户可能会创建许多路径。请确保时间线渲染始终保持高性能。
