Skip to main content

Documentation Index

Fetch the complete documentation index at: https://nvd-54.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

与 AI 智能体的对话很少是线性的。你可能想要重新措辞一个问题、重新生成一个不满意的回复,或者在不丢失之前内容的情况下探索完全不同的对话路径。分支对话将版本控制的语义引入你的聊天界面。每次编辑都会创建一个新分支,你可以在分支之间自由导航。
This feature requires the LangGraph Agent Server. Run your agent locally with langgraph dev or deploy it to LangSmith to use this pattern.

什么是分支对话?

分支对话将对话视为一棵树而非一个列表。每条消息是一个节点,编辑消息或重新生成回复会从该点创建一个分叉。原始路径作为兄弟分支被保留,因此用户可以在不同的对话轨迹之间来回切换。 核心功能:
  • **编辑任意用户消息:**重写之前的提示词并从该点重新运行智能体
  • **重新生成任意 AI 回复:**要求智能体对相同输入生成不同的答案
  • **分支导航:**使用每条消息的分支控件在不同版本的对话间切换

设置带历史记录的 useStream

要启用分支功能,传入 fetchStateHistory: true,使 useStream 检索分支操作所需的检查点元数据。 定义一个与你的智能体状态 schema 匹配的 TypeScript 接口,并将其作为类型参数传递给 useStream,以获得类型安全的状态值访问。在以下示例中,将 typeof myAgent 替换为你的接口名称:
import type { BaseMessage } from "@langchain/core/messages";

interface AgentState {
  messages: BaseMessage[];
}
import { useStream } from "@langchain/react";

const AGENT_URL = "http://localhost:2024";

export function Chat() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "branching_chat",
    fetchStateHistory: true,
  });

  return (
    <div>
      {stream.messages.map((msg) => {
        const metadata = stream.getMessagesMetadata(msg);
        return (
          <Message
            key={msg.id}
            message={msg}
            metadata={metadata}
            onEdit={(text) => handleEdit(stream, msg, metadata, text)}
            onRegenerate={() => handleRegenerate(stream, metadata)}
            onBranchSwitch={(id) => stream.setBranch(id)}
          />
        );
      })}
    </div>
  );
}

理解消息元数据

getMessagesMetadata(msg) 函数返回每条消息的分支信息:
interface MessageMetadata {
  branch: string;
  branchOptions: string[];
  firstSeenState: {
    parent_checkpoint: Checkpoint | null;
  };
}
属性描述
branch该消息特定版本的分支 ID
branchOptions该消息位置所有可用分支 ID 的数组
firstSeenState.parent_checkpoint该消息之前的检查点。用作编辑和重新生成的分叉点
当消息只有一个版本时,branchOptions 只包含一个条目。在编辑或重新生成后,新的分支 ID 会被添加到 branchOptions 中,你可以在它们之间导航。

编辑消息

要编辑用户消息并创建新分支:
  1. 从消息的元数据中获取 parent_checkpoint
  2. 使用该检查点提交编辑后的消息
  3. 智能体从该点重新运行,创建一个新分支
function handleEdit(
  stream: ReturnType<typeof useStream>,
  originalMsg: HumanMessage,
  metadata: MessageMetadata,
  newText: string
) {
  const checkpoint = metadata.firstSeenState?.parent_checkpoint;
  if (!checkpoint) return;

  stream.submit(
    {
      messages: [{ ...originalMsg, content: newText }],
    },
    { checkpoint }
  );
}
编辑后:
  • 消息的 branchOptions 新增一个条目
  • 视图自动切换到新分支
  • 智能体从分叉点使用更新后的消息重新运行
  • 原始版本被保留,可通过分支切换器访问

重新生成回复

要在不改变输入的情况下重新生成 AI 回复:
  1. 从 AI 消息的元数据中获取 parent_checkpoint
  2. 使用 undefined 输入和父检查点提交
  3. 智能体产生一个新的回复,创建一个新分支
