Skip to content

与 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 是消息之前的那一个检查点。将其用作编辑和重新生成的分叉点。

编辑消息

要编辑用户消息并分叉对话:

  1. 从消息的元数据中获取 parentCheckpointId
  2. 使用 forkFrom: { checkpointId } 提交编辑后的消息
  3. 智能体会从该点重新运行
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 回复:

  1. 从 AI 消息的元数据中获取 parent_checkpoint
  2. 使用空输入和 forkFrom: { checkpointId } 提交
  3. 智能体会从该点产生一条新的回复
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 重置为原始消息内容。
  • 使用深层的检查点树进行测试:经常编辑和重新生成的用户可能会创建许多路径。确保时间线渲染保持高性能。