分支对话

编辑消息、重新生成回复、在对话分支间自由切换

与 AI 智能体的对话很少是线性的。你可能想要改写问题、重新生成不满意的回复,或探索完全不同的对话方向,同时不丢失之前的内容。
分支对话为聊天界面带来了类似版本控制的能力:每次编辑都会创建新分支,你可以在不同分支间自由切换。

该功能需要 LangGraph Agent Server 支持。
使用 langgraph dev 本地运行智能体,或部署到 LangSmith 即可使用。


什么是分支对话?

分支对话将对话视为一棵树,而不是一个列表。
每条消息都是一个节点,编辑消息或重新生成回复会从该点创建分叉。
原始路径会作为同级分支保留,用户可在不同对话轨迹之间来回切换。

核心能力:

  • 编辑任意用户消息:重写历史提示,从该点重新运行智能体
  • 重新生成任意 AI 回复:对同一输入让智能体生成不同答案
  • 分支导航:通过消息级分支控件切换不同对话版本

配置带历史记录的 useStream

要启用分支功能,需传入 fetchStateHistory: true,让 useStream 获取分支操作所需的检查点元数据。

import type { BaseMessage } from "@langchain/core/messages";

interface AgentState {
  messages: BaseMessage[];
}

React 示例

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 列表
parent_checkpoint 这条消息之前的检查点,用作编辑/重新生成的分叉点

当消息只有一个版本时,branchOptions 只有一项;
编辑或重新生成后,会新增分支 ID,可在多个版本间切换。


编辑消息(创建新分支)

编辑用户消息并创建新分支:

  1. 从消息元数据获取 parent_checkpoint
  2. 使用该检查点提交编辑后的消息
  3. 智能体从该点重新运行,创建新分支
function handleEdit(
  stream,
  originalMsg,
  metadata,
  newText
) {
  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,
  metadata
) {
  const checkpoint = metadata.firstSeenState?.parent_checkpoint;
  if (!checkpoint) return;

  stream.submit(undefined, { checkpoint });
}

每次重新生成都会为该位置的 AI 消息创建新分支,用户可对比不同回复。


构建分支切换器

当消息存在多个分支时,显示紧凑的内联控件,展示当前版本序号与导航箭头。

function BranchSwitcher({ metadata, onSwitch }) {
  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])}
      >
        ◀
      </button>
      <span className="min-w-[3ch] text-center">
        {currentIndex + 1}/{branchOptions.length}
      </span>
      <button
        disabled={!hasNext}
        onClick={() => onSwitch(branchOptions[currentIndex + 1])}
      >
        ▶
      </button>
    </div>
  );
}

调用 stream.setBranch(branchId) 可瞬间切换分支(所有数据已预加载)。


底层原理:分支如何工作

LangGraph 将每一次状态变化都保存为检查点
当你带 checkpoint 参数提交时,后端会从该点分叉,而不是追加到当前对话。最终形成树结构:

用户:React 是什么?
└─ AI:React 是一个 JS 库…(分支A)
   └─ AI:React 是一个 UI 框架…(分支B,重新生成)
用户:讲讲 Hooks(分支A)
└─ AI:Hooks 是函数…
用户:讲讲 JSX(从A编辑而来)
└─ AI:JSX 是一种语法扩展…

所有分支都会持久保留,切换分支不会删除任何数据。


完整消息组件(含编辑/重生成/分支切换)

可直接复制使用:

function MessageWithBranching({ message, metadata, stream }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(message.content);
  const isHuman = message._getType() === "human";
  const isAI = message._getType() === "ai";
  const hasBranches = metadata.branchOptions.length > 1;

  return (
    <div className="relative py-2">
      {isEditing ? (
        <EditForm
          text={editText}
          onChange={setEditText}
          onSave={() => {
            handleEdit(stream, message, metadata, editText);
            setIsEditing(false);
          }}
          onCancel={() => {
            setEditText(message.content);
            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}
            </div>
          </div>

          <div className="mt-1 flex items-center gap-2 opacity-0 group-hover:opacity-100">
            {isHuman && (
              <button className="text-xs text-gray-400" onClick={() => setIsEditing(true)}>
                编辑
              </button>
            )}
            {isAI && (
              <button className="text-xs text-gray-400" onClick={() => handleRegenerate(stream, metadata)}>
                重生成
              </button>
            )}
            {hasBranches && (
              <BranchSwitcher
                metadata={metadata}
                onSwitch={(id) => stream.setBranch(id)}
              />
            )}
          </div>
        </>
      )}
    </div>
  );
}

function EditForm({ text, onChange, onSave, onCancel }) {
  return (
    <div className="space-y-2">
      <textarea
        className="w-full rounded-lg border p-3"
        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-white" onClick={onSave}>
          保存并重新运行
        </button>
        <button className="rounded border px-4 py-1.5" onClick={onCancel}>
          取消
        </button>
      </div>
    </div>
  );
}

最佳实践

  1. 必须开启 fetchStateHistory,否则无法获取分支信息
  2. 仅在有多分支时显示分支切换器,避免冗余
  3. 悬停显示控件,保持界面简洁
  4. 分支切换器保持紧凑,内联展示
  5. 切换分支时保持滚动位置
  6. 明确标识当前分支(颜色点/标签)
  7. 流式输出时禁用编辑/重生成
  8. 取消编辑时恢复原文本
  9. 深度分支树也要保证性能
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