function handleRegenerate(
  stream: ReturnType<typeof useStream>,
  metadata: MessageMetadata
) {
  const checkpoint = metadata.firstSeenState?.parent_checkpoint;
  if (!checkpoint) return;

  stream.submit(undefined, { checkpoint });
}
每次重新生成都会在该位置为 AI 消息创建一个新分支。用户可以使用分支切换器来比较不同的回复。
重新生成对非确定性智能体很有用。由于大语言模型(LLM)的输出会随温度参数变化,对相同提示词重新生成通常会产生明显不同的回复。

构建分支切换器

当消息有多个分支时,显示一个紧凑的内联控件,包含当前版本索引和导航箭头:
function BranchSwitcher({
  metadata,
  onSwitch,
}: {
  metadata: MessageMetadata;
  onSwitch: (branchId: string) => void;
}) {
  const { branch, branchOptions } = metadata;

  if (branchOptions.length <= 1) return null;

  const currentIndex = branchOptions.indexOf(branch);
  const hasPrev = currentIndex > 0;
  const hasNext = currentIndex < branchOptions.length - 1;

  return (
    <div className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600">
      <button
        disabled={!hasPrev}
        onClick={() => onSwitch(branchOptions[currentIndex - 1])}
        className="hover:text-gray-900 disabled:opacity-30"
        aria-label="上一个版本"
      >

      </button>
      <span className="min-w-[3ch] text-center">
        {currentIndex + 1}/{branchOptions.length}
      </span>
      <button
        disabled={!hasNext}
        onClick={() => onSwitch(branchOptions[currentIndex + 1])}
        className="hover:text-gray-900 disabled:opacity-30"
        aria-label="下一个版本"
      >

      </button>
    </div>
  );
}
当用户点击分支箭头时,调用 stream.setBranch(branchId) 将对话视图切换到该分支。由于所有分支数据已通过 fetchStateHistory: true 加载,所以切换是即时的。
切换分支不仅影响目标消息,还会影响所有后续消息。如果你切换到消息 3 的不同版本,消息 4、5、6 等也会更新,以反映该版本之后的对话内容。

分支的底层工作原理

LangGraph 将每个状态转换持久化为一个检查点。当你使用 checkpoint 参数提交时,后端从该点分叉,而不是追加到当前对话。结果是一个树状结构:
用户: "什么是 React?"
  └─ AI: "React 是一个 JavaScript 库..." (分支 A)
  └─ AI: "React 是一个 UI 框架..." (分支 B,重新生成)

用户: "讲讲 hooks" (分支 A)
  └─ AI: "Hooks 是一些函数..."

用户: "讲讲 JSX" (从分支 A 编辑)
  └─ AI: "JSX 是一种语法扩展..."
每个分支都是对话树中的独立路径。切换分支会更新显示的消息,但不会删除任何数据。所有分支都持久化在检查点存储中。

完整的消息组件

这是一个结合了消息显示、编辑、重新生成和分支切换的完整组件:
function MessageWithBranching({
  message,
  metadata,
  stream,
}: {
  message: BaseMessage;
  metadata: MessageMetadata;
  stream: ReturnType<typeof useStream>;
}) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(message.content as string);

  const isHuman = message._getType() === "human";
  const isAI = message._getType() === "ai";
  const hasBranches = metadata.branchOptions.length > 1;

  return (
    <div className="group relative py-2">
      {isEditing ? (
        <EditForm
          text={editText}
          onChange={setEditText}
          onSave={() => {
            handleEdit(stream, message as HumanMessage, metadata, editText);
            setIsEditing(false);
          }}
          onCancel={() => {
            setEditText(message.content as string);
            setIsEditing(false);
          }}
        />
      ) : (
        <>
          <div className={isHuman ? "text-right" : "text-left"}>
            <div
              className={
                isHuman
                  ? "inline-block rounded-lg bg-blue-600 px-4 py-2 text-white"
                  : "inline-block rounded-lg bg-gray-100 px-4 py-2"
              }
            >
              {message.content as string}
            </div>
          </div>

          <div className="mt-1 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
            {isHuman && (
              <button
                className="text-xs text-gray-400 hover:text-gray-700"
                onClick={() => setIsEditing(true)}
              >
                编辑
              </button>
            )}

            {isAI && (
              <button
                className="text-xs text-gray-400 hover:text-gray-700"
                onClick={() =>
                  handleRegenerate(stream, metadata)
                }
              >
                重新生成
              </button>
            )}

            {hasBranches && (
              <BranchSwitcher
                metadata={metadata}
                onSwitch={(id) => stream.setBranch(id)}
              />
            )}
          </div>
        </>
      )}
    </div>
  );
}

