现场是这样的。LangGraph 0.2 的图里用 interrupt 做人工确认,编译时只传了 checkpointer=MemorySaver(),第一次运行停在断点,用 Command(resume='yes') 继续后节点拿到的 state 里所有字段都是 None,thread_id 用的还是同一个。

中间走过弯路,把「checkpointer 存在进程内存,重启后断点已丢」当成主因,改完现象没变。

真正麻烦的地方在于,几个原因的现象几乎一样。恢复时传的是新的 config,thread_id 与原来那次不一致;节点函数声明了 state 参数但读的是闭包里的变量;interrupt 的返回值没有经由 Command 回传,恢复时被当成空输入。

第一件事是确认两次调用用的是同一个 thread_id

python -c "print({'thread_id':'review-20261111-01'})"

再往下把 checkpointer 换成落盘实现再复现

python -c "from langgraph.checkpoint.sqlite import SqliteSaver;print(SqliteSaver.from_conn_string('ckpt.sqlite'))"

恢复时显式带上同样的 configurable

python -c "from langgraph.types import Command;print(Command(resume='yes'))"

末了翻出 checkpoint 列表确认写进去了几条

python -c "import sqlite3;c=sqlite3.connect('ckpt.sqlite');print(c.execute('select count(*) from checkpoints').fetchone())"

别只看报错消失,验证要多做一步。恢复后节点读到的 state 与断点前一致,干预结果能写回图状态,同一 thread_id 连续跑三次结果稳定。

这里还有个坑。中断恢复靠 thread_id 串起整条状态链,这个值一旦重新生成就等于开了新会话,恢复前先把它打日志。监控比日志更早给出信号,事后回看经常能提前半小时发现异常。

作者 9823189