Skip to content

分支对话 ​

通过从检查点派生分支,编辑消息并重新生成回答

与 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 后端的类型推断。

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 (
    <div>
      {stream.messages.map((msg) => (
        <MessageWithForkControls key={msg.id} stream={stream} message={msg} />
      ))}
    </div>
  );
}
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",
});
`&lt;/script&gt;`

<template>
  <div>
    <MessageWithForkControls
      v-for="msg in stream.messages.value"
      :key="msg.id"
      :stream="stream"
      :message="msg"
    />
  </div>
</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",
  });
`&lt;/script&gt;`

<div>
  {#each stream.messages as msg (msg.id)}
    <Message
      message={msg}
      {stream}
    />
  {/each}
</div>
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. Agent 从该节点重新运行
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 } }
  );
}

编辑之后:

  • Agent 携带更新后的消息从分叉点重新运行
  • 原始路径在线程(thread)历史记录中依然可用

重新生成回答 ​

要在不更改输入的情况下重新生成 AI 回答:

  1. 从 AI 消息的元数据中获取 parent_checkpoint
  2. 使用空输入并携带 forkFrom: { checkpointId } 进行提交
  3. Agent 从该节点生成全新的回答
ts
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)重置为原始消息内容。
  • 在深层检查点树下进行测试:频繁编辑和重新生成的用户可能会创建许多路径。请确保时间线渲染始终保持高性能。