In LangGraph, each graph execution is automatically saved as a checkpoint snapshot.
You can go back to any moment in the past at any time - replay, debug, or go to a new execution route.
In the first few lessons of Module 3, we have learned about the mechanisms of breakpoints and human-in-the-loop.Time Travelis a natural extension of these capabilities: it allows you to not only "pause" the execution of the graph, but also operate it like a time machine,Return to any historical moment that has been executed, and start again from there.
LangGraph After each graph execution step, it will becheckpointerAutomatically save the current complete state as a snapshot. These snapshots form a timeline, and each snapshot has a uniquecheckpoint_id. Time travel is realized based on this timeline.
Time travel consists of two operations in LangGraph:
· Replay: Starting from a certain past checkpoint and following the same path again, the result will be the same as history.
· Fork: Return to a certain checkpoint,Modifystate at that time, and then re-execute - this will produce a new branch of execution, the results may be completely different from the history.
This mechanism is highly similar to Git version control:Checkpoints are like commits, replaying is like checking out a historical commit and viewing it again, and forking is like creating a new branch from a historical commit and committing different changes.
| time travel operations | Trigger mode | execution behavior | Typical uses |
|---|---|---|---|
| Replay | Pass in existingcheckpoint_idRun chart |
Continue running from this checkpoint and take the original path | Reproduce the problem and verify the consistency of the results |
| Fork | update_stateRun after modifying the historical status |
Create a new checkpoint branch and take a new path | Experiment with different inputs and fix historical errors |
The notebook uses a simple "arithmetic helper" Agent to demonstrate time travel. This Agent can call three tools: addition, multiplication, and division. Its graph structure is exactly the same as Module 1's ReAct Agent. The core is that it enablesMemorySaverAs a checkpointer, this is a prerequisite for time travel.
from langchain_deepseek import ChatDeepSeek
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
def add(a: int, b: int) -> int:
"""Adds a and b."""
return a + b
def divide(a: int, b: int) -> float:
"""Divide a by b."""
return a / b
tools = [add, multiply, divide]
llm = ChatDeepSeek(model="deepseek-v4-pro")
llm_with_tools = llm.bind_tools(tools)
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.prebuilt import tools_condition, ToolNode
from langchain_core.messages import SystemMessage
# 系统提示,告诉 LLM 它是一个算术助手
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic on a set of inputs.")
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "assistant")
# ★ 关键:编译时传入 checkpointer,这样每步执行都会被持久化为快照
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
Time travel relies entirely on the existence of checkpoints.Without checkpointer, the graph ends when it is executed, and there is no history to go back.MemorySaver saves all checkpoints in memory and is suitable for development and debugging. In production environments, SqliteSaver or PostgresSaver is usually used to persist to the database.
from langchain_core.messages import HumanMessage
# 指定线程 ID,同一 thread_id 下的所有执行步骤都会被关联到同一时间线
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
thread = {"configurable": {"thread_id": "1"}}
# 流式执行,打印每步最后一条消息
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
After execution, it prints in sequence: Human message → AI decides to call multiply → tool returns 6 → AI finally answers "2 times 3 equals 6".every step of the processare automatically saved as a checkpoint snapshot.
After the graph is executed, you can view the historical status through two APIs:
graph.get_state(config): Get the current (latest) status snapshotgraph.get_state_history(config): Get the thread'sallHistorical checkpoints, sorted in reverse chronological order# 获取当前状态
current = graph.get_state({'configurable': {'thread_id': '1'}})
# 获取所有历史检查点(按时间倒序,索引 0 = 最新)
all_states = [s for s in graph.get_state_history(thread)]
print(len(all_states)) # 输出: 5 ← 5 个步骤产生 5 个检查点
"Multiply 2 and 3" produced a total of5 checkpoints:
Each checkpoint is aStateSnapshotObject, containing the following key fields:
# 查看倒数第二个检查点(最早的实际执行步骤)
snapshot = all_states[-2]
# ① values:该时刻图的完整状态(所有消息)
snapshot.values
# → {'messages': [HumanMessage(content='Multiply 2 and 3', id='4ee8c440...')]}
# ② next:从该快照继续执行时,下一步要去哪个节点
snapshot.next
# → ('assistant',)
# ③ config:该检查点的标识信息,包含 thread_id 和 checkpoint_id
snapshot.config
# → {'configurable': {'thread_id': '1',
# 'checkpoint_ns': '',
# 'checkpoint_id': '1ef6a440-a003-6c74-8000-8a2d82b0d126'}}
# ④ metadata:额外信息,如步骤编号、来源
snapshot.metadata
# → {'source': 'loop', 'writes': None, 'step': 0, 'parents': {}}
# ⑤ created_at:该检查点的创建时间
snapshot.created_at
# → '2024-09-03T22:29:52.988265+00:00'
# ⑥ parent_config:上一个检查点的标识(形成链式结构)
snapshot.parent_config
| Field | meaning | Uses in time travel |
|---|---|---|
values |
The complete state dictionary of the time graph | View the current message list, variable values, etc. |
next |
The node name tuple to be executed next | Determine whether the graph has ended, or confirm which node to take next |
config.checkpoint_id |
Unique ID for this checkpoint (UUID format) | This ID needs to be passed in when Replay or Fork |
metadata.step |
The sequence number of this step in the execution sequence | Quickly locate the position in the execution sequence |
parent_config |
config from the previous checkpoint | Trace forward along the checkpoint chain |
get_state_historyReturn list byreverse chronological orderArrange - Index0is the latest checkpoint, index-1(the last one) is the earliest checkpoint (when the graph is first started). Thereforeall_states[-2]It is the first step that is actually executed after the graph is started, that is, the moment when user input is received and the assistant node is about to be entered.
Replaying is the simplest operation in time travel: find a historical checkpoint, move itconfig(includescheckpoint_id) passed tograph.stream(), the graph will continue running from that point in time, and the execution path will be exactly the same as before.
fromall_statesSelect the target time. For example, chooseall_states[-2], that is, the moment after the graph is started and just receives the user message and is about to execute the assistant node.
Viewto_replay.valuesConfirm the content of the message at that time and viewto_replay.nextConfirm which node will be executed next.
The first parameter is passedNone(No new input is passed), the second parameter is passedto_replay.config, LangGraph will recognize that the checkpoint has been executed and automatically continue running from there.
# 步骤 1:选定要重放的历史检查点
to_replay = all_states[-2]
# 确认该时刻的状态(只有用户的输入消息)
to_replay.values
# → {'messages': [HumanMessage(content='Multiply 2 and 3')]}
# 确认下一步节点
to_replay.next
# → ('assistant',)
# 确认 checkpoint_id
to_replay.config
# → {'configurable': {'thread_id': '1',
# 'checkpoint_id': '1ef6a440-a003-6c74-8000-8a2d82b0d126'}}
# 步骤 2:从该检查点重放
# input=None 表示不注入新输入,让图从检查点状态继续
for event in graph.stream(None, to_replay.config, stream_mode="values"):
event['messages'][-1].pretty_print()
Output after replay:
================================ Human Message =================================
Multiply 2 and 3
================================== Ai Message ==================================
Tool Calls: multiply(a=2, b=3)
================================= Tool Message =================================
6
================================== Ai Message ==================================
The result of multiplying 2 and 3 is 6.
When you pass in acheckpoint_idWhen , LangGraph looks at the checkpoint's position in the timeline:
· If after this checkpointThere is a subsequent checkpoint(i.e. it has been executed originally), the figure will show the subsequent stepsreplay as is, LLM will not be called again.
· If after this checkpointNo subsequent checkpoints(The new checkpoint generated by the fork), the figure considers this to be a brand new execution starting point, which willReal operationsubsequent nodes.
The most direct use of replay isDebug and reproduce: When your Agent produces strange output during a certain run, you can accurately restore the execution environment at that time through replay and step by step check the input and output of each node without re-running the entire workflow.
Forking is more powerful than replaying: you don’t just go back in time;Modify the status at that time, and then starting from the modified state, let the graph take a new path. Modification operation passedgraph.update_state()Once completed, it will create a new checkpoint (new branch) based on the original checkpoint without affecting the original timeline.
When modifying the status,messagesFields have special behavior:MessagesState uses add_messages reducer, the default is to append new messages instead of replacing them. To overwrite (modify) an existing message, you mustProvide the same message ID. In this way, the reducer knows "this is a modification of an existing message" rather than "this is a new message".
# 步骤 1:选定要分叉的历史检查点(与重放相同)
to_fork = all_states[-2]
# 查看当时的消息和其 ID
to_fork.values["messages"]
# → [HumanMessage(content='Multiply 2 and 3', id='4ee8c440-0e4a-47d7-852f-06e2a6c4f84d')]
to_fork.config["configurable"]["checkpoint_id"]
# → '1ef6a440-a003-6c74-8000-8a2d82b0d126'
# 步骤 2:使用 update_state 修改该检查点的状态
# 关键:传入相同的消息 ID → 触发"覆盖"而非"追加"
fork_config = graph.update_state(
to_fork.config, # 指定在哪个检查点上做修改
{"messages": [HumanMessage(
content='Multiply 5 and 3', # ← 把"2 and 3"改成"5 and 3"
id=to_fork.values["messages"][0].id # ← 同一个消息 ID!
)]},
)
# update_state 返回新创建的分叉检查点的 config
fork_config
# → {'configurable': {'thread_id': '1',
# 'checkpoint_id': '1ef6a442-3661-62f6-8001-d3c01b96f98b'}} ← 全新 ID!
No ID provided (append):{"messages": [HumanMessage(content='Multiply 5 and 3')]}← Generate new ID, which will be after the original messageNewOne message, the status changes to 2 messages.
Provide same ID (override):{"messages": [HumanMessage(content='Multiply 5 and 3', id='4ee8c440...')]}← Find and replace the message, the status is still 1 message, the content has changed.
# 步骤 3:确认分叉后的状态
graph.get_state({'configurable': {'thread_id': '1'}}).values["messages"]
# → [HumanMessage(content='Multiply 5 and 3', id='4ee8c440...')] ← 内容已更新
# 步骤 4:从分叉检查点运行图(这次 LangGraph 会真实执行,而非重放)
for event in graph.stream(None, fork_config, stream_mode="values"):
event['messages'][-1].pretty_print()
Execution output after forking - notice that the problem has changed to "multiplying 5 and 3":
================================ Human Message =================================
Multiply 5 and 3
================================== Ai Message ==================================
Tool Calls: multiply(a=5, b=3)
================================= Tool Message =================================
15
================================== Ai Message ==================================
The result of multiplying 5 and 3 is 15.
The original timeline remains intact, and the bifurcated timeline is generated from the bifurcation pointindependent chain, both inthread_idSame butcheckpoint_idThe links are different. current status (get_state) will point to the latest written fork end.
In addition to native Python code, LangGraph also provides an HTTP API (viaLangGraph SDK), allowing you to also use time travel in LangSmith Studio/Production Services. The way this API works is highly symmetrical to Python code and the concepts are exactly the same.
Run in the module-3/studio directorylanggraph dev, Studio UI will be inhttps://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024Open. By connecting to the local server through the SDK, you can call the API to achieve time travel.
from langgraph_sdk import get_client
# 连接本地 LangGraph 服务
client = get_client(url="http://127.0.0.1:2024")
# 1. 先运行一次,产生历史
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
thread = await client.threads.create()
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input=initial_input,
stream_mode="updates",
):
# 处理流式输出...
pass
# 2. 获取历史检查点
states = await client.threads.get_history(thread['thread_id'])
to_replay = states[-2] # 取最早的实际执行检查点
to_replay['checkpoint_id']
# → '1ef6a449-817f-6b55-8000-07c18fbdf7c8'
# 3. 从该检查点重放(传入 checkpoint_id 参数)
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input=None, # 不传新输入
stream_mode="values",
checkpoint_id=to_replay['checkpoint_id'] # ← 关键:传入检查点 ID
):
print(f"Receiving event type: {chunk.event}")
print(chunk.data)
# 获取要分叉的检查点
states = await client.threads.get_history(thread['thread_id'])
to_fork = states[-2]
msg_id = to_fork['values']['messages'][0]['id'] # 原始消息的 ID
# 用 update_state 修改历史状态(需提供消息 ID 以触发覆盖)
forked_input = {"messages": HumanMessage(
content="Multiply 3 and 3",
id=msg_id # ← 同一消息 ID
)}
forked_config = await client.threads.update_state(
thread["thread_id"],
forked_input,
checkpoint_id=to_fork['checkpoint_id'] # ← 在哪个检查点上分叉
)
# forked_config 包含新创建的分叉检查点 ID
# 从分叉检查点运行(图会真实执行,计算 3×3=9)
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input=None,
stream_mode="updates",
checkpoint_id=forked_config['checkpoint_id'] # ← 分叉检查点 ID
):
# 输出:Multiply 3 and 3 → 9
pass
Python (local):graph.stream(None, to_replay.config)— config directly contains checkpoint_id
API (remote):client.runs.stream(..., checkpoint_id=...)— checkpoint_id is passed in as an independent parameter
The semantics of the two are exactly the same, but the interface form is slightly different. In the Studio UI, the same operation can be triggered by clicking the checkpoint with the mouse without writing code.
Time travel is not only an academic concept, it has extremely practical value when building production-level AI agents. The following are three core application scenarios:
Your Agent gives the wrong answer when handling a user request, but you can't reproduce the problem because the results are slightly different each time LLM is re-invoked.
Time travel solution:Find the error runthread_id, useget_state_historyList all checkpoints, locate the moment before the problem occurs, and use the checkpointconfigReplay. Since replay does not recall LLM (but instead reproduces it as-is), you can100% restoration of problem scenes, step by step check the input and output of each node.
# 找到问题线程
problem_thread = {"configurable": {"thread_id": "problem-thread-id"}}
# 列出所有检查点,找到问题节点前的那一刻
all_states = [s for s in graph.get_state_history(problem_thread)]
# 找到某个可疑的检查点
suspicious = all_states[3]
print(suspicious.next) # 确认接下来会执行什么
print(suspicious.values) # 查看当时的完整状态
# 从该点重放,观察后续执行
for event in graph.stream(None, suspicious.config, stream_mode="values"):
event['messages'][-1].pretty_print()
You want to test: How would the Agent's answer change if the user originally asked the question in a different way (such as changing the wording)? Or you want the same Agent to start from the same starting point but try using different tools or strategies.
Time travel solution:To return to the target checkpoint, useupdate_stateModify the input, create a fork, and run from the fork point. You can create multiple forks,A/B test different prompt words or inputs, all results are retained in the checkpoint chain for comparison.
A certain tool call of the Agent failed (such as API timeout, external service unavailability), causing the entire execution chain to be interrupted. You don't want to start over from scratch because a lot of valid work has already been done.
Time travel solution:Find the failing stepbeforecheckpoint, useupdate_stateManually inject a "fake success result" (the value that the simulation tool should have returned) and continue running from the modified fork point, skipping the failed tool call.
# 假设工具节点失败了,失败前的检查点是 all_states[2]
before_failure = all_states[2]
# 手动注入工具应当返回的结果(跳过真实调用)
from langchain_core.messages import ToolMessage
recovery_config = graph.update_state(
before_failure.config,
{"messages": [ToolMessage(
content="42", # 手动提供工具结果
tool_call_id="..." # 对应原始工具调用的 ID
)]},
as_node="tools" # 以 tools 节点的身份写入,保持元数据正确
)
# 从恢复检查点继续执行(LLM 会看到工具结果并继续推理)
for event in graph.stream(None, recovery_config, stream_mode="values"):
event['messages'][-1].pretty_print()
| Application scenarios | API used | core operations | value |
|---|---|---|---|
| Debug and reproduce | get_state_history + stream(None, config) |
replay | 100% restoration of the error scene |
| A/B experiment | update_state + stream(None, fork_config) |
bifurcation | Test multiple strategies from the same starting point |
| error recovery | update_state(as_node=...) + stream |
Fork + injection result | Continue execution bypassing failed steps |
compile(checkpointer=...)checkpointer is passed in, and each timestream/invokemust be provided whenthread_id. Without these two points, there would be no historical record and no time travel.Directly invoke again: The graph is re-executed from START. Both LLM and tools will be re-called. Due to the non-determinism of LLM, the results may be different.
replay: The graph continues from the historical checkpoint. LangGraph knows which steps have been "executed" and reproduces them as they are without re-calling LLM. The resultTotally sure, same as history。
exists.update_stateCreated is anew checkpoint node, hanging after a certain node in the original timeline, forming a new branch. The original checkpoint chain is completely unaffected and can still be passedget_state_historyView, you can also continue to replay or fork again from the original checkpoint.
MessagesState usesadd_messagesreducer, its rules are:If the ID of the new message is the same as the ID of the existing message, replace it; otherwise append it as a new message. Therefore, when modifying existing messages, you must start from the original checkpointvalues["messages"][i].idGet the ID and assign it to the new message.
Not here. MemorySaver is only saved in the memory of the Python process, and all data is lost after the process terminates. Should be used in production environmentsSqliteSaver(save to local SQLite file) orPostgresSaver(saved to a PostgreSQL database) for persistent storage. LangGraph Platform (commercial version) provides managed persistence services.
Time travel is the top-level capability of the LangGraph debugging experience. When you have checkpointer (introduced in Module 2), breakpoints (first four lessons in Module 3), and time travel, you have a complete"Run → Pause → Check → Backtrace → Modify → Continue"Complete debugging closed loop. This makes it possible to build complex, reliable production-grade LLM applications—no longer black boxes, but observable, controllable, and reproducible systems.