Skip to content
LangChain 文档版本 跟随上游 源快照 1b37992a 更新于 2026-10-08

使用时间旅行

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

概述

LangGraph 通过检查点支持时间旅行:

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

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

重放

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

WARNING

重放会重新执行节点——它并不只是从缓存中读取。LLM 调用、API 请求和中断会再次触发,并可能返回不同的结果。从最终检查点(没有 next 节点)重放是无操作。

重放

使用 get_state_history 找到你想要重放来源的检查点,然后用该检查点的 config 调用 invoke:

python
from langgraph.graph import StateGraph, START
from langgraph.checkpoint.memory import InMemorySaver
from typing_extensions import TypedDict, NotRequired
from langchain_core.utils.uuid import uuid7

class State(TypedDict):
    topic: NotRequired[str]
    joke: NotRequired[str]

def generate_topic(state: State):
    return {"topic": "socks in the dryer"}

def write_joke(state: State):
    return {"joke": f"Why do {state['topic']} disappear? They elope!"}

checkpointer = InMemorySaver()
graph = (
    StateGraph(State)
    .add_node("generate_topic", generate_topic)
    .add_node("write_joke", write_joke)
    .add_edge(START, "generate_topic")
    .add_edge("generate_topic", "write_joke")
    .compile(checkpointer=checkpointer)
)

# Step 1: Run the graph
config = {"configurable": {"thread_id": str(uuid7())}}
result = graph.invoke({}, config)

# Step 2: Find a checkpoint to replay from
history = list(graph.get_state_history(config))
# History is in reverse chronological order
for state in history:
    print(f"next={state.next}, checkpoint_id={state.config['configurable']['checkpoint_id']}")

# Step 3: Replay from a specific checkpoint
# Find the checkpoint before write_joke
before_joke = next(s for s in history if s.next == ("write_joke",))
replay_result = graph.invoke(None, before_joke.config)
# write_joke re-executes (runs again), generate_topic does not

分叉

分叉会从过去某个经过修改状态的检查点创建新分支。在先前的检查点上调用 update_state 以创建分叉,然后用 None 调用 invoke 以继续执行。

分叉

WARNING

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

python
# Find checkpoint before write_joke
history = list(graph.get_state_history(config))
before_joke = next(s for s in history if s.next == ("write_joke",))

# Fork: update state to change the topic
fork_config = graph.update_state(
    before_joke.config,
    values={"topic": "chickens"},
)

# Resume from the fork — write_joke re-executes with the new topic
fork_result = graph.invoke(None, fork_config)
print(fork_result["joke"])  # A joke about chickens, not socks

从特定节点

当你调用 update_state 时,值会使用指定节点的写入器(包括归约器)来应用。检查点会记录该节点产生了此更新,执行将从该节点的后继节点继续。

默认情况下,LangGraph 会从检查点的版本历史中推断 as_node。当从特定检查点分叉时,这种推断几乎总是正确的。

在以下情况下应显式指定 as_node:

  • 并行分支:多个节点在同一步中更新了状态,LangGraph 无法确定哪个是最后一个(InvalidUpdateError)。
  • 没有执行历史:在全新线程上设置状态(常见于测试)。
  • 跳过节点:将 as_node 设置为靠后的节点,让图认为该节点已经运行过。
python
# graph: generate_topic -> write_joke

# Treat this update as if generate_topic produced it.
# Execution resumes at write_joke (the successor of generate_topic).
fork_config = graph.update_state(
    before_joke.config,
    values={"topic": "chickens"},
    as_node="generate_topic",
)

中断

如果你的图在人在回路工作流中使用 interrupt,那么在时间旅行期间中断总是会被重新触发。包含该中断的节点会重新执行,且 interrupt() 会暂停以等待新的 Command(resume=...)。

python
from langgraph.types import interrupt, Command

class State(TypedDict):
    value: list[str]

def ask_human(state: State):
    answer = interrupt("What is your name?")
    return {"value": [f"Hello, {answer}!"]}

def final_step(state: State):
    return {"value": ["Done"]}

graph = (
    StateGraph(State)
    .add_node("ask_human", ask_human)
    .add_node("final_step", final_step)
    .add_edge(START, "ask_human")
    .add_edge("ask_human", "final_step")
    .compile(checkpointer=InMemorySaver())
)

config = {"configurable": {"thread_id": "1"}}

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

# Replay from before ask_human
history = list(graph.get_state_history(config))
before_ask = [s for s in history if s.next == ("ask_human",)][-1]

replay_result = graph.invoke(None, before_ask.config)
# Pauses at interrupt — waiting for new Command(resume=...)

# Fork from before ask_human
fork_config = graph.update_state(before_ask.config, {"value": ["forked"]})
fork_result = graph.invoke(None, fork_config)
# Pauses at interrupt — waiting for new Command(resume=...)

# Resume the forked interrupt with a different answer
graph.invoke(Command(resume="Bob"), fork_config)
# Result: {"value": ["forked", "Hello, Bob!", "Done"]}

多个中断

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

python
def ask_name(state):
    name = interrupt("What is your name?")
    return {"value": [f"name:{name}"]}

def ask_age(state):
    age = interrupt("How old are you?")
    return {"value": [f"age:{age}"]}

# Graph: ask_name -> ask_age -> final
# After completing both interrupts:

# Fork from BETWEEN the two interrupts (after ask_name, before ask_age)
history = list(graph.get_state_history(config))
between = [s for s in history if s.next == ("ask_age",)][-1]

fork_config = graph.update_state(between.config, {"value": ["modified"]})
result = graph.invoke(None, fork_config)
# ask_name result preserved ("name:Alice")
# ask_age pauses at interrupt — waiting for new answer

子图

对子图进行时间旅行取决于该子图是否拥有自己的检查点器。这决定了你可以从哪个粒度的检查点进行时间旅行。

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

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

python
# Subgraph without its own checkpointer (default)
subgraph = (
    StateGraph(State)
    .add_node("step_a", step_a)       # Has interrupt()
    .add_node("step_b", step_b)       # Has interrupt()
    .add_edge(START, "step_a")
    .add_edge("step_a", "step_b")
    .compile()  # No checkpointer — inherits from parent
)

graph = (
    StateGraph(State)
    .add_node("subgraph_node", subgraph)
    .add_edge(START, "subgraph_node")
    .compile(checkpointer=InMemorySaver())
)

config = {"configurable": {"thread_id": "1"}}

# Complete both interrupts
graph.invoke({"value": []}, config)            # Hits step_a interrupt
graph.invoke(Command(resume="Alice"), config)  # Hits step_b interrupt
graph.invoke(Command(resume="30"), config)     # Completes

# Time travel from before the subgraph
history = list(graph.get_state_history(config))
before_sub = [s for s in history if s.next == ("subgraph_node",)][-1]

fork_config = graph.update_state(before_sub.config, {"value": ["forked"]})
result = graph.invoke(None, fork_config)
# The entire subgraph re-executes from scratch
# You cannot time travel to a point between step_a and step_b

有关配置子图检查点器的更多信息,请参阅子图持久化。


BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。