> ## Documentation Index
> Fetch the complete documentation index at: https://nvd-54.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 使用时间旅行

> 重放过去的执行并分叉以探索 LangGraph 中的替代路径

## 概述

LangGraph 通过[检查点](/oss/javascript/langgraph/persistence#checkpoints)支持时间旅行：

* **[重放](#replay)**：从先前的检查点重试。
* **[分叉](#fork)**：从先前的检查点以修改后的状态分支，以探索替代路径。

两者都通过从先前的检查点恢复来工作。检查点之前的节点不会重新执行（结果已保存）。检查点之后的节点重新执行，包括任何 LLM 调用、API 请求和[中断](/oss/javascript/langgraph/interrupts)（可能产生不同的结果）。

## 重放

使用先前检查点的配置调用图以从该点重放。

<Warning>
  重放会重新执行节点——它不只是从缓存中读取。LLM 调用、API 请求和[中断](/oss/javascript/langgraph/interrupts)会再次触发，可能返回不同的结果。从最终检查点（没有 `next` 节点）重放是无操作的。
</Warning>

<img src="https://mintcdn.com/nvd-54/KKmYhbrpyTWf5iQl/oss/images/re_play.png?fit=max&auto=format&n=KKmYhbrpyTWf5iQl&q=85&s=33cdbe5b44f7be06f44f48a35180dc19" alt="重放" width="2276" height="986" data-path="oss/images/re_play.png" />

使用 [`getStateHistory`](https://reference.langchain.com/javascript/classes/_langchain_langgraph.pregel.Pregel.html#getStateHistory) 找到你想要重放的检查点，然后使用该检查点的配置调用 [`invoke`](https://reference.langchain.com/javascript/classes/_langchain_langgraph.index.CompiledStateGraph.html#invoke)：

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { v7 as uuid7 } from "uuid";
import { StateGraph, MemorySaver, START } from "@langchain/langgraph";

const StateAnnotation = Annotation.Root({
  topic: Annotation<string>(),
  joke: Annotation<string>(),
});

function generateTopic(state: typeof StateAnnotation.State) {
  return { topic: "socks in the dryer" };
}

function writeJoke(state: typeof StateAnnotation.State) {
  return { joke: `Why do ${state.topic} disappear? They elope!` };
}

const checkpointer = new MemorySaver();
const graph = new StateGraph(StateAnnotation)
  .addNode("generateTopic", generateTopic)
  .addNode("writeJoke", writeJoke)
  .addEdge(START, "generateTopic")
  .addEdge("generateTopic", "writeJoke")
  .compile({ checkpointer });

// 步骤 1：运行图
const config = { configurable: { thread_id: uuid7() } };
const result = await graph.invoke({}, config);

// 步骤 2：找到要重放的检查点
const states = [];
for await (const state of graph.getStateHistory(config)) {
  states.push(state);
}

// 步骤 3：从特定检查点重放
const beforeJoke = states.find((s) => s.next.includes("writeJoke"));
const replayResult = await graph.invoke(null, beforeJoke.config);
// writeJoke 重新执行（再次运行），generateTopic 不会
```

## 分叉

分叉从过去的检查点以修改后的状态创建新分支。在先前的检查点上调用 [`update_state`](https://reference.langchain.com/javascript/classes/_langchain_langgraph.pregel.Pregel.html#updateState) 创建分叉，然后使用 `None` 调用 [`invoke`](https://reference.langchain.com/javascript/classes/_langchain_langgraph.index.CompiledStateGraph.html#invoke) 继续执行。

<img src="https://mintcdn.com/nvd-54/u9mURd9IHXggg6bM/oss/images/checkpoints_full_story.jpg?fit=max&auto=format&n=u9mURd9IHXggg6bM&q=85&s=1db1775de67d5fcd25ebe535b0f3610c" alt="分叉" width="3705" height="2598" data-path="oss/images/checkpoints_full_story.jpg" />

<Warning>
  `update_state` **不会**回滚线程。它创建一个从指定点分支的新检查点。原始执行历史保持不变。
</Warning>

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// 找到 writeJoke 之前的检查点
const states = [];
for await (const state of graph.getStateHistory(config)) {
  states.push(state);
}
const beforeJoke = states.find((s) => s.next.includes("writeJoke"));

// 分叉：更新状态以更改主题
const forkConfig = await graph.updateState(
  beforeJoke.config,
  { topic: "chickens" },
);

// 从分叉恢复——writeJoke 使用新主题重新执行
const forkResult = await graph.invoke(null, forkConfig);
console.log(forkResult.joke); // 关于鸡的笑话，而不是袜子
```

### 从特定节点

当你调用 [`update_state`](https://reference.langchain.com/javascript/classes/_langchain_langgraph.pregel.Pregel.html#updateState) 时，值通过指定节点的写入器（包括[归约器](/oss/javascript/langgraph/graph-api#reducers)）应用。检查点记录该节点产生了更新，执行从该节点的后继节点恢复。

默认情况下，LangGraph 从检查点的版本历史推断 `as_node`。从特定检查点分叉时，此推断几乎总是正确的。

在以下情况下显式指定 `as_node`：

* **并行分支**：多个节点在同一步骤中更新了状态，LangGraph 无法确定哪个是最后的（`InvalidUpdateError`）。
* **无执行历史**：在新线程上设置状态（在[测试](/oss/javascript/langgraph/test)中常见）。
* **跳过节点**：将 `as_node` 设置为后面的节点，使图认为该节点已经运行过。

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// 图：generateTopic -> writeJoke

// 将此更新视为 generateTopic 产生的。
// 执行从 writeJoke（generateTopic 的后继节点）恢复。
const forkConfig = await graph.updateState(
  beforeJoke.config,
  { topic: "chickens" },
  { asNode: "generateTopic" },
);
```

## 中断

如果你的图使用 [`interrupt`](https://reference.langchain.com/javascript/langchain-langgraph/index/interrupt) 进行[人机协作](/oss/javascript/langgraph/interrupts)工作流，中断在时间旅行期间始终会被重新触发。包含中断的节点会重新执行，`interrupt()` 暂停等待新的 `Command(resume=...)`。

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { interrupt, Command } from "@langchain/langgraph";

function askHuman(state: { value: string[] }) {
  const answer = interrupt("What is your name?");
  return { value: [`Hello, ${answer}!`] };
}

function finalStep(state: { value: string[] }) {
  return { value: ["Done"] };
}

// ... 使用检查点构建图 ...

// 首次运行：命中中断
await graph.invoke({ value: [] }, config);
// 使用答案恢复
await graph.invoke(new Command({ resume: "Alice" }), config);

// 从 askHuman 之前重放
const states = [];
for await (const state of graph.getStateHistory(config)) {
  states.push(state);
}
const beforeAsk = states.filter((s) => s.next.includes("askHuman")).pop();

const replayResult = await graph.invoke(null, beforeAsk.config);
// 在中断处暂停——等待新的 Command({ resume: ... })

// 从 askHuman 之前分叉
const forkConfig = await graph.updateState(beforeAsk.config, { value: ["forked"] });
const forkResult = await graph.invoke(null, forkConfig);
// 在中断处暂停——等待新的 Command({ resume: ... })

// 使用不同的答案恢复分叉的中断
await graph.invoke(new Command({ resume: "Bob" }), forkConfig);
// 结果：{ value: ["forked", "Hello, Bob!", "Done"] }
```

### 多个中断

如果你的图在多个点收集输入（例如，多步骤表单），你可以从中断之间分叉以更改后面的答案，而无需重新询问前面的问题。

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// 从两个中断之间分叉（在 askName 之后，askAge 之前）
const states = [];
for await (const state of graph.getStateHistory(config)) {
  states.push(state);
}
const between = states.filter((s) => s.next.includes("askAge")).pop();

const forkConfig = await graph.updateState(between.config, { value: ["modified"] });
const result = await graph.invoke(null, forkConfig);
// askName 结果保留（"name:Alice"）
// askAge 在中断处暂停——等待新答案
```

## 子图

使用[子图](/oss/javascript/langgraph/use-subgraphs)进行时间旅行取决于子图是否有自己的检查点。这决定了你可以从哪个粒度的检查点进行时间旅行。

<Tabs>
  <Tab title="继承的检查点（默认）">
    默认情况下，子图继承父图的检查点。父图将整个子图视为**单个超级步骤**——整个子图执行只有一个父级检查点。从子图之前进行时间旅行会从头开始重新执行它。

    你无法时间旅行到默认子图中节点*之间*的某个点——你只能从父级进行时间旅行。

    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
    // 没有自己检查点的子图（默认）
    const subgraph = new StateGraph(StateAnnotation)
      .addNode("stepA", stepA)       // 有 interrupt()
      .addNode("stepB", stepB)       // 有 interrupt()
      .addEdge(START, "stepA")
      .addEdge("stepA", "stepB")
      .compile();  // 无检查点——从父图继承

    const graph = new StateGraph(StateAnnotation)
      .addNode("subgraphNode", subgraph)
      .addEdge(START, "subgraphNode")
      .compile({ checkpointer });

    // 完成两个中断
    await graph.invoke({ value: [] }, config);
    await graph.invoke(new Command({ resume: "Alice" }), config);
    await graph.invoke(new Command({ resume: "30" }), config);

    // 从子图之前进行时间旅行
    const states = [];
    for await (const state of graph.getStateHistory(config)) {
      states.push(state);
    }
    const beforeSub = states.filter((s) => s.next.includes("subgraphNode")).pop();

    const forkConfig = await graph.updateState(beforeSub.config, { value: ["forked"] });
    const result = await graph.invoke(null, forkConfig);
    // 整个子图从头开始重新执行
    // 你无法时间旅行到 stepA 和 stepB 之间的某个点
    ```
  </Tab>

  <Tab title="子图检查点">
    在子图上设置 `checkpointer=True` 以给它自己的检查点历史。这会在子图**内部**的每个步骤创建检查点，允许你从其内部的特定点进行时间旅行——例如，在两个中断之间。

    使用 [`get_state`](https://reference.langchain.com/javascript/classes/_langchain_langgraph.pregel.Pregel.html#getState) 配合 `subgraphs=True` 访问子图自己的检查点配置，然后从中分叉：

    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
    // 有自己检查点的子图
    const subgraph = new StateGraph(StateAnnotation)
      .addNode("stepA", stepA)       // 有 interrupt()
      .addNode("stepB", stepB)       // 有 interrupt()
      .addEdge(START, "stepA")
      .addEdge("stepA", "stepB")
      .compile({ checkpointer: true });  // 自己的检查点历史

    const graph = new StateGraph(StateAnnotation)
      .addNode("subgraphNode", subgraph)
      .addEdge(START, "subgraphNode")
      .compile({ checkpointer });

    // 运行直到 stepA 中断，然后恢复 -> 命中 stepB 中断
    await graph.invoke({ value: [] }, config);
    await graph.invoke(new Command({ resume: "Alice" }), config);

    // 获取子图自己的检查点（在 stepA 和 stepB 之间）
    const parentState = await graph.getState(config, { subgraphs: true });
    const subConfig = parentState.tasks[0].state.config;

    // 从子图检查点分叉
    const forkConfig = await graph.updateState(subConfig, { value: ["forked"] });
    const result = await graph.invoke(null, forkConfig);
    // stepB 重新执行，stepA 的结果保留
    ```
  </Tab>
</Tabs>

有关配置子图检查点的更多信息，请参阅[子图持久化](/oss/javascript/langgraph/use-subgraphs#subgraph-persistence)。

***

<div className="source-links">
  <Callout icon="terminal-2">
    [将这些文档连接](/use-these-docs)到 Claude、VSCode 等工具，通过 MCP 获取实时答案。
  </Callout>

  <Callout icon="edit">
    [在 GitHub 上编辑此页面](https://github.com/langchain-ai/docs/edit/main/src/oss/langgraph/use-time-travel.mdx) 或 [提交问题](https://github.com/langchain-ai/docs/issues/new/choose)。
  </Callout>
</div>
