以编程方式使用文档

概述

LangGraph 通过以下方式支持时间旅行 检查点:

  • - **重放**:从先前的检查点重试。
  • - **分叉**:从先前的检查点分叉并修改状态以探索替代路径。

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

重放

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

!重放

使用 getStateHistory 查找要重放的检查点,然后使用该检查点的配置调用 invoke

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 });

// Step 1: Run the graph
const config = { configurable: { thread_id: uuid7() } };
const result = await graph.invoke({}, config);

// Step 2: Find a checkpoint to replay from
const states = [];
for await (const state of graph.getStateHistory(config)) {
  states.push(state);
}

// Step 3: Replay from a specific checkpoint
const beforeJoke = states.find((s) => s.next.includes("writeJoke"));
const replayResult = await graph.invoke(null, beforeJoke.config);
// writeJoke re-executes (runs again), generateTopic does not

分叉

分叉从过去的检查点创建具有修改状态的新分支。调用 update_state 在先前检查点上创建分叉,然后使用 invoke 配合 None 继续执行。

!分叉

// Find checkpoint before writeJoke
const states = [];
for await (const state of graph.getStateHistory(config)) {
  states.push(state);
}
const beforeJoke = states.find((s) => s.next.includes("writeJoke"));

// Fork: update state to change the topic
const forkConfig = await graph.updateState(
  beforeJoke.config,
  { topic: "chickens" },
);

// Resume from the fork — writeJoke re-executes with the new topic
const forkResult = await graph.invoke(null, forkConfig);
console.log(forkResult.joke); // A joke about chickens, not socks

当调用 @[

] 时,值使用指定节点的写入器(包括update_state归约器 )应用。检查点记录该节点已产生更新,执行从该节点的后继节点恢复。默认情况下,LangGraph 从检查点的版本历史推断

。从特定检查点分叉时,此推断几乎总是正确的。 as_node 明确指定

的时机: as_node 并行分支

  • :多个节点在同一步骤中更新了状态,且 LangGraph 无法确定哪个是最后一个(无执行历史InvalidUpdateError).
  • :在新线程上设置状态(常见于测试 ).
  • 跳过节点:设置为后续节点,使图认为该节点已运行。 as_node Set
// graph: generateTopic -> writeJoke

// Treat this update as if generateTopic produced it.
// Execution resumes at writeJoke (the successor of generateTopic).
const forkConfig = await graph.updateState(
  beforeJoke.config,
  { topic: "chickens" },
  { asNode: "generateTopic" },
);

中断

如果你的图使用 interrupt 进行 human-in-the-loop 工作流,中断在时间旅行期间总是会被重新触发。包含中断的节点会重新执行,并 interrupt() 为新的 Command(resume=...).

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

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

// ... build graph with checkpointer ...

// First run: hits interrupt
await graph.invoke({ value: [] }, config);
// Resume with answer
await graph.invoke(new Command({ resume: "Alice" }), config);

// Replay from before 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);
// Pauses at interrupt — waiting for new Command({ resume: ... })

// Fork from before askHuman
const forkConfig = await graph.updateState(beforeAsk.config, { value: ["forked"] });
const forkResult = await graph.invoke(null, forkConfig);
// Pauses at interrupt — waiting for new Command({ resume: ... })

// Resume the forked interrupt with a different answer
await graph.invoke(new Command({ resume: "Bob" }), forkConfig);
// Result: { value: ["forked", "Hello, Bob!", "Done"] }

多个中断

如果你的图在多个点收集输入(例如,多步骤表单),你可以在中断之间进行分支,更改后续答案而无需重新询问之前的问题。

// Fork from BETWEEN the two interrupts (after askName, before 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 result preserved ("name:Alice")
// askAge pauses at interrupt — waiting for new answer

子图

使用 子图 进行时间旅行取决于子图是否有自己的检查点器。这决定了你可以从中进行时间旅行的检查点粒度。

Inherited checkpointer (default)

默认情况下,子图继承父级的检查点器。父级将整个子图视为 **单个超级步骤** — 整个子图执行只有一个父级检查点。从子图重新从头执行之前的时间点进行时间旅行。

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

// Subgraph without its own checkpointer (default)
const subgraph = new StateGraph(StateAnnotation)
  .addNode("stepA", stepA)       // Has interrupt()
  .addNode("stepB", stepB)       // Has interrupt()
  .addEdge(START, "stepA")
  .addEdge("stepA", "stepB")
  .compile();  // No checkpointer — inherits from parent

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

// Complete both interrupts
await graph.invoke({ value: [] }, config);
await graph.invoke(new Command({ resume: "Alice" }), config);
await graph.invoke(new Command({ resume: "30" }), config);

// Time travel from before the subgraph
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);
// The entire subgraph re-executes from scratch
// You cannot time travel to a point between stepA and stepB

Subgraph checkpointer

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

使用 get_statesubgraphs=True 访问子图自己的检查点配置,然后从中分叉:

// Subgraph with its own checkpointer
const subgraph = new StateGraph(StateAnnotation)
  .addNode("stepA", stepA)       // Has interrupt()
  .addNode("stepB", stepB)       // Has interrupt()
  .addEdge(START, "stepA")
  .addEdge("stepA", "stepB")
  .compile({ checkpointer: true });  // Own checkpoint history

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

// Run until stepA interrupt, then resume -> hits stepB interrupt
await graph.invoke({ value: [] }, config);
await graph.invoke(new Command({ resume: "Alice" }), config);

// Get the subgraph's own checkpoint (between stepA and stepB)
const parentState = await graph.getState(config, { subgraphs: true });
const subConfig = parentState.tasks[0].state.config;

// Fork from the subgraph checkpoint
const forkConfig = await graph.updateState(subConfig, { value: ["forked"] });
const result = await graph.invoke(null, forkConfig);
// stepB re-executes, stepA's result is preserved

参见 子图持久化 了解更多关于配置子图检查点程序的信息。