function EditForm({
  text,
  onChange,
  onSave,
  onCancel,
}: {
  text: string;
  onChange: (text: string) => void;
  onSave: () => void;
  onCancel: () => void;
}) {
  return (
    <div className="space-y-2">
      <textarea
        className="w-full rounded-lg border p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
        value={text}
        onChange={(e) => onChange(e.target.value)}
        rows={3}
      />
      <div className="flex gap-2">
        <button
          className="rounded bg-blue-600 px-4 py-1.5 text-sm text-white hover:bg-blue-700"
          onClick={onSave}
        >
          保存并重新运行
        </button>
        <button
          className="rounded border px-4 py-1.5 text-sm hover:bg-gray-50"
          onClick={onCancel}
        >
          取消
        </button>
      </div>
    </div>
  );
}

结合乐观更新

将分支与乐观更新结合,获得无缝的编辑体验。当用户保存编辑时,在服务器响应之前乐观地显示更新后的消息:
function handleEditOptimistic(
  stream: ReturnType<typeof useStream>,
  originalMsg: HumanMessage,
  metadata: MessageMetadata,
  newText: string
) {
  const checkpoint = metadata.firstSeenState?.parent_checkpoint;
  if (!checkpoint) return;

  const updatedMsg = { ...originalMsg, content: newText };

  stream.submit(
    { messages: [updatedMsg] },
    {
      checkpoint,
      optimisticValues: (prev) => {
        if (!prev?.messages) return { messages: [updatedMsg] };

        const idx = prev.messages.findIndex((m) => m.id === originalMsg.id);
        if (idx === -1) return prev;

        return {
          ...prev,
          messages: [...prev.messages.slice(0, idx), updatedMsg],
        };
      },
    }
  );
}

添加键盘导航

为高级用户添加键盘快捷键来导航分支:
useEffect(() => {
  function handleKeyDown(e: KeyboardEvent) {
    if (!focusedMessageMetadata) return;

    const { branch, branchOptions } = focusedMessageMetadata;
    const idx = branchOptions.indexOf(branch);

    if (e.altKey && e.key === "ArrowLeft" && idx > 0) {
      stream.setBranch(branchOptions[idx - 1]);
    }
    if (e.altKey && e.key === "ArrowRight" && idx < branchOptions.length - 1) {
      stream.setBranch(branchOptions[idx + 1]);
    }
  }

  window.addEventListener("keydown", handleKeyDown);
  return () => window.removeEventListener("keydown", handleKeyDown);
}, [focusedMessageMetadata, stream]);
Alt + ← / Alt + → 是分支导航的自然映射,因为它类似于浏览器的前进/后退导航。

最佳实践

  • 始终启用 fetchStateHistory:没有它,getMessagesMetadata 无法返回分支信息。
  • 仅在有多个分支时显示分支切换器1/1 的指示器只会增加界面杂乱而无实际价值。
  • 悬停时显示分支控件:分支导航箭头和编辑按钮应在悬停时显示,以保持界面整洁。
  • 保持分支切换器紧凑:它与消息控件内联显示,不应占据界面主导地位。
  • 保持滚动位置:切换分支时,尽量将视口锚定在发生变化的消息上。
  • 标示活动分支:使用细微的视觉提示(如彩色圆点或分支标签)让用户知道正在查看哪个分支。
  • 流式输出时禁用控件:在智能体正在流式输出回复时,不要允许编辑或重新生成。在启用这些操作前检查 stream.isLoading
  • 取消时保留编辑文本:如果用户开始编辑后取消,将文本区域重置为原始消息内容。
  • 测试深层分支树:频繁编辑和重新生成的用户可能创建许多分支。确保分支切换器和数据处理保持良好性能。