LangGraph checkpoint 与故障恢复:崩溃、重启、幂等与重放实测
“加了 checkpointer 就不会丢进度了”——这句话对一半。checkpoint 能保存已经完成的步骤,但它不会替你回答另一个问题:正在执行的那个节点,恢复时会不会再做一遍? 如果这个节点里有扣款、发货、发邮件,答案直接决定了你会不会重复扣款。
本页写给已经在用 LangGraph 做多步骤流程、准备上线的开发者。用一个三步订单流程(扣款 → 发货 → 通知)把崩溃、重启、人工审批、重放这些场景逐个跑一遍,每个实验都有可复现的脚本和实际输出,最后给出一张故障树。
适用版本与验证信息
| 项目 | 内容 |
|---|---|
| 验证日期 | 2026-10-25 |
| Python | 3.12.12 |
| 包版本 | langgraph==1.2.17、langgraph-checkpoint==4.3.0、langgraph-checkpoint-sqlite==3.1.2 |
| 模型 | 不涉及:所有实验是纯图,不调用任何模型,不联网 |
| 官方来源 | Persistence (opens in a new tab)、Checkpointers (opens in a new tab)、Interrupts (opens in a new tab)、Fault tolerance (opens in a new tab)、Time travel (opens in a new tab),对照官方文档仓库 langchain-ai/docs (opens in a new tab) 提交 b8d4ced(2026-10-16) |
| 实验代码 | 站点仓库 labs/langgraph-checkpoint-recovery/,依赖锁定在 labs/requirements/v1.txt |
实验里的“崩溃”是主动抛出的异常,用来验证框架的恢复语义。它不能模拟断电、kill -9 之类发生在写入 checkpoint 过程中的故障,也不代表任何数据库后端的持久性能。生产环境请用官方推荐的数据库实现并自行做破坏性测试。
先说结论
- checkpoint 保存的是“已完成的步骤”。 恢复时从失败的那个节点重新执行该节点,已完成的节点不会重跑(实验 1、2)。
- 并行分支里已成功的兄弟节点也不会重跑。 同一步里有节点失败时,成功节点的写入会被保留,恢复时只重跑失败的那个(实验 3)。
- 失败的节点本身会被重跑。 如果它在抛异常之前已经产生了外部副作用,重跑就会重复产生;解决办法是幂等键,而不是 checkpoint(实验 4)。
interrupt()之后恢复,节点从第一行开始重跑。interrupt()之前的代码会再执行一遍(实验 5)。- 重放不是“只读回看”。 从历史 checkpoint 重放,会重新执行它之后的所有节点,包括副作用(实验 6)。
RetryPolicy的默认策略会跳过一部分异常,包括TimeoutError。 不显式配置retry_on,超时不会被重试(实验 7)。- 配置错误大多“静默”。 换了
thread_id再传一次输入,会被当成一次全新的运行(实验 8)。
实验场景:订单流程
三个节点依次执行:charge(扣款)→ ship(发货)→ notify(通知)。每次真正产生副作用,都会往 LEDGER 列表里追加一条记录,实验就是看这个列表。FAIL 控制 ship 节点模拟几次“承运商接口超时”。
"""A three-step order workflow with visible side effects, shared by the recovery labs.
Every side effect is appended to LEDGER so the labs can show what ran, and how often.
"""
from typing import TypedDict
from langgraph.graph import END, START, StateGraph
LEDGER: list = [] # one entry per executed side effect
FAIL = {"ship": 0} # node name -> remaining number of simulated crashes
class State(TypedDict, total=False):
order_id: str
charged: bool
shipped: bool
notified: bool
def charge(state: State):
LEDGER.append(f"charge {state['order_id']}")
return {"charged": True}
def ship(state: State):
if FAIL["ship"] > 0:
FAIL["ship"] -= 1
raise RuntimeError("carrier API timeout")
LEDGER.append(f"ship {state['order_id']}")
return {"shipped": True}
def notify(state: State):
LEDGER.append(f"notify {state['order_id']}")
return {"notified": True}
def build(checkpointer=None):
builder = StateGraph(State)
for name, fn in [("charge", charge), ("ship", ship), ("notify", notify)]:
builder.add_node(name, fn)
builder.add_edge(START, "charge")
builder.add_edge("charge", "ship")
builder.add_edge("ship", "notify")
builder.add_edge("notify", END)
return builder.compile(checkpointer=checkpointer)实验 1:崩溃之后——不带 checkpointer 重跑,还是带着 checkpointer 续跑
同一个流程在 ship 节点崩溃一次。A 组没有 checkpointer,B 组有:
"""Crash in the middle of the flow: without a checkpointer vs. with one."""
from langgraph.checkpoint.memory import InMemorySaver
import order_flow
from order_flow import FAIL, LEDGER, build
def attempt(graph, payload, config=None):
try:
graph.invoke(payload, config)
return "ok"
except RuntimeError as exc:
return f"crashed: {exc}"
print("A) no checkpointer: the retry starts from scratch")
FAIL["ship"] = 1
graph = build()
print(" run 1:", attempt(graph, {"order_id": "A1"}))
print(" run 2:", attempt(graph, {"order_id": "A1"}))
print(" ledger:", LEDGER)
print("B) with a checkpointer: resume continues at the failed node")
LEDGER.clear()
FAIL["ship"] = 1
graph = build(InMemorySaver())
config = {"configurable": {"thread_id": "order-A1"}}
print(" run 1:", attempt(graph, {"order_id": "A1"}, config))
snapshot = graph.get_state(config)
print(" saved state:", snapshot.values, "| next:", snapshot.next)
print(" resume:", attempt(graph, None, config))
print(" ledger:", LEDGER)
print(" final state:", graph.get_state(config).values)A) no checkpointer: the retry starts from scratch
run 1: crashed: carrier API timeout
run 2: ok
ledger: ['charge A1', 'charge A1', 'ship A1', 'notify A1']
B) with a checkpointer: resume continues at the failed node
run 1: crashed: carrier API timeout
saved state: {'order_id': 'A1', 'charged': True} | next: ('ship',)
resume: ok
ledger: ['charge A1', 'ship A1', 'notify A1']
final state: {'order_id': 'A1', 'charged': True, 'shipped': True, 'notified': True}A 组重试时从头开始,charge A1 出现了两次,也就是客户被扣了两次钱。B 组崩溃后,保存的状态里 charged: True,next 指向 ship;用 invoke(None, config) 恢复,账本里只有一次扣款。注意恢复时传入的是 None:传 None 表示“从上次的位置继续”,而不是“用空输入开始”。
实验 2:进程整个挂掉——SQLite checkpointer 跨进程恢复
内存里的 checkpointer 随进程消失,官方文档也明确说明 InMemorySaver 在进程重启后会丢失所有 checkpoint。要在重启后恢复,需要把 checkpoint 放到持久化后端。官方列出的实现里,SqliteSaver 面向开发和本地流程,PostgresSaver 面向生产。下面用 SqliteSaver 验证跨进程恢复:脚本把自己作为两个独立的操作系统进程各启动一次,共用同一个数据库文件。
"""Recover after the whole process dies: a SQLite checkpointer survives restarts.
Run without arguments, the script is the orchestrator: it launches itself twice as
separate OS processes ("crash" then "resume") that share one SQLite file.
"""
import subprocess
import sys
import tempfile
from pathlib import Path
CONFIG = {"configurable": {"thread_id": "order-A1"}}
def worker(phase: str, db: str) -> None:
from langgraph.checkpoint.sqlite import SqliteSaver
from order_flow import FAIL, LEDGER, build
FAIL["ship"] = 1 if phase == "crash" else 0
with SqliteSaver.from_conn_string(db) as saver:
graph = build(saver)
try:
graph.invoke({"order_id": "A1"} if phase == "crash" else None, CONFIG)
print(f"[{phase}] finished, state={graph.get_state(CONFIG).values}")
except RuntimeError as exc:
print(f"[{phase}] crashed: {exc}; next={graph.get_state(CONFIG).next}")
print(f"[{phase}] side effects in this process: {LEDGER}")
def main() -> None:
with tempfile.TemporaryDirectory() as tmp:
db = str(Path(tmp) / "checkpoints.sqlite")
for phase in ("crash", "resume"):
proc = subprocess.run(
[sys.executable, __file__, phase, db], capture_output=True, text=True, check=True
)
print(proc.stdout, end="")
if __name__ == "__main__":
if len(sys.argv) == 3:
worker(sys.argv[1], sys.argv[2])
else:
main()[crash] crashed: carrier API timeout; next=('ship',)
[crash] side effects in this process: ['charge A1']
[resume] finished, state={'order_id': 'A1', 'charged': True, 'shipped': True, 'notified': True}
[resume] side effects in this process: ['ship A1', 'notify A1']第一个进程在 ship 处崩溃,只做过 charge A1;第二个进程是全新的 Python 解释器,没有任何内存状态,仅靠同一个 thread_id 就从 ship 继续,只执行 ship A1 和 notify A1。两个进程的副作用合起来恰好是每步一次。
实验 3:并行分支——成功的兄弟节点不会重跑
同一步里有两个并行节点,其中一个失败。官方把这个机制叫作 pending writes:失败时成功节点的写入已经落盘,恢复时不用重做。
"""Pending writes: when one parallel node fails, the siblings that finished are not re-run on resume."""
import operator
from typing import Annotated, TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
CALLS = {"fast": 0, "flaky": 0}
class State(TypedDict, total=False):
results: Annotated[list, operator.add]
def fast(state: State):
CALLS["fast"] += 1
return {"results": ["fast"]}
def flaky(state: State):
CALLS["flaky"] += 1
if CALLS["flaky"] == 1:
raise RuntimeError("upstream 503")
return {"results": ["flaky"]}
builder = StateGraph(State)
builder.add_node("fast", fast)
builder.add_node("flaky", flaky)
builder.add_node("join", lambda state: {})
builder.add_edge(START, "fast")
builder.add_edge(START, "flaky")
builder.add_edge("fast", "join")
builder.add_edge("flaky", "join")
builder.add_edge("join", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "t1"}}
try:
graph.invoke({"results": []}, config)
except RuntimeError as exc:
print("run 1 crashed:", exc)
result = graph.invoke(None, config)
print("resume result:", sorted(result["results"]))
print("calls:", CALLS)run 1 crashed: upstream 503
resume result: ['fast', 'flaky']
calls: {'fast': 1, 'flaky': 2}fast 只调用了 1 次,flaky 调用了 2 次(第一次失败、第二次成功),最终结果里两个节点的输出都在。这意味着把慢而贵的调用放在并行分支里,不会因为兄弟节点失败而被连带重做。
实验 4:失败的节点会重跑——用幂等键防重复扣款
checkpoint 的粒度是节点。如果节点在外部副作用已经生效、返回值还没交出去时失败(比如扣款成功但响应丢失),恢复时这个节点会再执行一次。
"""A checkpoint saves finished nodes, not half-finished ones: guard side effects with an idempotency key."""
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
PAYMENT_PROVIDER: dict = {} # idempotency_key -> amount; stands in for a real payment API
CALLS = {"count": 0}
class State(TypedDict, total=False):
order_id: str
receipt: str
def charge_naive(state: State):
CALLS["count"] += 1
PAYMENT_PROVIDER[f"naive-{CALLS['count']}"] = 99 # every call creates a new charge
if CALLS["count"] == 1:
raise TimeoutError("response lost after the charge went through")
return {"receipt": "ok"}
def charge_idempotent(state: State):
CALLS["count"] += 1
PAYMENT_PROVIDER[f"order-{state['order_id']}"] = 99 # same key: the provider deduplicates
if CALLS["count"] == 1:
raise TimeoutError("response lost after the charge went through")
return {"receipt": "ok"}
def run(node, label: str) -> None:
PAYMENT_PROVIDER.clear()
CALLS["count"] = 0
builder = StateGraph(State)
builder.add_node("charge", node)
builder.add_edge(START, "charge")
builder.add_edge("charge", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": label}}
try:
graph.invoke({"order_id": "A1"}, config)
except TimeoutError:
graph.invoke(None, config) # resume re-runs the node that failed
print(f"{label}: node ran {CALLS['count']} times, charges at provider = {len(PAYMENT_PROVIDER)}")
run(charge_naive, "naive")
run(charge_idempotent, "idempotent")naive: node ran 2 times, charges at provider = 2
idempotent: node ran 2 times, charges at provider = 1两种实现都被执行了 2 次,区别在支付侧:朴素版每次调用都新建一笔扣款,共 2 笔;幂等版每次调用带同一个幂等键(这里用订单号),支付方据此去重,只有 1 笔。实验里的 PAYMENT_PROVIDER 是用字典模拟的支付接口,真实场景请使用支付方文档规定的幂等机制。原则:凡是不能重复执行的副作用,都要能用同一个键安全地重试。
实验 5:interrupt() 恢复后,节点从第一行重跑
人工审批、等待外部输入通常用 interrupt()。官方文档说明,恢复时运行时会重新执行整个节点,而不是从 interrupt() 那一行继续。
"""interrupt() pauses a node; on resume the node restarts from its first line."""
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
LOG: list = []
class State(TypedDict, total=False):
decision: str
def review(state: State):
LOG.append("before interrupt") # runs again on resume
answer = interrupt({"question": "Approve refund of 99?"})
LOG.append("after interrupt")
return {"decision": answer}
builder = StateGraph(State)
builder.add_node("review", review)
builder.add_edge(START, "review")
builder.add_edge("review", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "t1"}}
paused = graph.invoke({}, config)
print("paused with:", paused["__interrupt__"][0].value)
print("log after pause:", LOG)
final = graph.invoke(Command(resume="approved"), config)
print("final state:", final)
print("log after resume:", LOG)paused with: {'question': 'Approve refund of 99?'}
log after pause: ['before interrupt']
final state: {'decision': 'approved'}
log after resume: ['before interrupt', 'before interrupt', 'after interrupt']“before interrupt”出现了两次:暂停前一次,恢复后一次。如果这行是发邮件,用户会收到两封。官方文档的建议与此一致:interrupt() 之前的副作用必须是幂等的(例如用 upsert 而不是 insert),或者把副作用放到 interrupt() 之后、或者拆到单独的节点里。同样的原因,官方也提醒不要在一个节点里用 while True 反复调用 interrupt(),每次恢复都会重放前面所有迭代。
实验 6:检查点历史与重放
get_state_history 能列出一个线程的全部 checkpoint,可以用来排查“到底在哪一步出了问题”。把某个历史 checkpoint 的 config 传给 invoke(None, ...),就能从那里重新执行。
"""Inspect checkpoint history, then replay from an earlier checkpoint (side effects run again)."""
from langgraph.checkpoint.memory import InMemorySaver
from order_flow import LEDGER, build
graph = build(InMemorySaver())
config = {"configurable": {"thread_id": "order-A1"}}
graph.invoke({"order_id": "A1"}, config)
history = list(graph.get_state_history(config)) # newest first
print("checkpoints (oldest first):")
for snap in reversed(history):
print(f" step={snap.metadata['step']:>2} next={snap.next} values={snap.values}")
before_ship = next(s for s in history if s.next == ("ship",))
LEDGER.clear()
graph.invoke(None, before_ship.config)
print("replay from the checkpoint before 'ship' executed:", LEDGER)checkpoints (oldest first):
step=-1 next=('__start__',) values={}
step= 0 next=('charge',) values={'order_id': 'A1'}
step= 1 next=('ship',) values={'order_id': 'A1', 'charged': True}
step= 2 next=('notify',) values={'order_id': 'A1', 'charged': True, 'shipped': True}
step= 3 next=() values={'order_id': 'A1', 'charged': True, 'shipped': True, 'notified': True}
replay from the checkpoint before 'ship' executed: ['ship A1', 'notify A1']历史按步骤递增:step=-1 是起点,step=1 的 next 是 ship,step=3 是终点。从 ship 之前的 checkpoint 重放,账本里新增了 ship A1 和 notify A1——重放会重新执行之后的所有节点,副作用也会再发生一次。所以把重放当作调试手段时,务必在隔离环境、或在副作用节点都幂等的前提下进行。
实验 7:同一次运行里的瞬时错误——RetryPolicy 的默认行为
不必每个瞬时错误都走“崩溃再恢复”:节点可以带 RetryPolicy,在同一次运行里自动重试。官方文档列出了默认不重试的异常类型;下面用实验确认。
"""RetryPolicy handles transient errors inside one run; the default retry_on skips some exception types, including TimeoutError."""
from typing import TypedDict
from langgraph.graph import END, START, StateGraph
from langgraph.types import RetryPolicy
class State(TypedDict, total=False):
value: str
def make_flaky(exc_type, failures: int):
calls = {"n": 0}
def flaky(state: State):
calls["n"] += 1
if calls["n"] <= failures:
raise exc_type(f"transient failure #{calls['n']}")
return {"value": "ok"}
return flaky, calls
def run(label: str, exc_type, policy: RetryPolicy) -> None:
node, calls = make_flaky(exc_type, failures=2)
builder = StateGraph(State)
builder.add_node("call_api", node, retry_policy=policy)
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
graph = builder.compile()
try:
result = graph.invoke({})
outcome = f"succeeded with {result}"
except Exception as exc:
outcome = f"failed with {type(exc).__name__}"
print(f"{label:<38} attempts={calls['n']}, {outcome}")
fast = {"initial_interval": 0.01, "jitter": False}
run("ConnectionError, default retry_on", ConnectionError, RetryPolicy(max_attempts=3, **fast))
run("TimeoutError, default retry_on", TimeoutError, RetryPolicy(max_attempts=3, **fast))
run("RuntimeError, default retry_on", RuntimeError, RetryPolicy(max_attempts=3, **fast))
run("TimeoutError, retry_on=TimeoutError", TimeoutError, RetryPolicy(max_attempts=3, retry_on=TimeoutError, **fast))
run("TimeoutError, max_attempts=2", TimeoutError, RetryPolicy(max_attempts=2, retry_on=TimeoutError, **fast))ConnectionError, default retry_on attempts=3, succeeded with {'value': 'ok'}
TimeoutError, default retry_on attempts=1, failed with TimeoutError
RuntimeError, default retry_on attempts=1, failed with RuntimeError
TimeoutError, retry_on=TimeoutError attempts=3, succeeded with {'value': 'ok'}
TimeoutError, max_attempts=2 attempts=2, failed with TimeoutError读取已安装版本的 default_retry_on 源码可以解释这个结果:它先判断 ConnectionError 直接返回“可重试”,随后对 ValueError、RuntimeError、OSError 等一批类型返回“不重试”。TimeoutError 属于 OSError 但不属于 ConnectionError,所以默认不会被重试,必须显式写 retry_on=TimeoutError。max_attempts 是包含第一次在内的总次数:设为 2 时,两次都失败后抛出原异常。
实验 8:配置错误
最后把三个最常见的配置错误放在一起:
"""Configuration mistakes that make recovery silently fail or fail loudly."""
from langgraph.checkpoint.memory import InMemorySaver
from order_flow import FAIL, LEDGER, build
def show(label: str, fn) -> None:
try:
print(f"{label}: {fn()}")
except Exception as exc:
print(f"{label}: {type(exc).__name__}: {str(exc).splitlines()[0]}")
print("1) graph compiled without a checkpointer")
plain = build()
show(" get_state", lambda: plain.get_state({"configurable": {"thread_id": "t"}}))
print("2) checkpointer present, but no thread_id in the config")
saved = build(InMemorySaver())
show(" invoke", lambda: saved.invoke({"order_id": "A1"}))
print("3) resume with a different thread_id starts a new run")
FAIL["ship"] = 1
saved = build(InMemorySaver())
show(" first run (thread order-A1)", lambda: saved.invoke({"order_id": "A1"}, {"configurable": {"thread_id": "order-A1"}}))
FAIL["ship"] = 0
show(" resume on thread order-A1-retry", lambda: saved.invoke(None, {"configurable": {"thread_id": "order-A1-retry"}}))
show(" re-send the input on thread order-A1-retry", lambda: saved.invoke({"order_id": "A1"}, {"configurable": {"thread_id": "order-A1-retry"}}))
print(" ledger:", LEDGER)1) graph compiled without a checkpointer
get_state: ValueError: No checkpointer set
2) checkpointer present, but no thread_id in the config
invoke: ValueError: Checkpointer requires one or more of the following 'configurable' keys: thread_id, checkpoint_ns, checkpoint_id
3) resume with a different thread_id starts a new run
first run (thread order-A1): RuntimeError: carrier API timeout
resume on thread order-A1-retry: EmptyInputError: Received no input for __start__
re-send the input on thread order-A1-retry: {'order_id': 'A1', 'charged': True, 'shipped': True, 'notified': True}
ledger: ['charge A1', 'charge A1', 'ship A1', 'notify A1']- 没有 checkpointer 的图调用
get_state,抛出ValueError: No checkpointer set; - 有 checkpointer 却没有传
thread_id,抛出ValueError,提示需要thread_id等配置键; - 换了一个
thread_id去“恢复”,传None会得到EmptyInputError(新线程里没有可恢复的内容)。更危险的是重新传入原始输入:它被当作新运行,账本里charge A1又出现了一次。恢复时务必沿用同一个thread_id,并且传None。
故障树:恢复结果不对时怎么查
| 现象 | 最可能的原因 | 怎么验证 | 处理 |
|---|---|---|---|
| 重试后重复扣款 / 重复发货 | 没有 checkpointer,或换了 thread_id 后重新传了原始输入 | 看 get_state(config).next 是否指向失败节点 | 沿用同一 thread_id,用 invoke(None, config) 恢复(实验 1、8) |
恢复时抛 No checkpointer set | 编译图时没有传 checkpointer | graph.checkpointer 是否为 None | compile(checkpointer=...)(实验 8) |
恢复时抛 Checkpointer requires ... 'configurable' keys | 配置里缺少 thread_id | 检查 config["configurable"] | 每次调用都带 thread_id(实验 8) |
| 进程重启后恢复不了 | 使用了 InMemorySaver | 看 checkpointer 类型 | 换成持久化后端(实验 2) |
| 重启后恢复,已完成的步骤又被执行 | 把 thread_id 写成随机值,重启后对不上 | 对比崩溃前后使用的 thread_id | thread_id 用业务主键(如订单号)派生(实验 2) |
| 恢复后同一个节点的副作用重复了 | 失败的节点会被重跑,副作用不幂等 | 在副作用接口侧查重复记录 | 加幂等键(实验 4) |
| 人工审批后邮件发了两次 | interrupt() 之前有副作用 | 看节点里 interrupt() 之前的代码 | 副作用移到 interrupt() 之后或拆成独立节点(实验 5) |
| 超时之后没有自动重试 | RetryPolicy 默认不重试 TimeoutError | 在 retry_on 里加上该类型 | 显式写 retry_on=TimeoutError(实验 7) |
| 重放历史 checkpoint 后出现新的副作用 | 重放会重新执行后续节点 | 看重放前后的账本 | 在隔离环境重放,或先让副作用幂等(实验 6) |
失败与回滚
- 回滚方式:checkpoint 不会“撤销”副作用,只能让流程不再前进。真正的回滚要靠业务侧的补偿动作(退款、撤销发货),并且补偿动作自己也要幂等;
- 清理:已经确认放弃的线程,在持久化后端里按
thread_id删除对应数据,避免数据库无限增长;官方 Checkpointers (opens in a new tab) 页面介绍了delete_thread/adelete_thread接口; - 上线前测试:为每个有副作用的节点写一条测试——在它返回之前注入一次失败,恢复之后断言外部系统里只有一份结果。本页实验 4 就是这个测试的最小形态;
- 不要依赖本页的数字:版本升级可能改变默认值(例如
RetryPolicy的默认策略),请用python3 labs/run.py --lab langgraph-checkpoint-recovery在你的版本下重新验证。
相关阅读
- 用
create_agent还是手写图来承载这类流程:create_agent 还是 LangGraph?,其中包含HumanInTheLoopMiddleware配置错误的实测; - 旧版 Agent 迁移到 v1 时
recursion_limit行为的变化:LangChain v1 迁移 Runbook; - 迁移后某个旧符号找不到:导入兼容矩阵。
来源
- Persistence (opens in a new tab):thread、checkpoint、
InMemorySaver进程重启后丢失; - Checkpointers (opens in a new tab):pending writes、各数据库实现;
- Interrupts (opens in a new tab):恢复时节点从头执行,
interrupt()之前的副作用应当幂等; - Fault tolerance (opens in a new tab):
RetryPolicy的参数与默认重试策略(节点超时与错误处理需要langgraph>=1.2); - Time travel (opens in a new tab):检查点历史与重放。
以上来源均于 2026-10-25 核对。本页所有输出来自站点仓库 labs/langgraph-checkpoint-recovery/ 在上表版本下的实际运行。