LangGraph 生产级笔记:Checkpointer + interrupt(),把人审做成可恢复状态机

千笔-AIWritePaper · https://www.aiwritepaper.com

把「人审」塞进 Agent,最常见翻车是进程一重启就丢现场,或审批接口返回后图从错误节点重跑。LangGraph 的答案不是另写一套工作流引擎,而是两块拼在一起:Checkpointer 把线程状态落成可恢复快照interrupt() 在节点任意位置动态暂停 ,恢复时用同一个 thread_idCommand(resume=...) 的值交回 interrupt() 调用点。本文只复述官方 Persistence、Memory 与 Interrupts 文档里能对照的行为,并落到可核对的最小代码与检查清单。

图:编译挂上持久化 checkpointer → 带 thread_id 运行 → interrupt 暂停并表面 payload → Command(resume) 恢复;底部为官方四条硬规则摘要。

目标说明

读完你应能独立完成五件事:

  1. 分清 Checkpointer (线程内短期记忆 / 图状态快照)与 Store(跨线程长期键值),人审恢复主要靠前者。
  2. 在生产编译图时挂上 PostgresSaver / SqliteSaver 等持久化实现,而不是把 InMemorySaver 直接上线。
  3. 在节点里调用 interrupt(payload),用 stream_events(..., version="v3") 读取 stream.interrupted / stream.interrupts,或用 invoke__interrupt__
  4. 相同 thread_idCommand(resume=...) 续跑,并理解「恢复时节点会从开头重跑」。
  5. 对照官方规则:勿裸 try/except 吞中断、勿打乱 interrupt 顺序、payload 可序列化、中断前副作用幂等。

规格钉死(来自官方文档):

  • Checkpointer 持久化单线程图状态;用途含对话续写、HITL、时间旅行、故障恢复。
  • Store 持久化应用自定义数据,跨线程;适合用户偏好与共享知识。
  • thread_id 是游标:复用则续状态,换新值等于新线程空状态。
  • interrupt() 暂停执行、写入 checkpoint、无限等待外部输入;恢复值成为该次 interrupt() 的返回值。
  • 静态 interrupt_before / interrupt_after 适合调试,不推荐作为生产 HITL 主路径。
  • Agent Server 场景下,服务端可托管持久化,应用侧仍需理解 thread 与 resume 语义。

适用场景与边界

适合做成可恢复人审

  • 发邮件、改库、转账、调用付费 API 前必须人点同意,或改参数后再执行。
  • 审改 LLM 草稿、工具参数后再进下游节点。
  • 并行分支各自 interrupt,一次用 interrupt id 映射批量 resume。
  • 进程会重启、要隔夜审批:状态必须在数据库或文件型 checkpointer 里。
  • 需要「时间旅行」:按 checkpoint 查看历史状态,再决定是否从某点续跑。

不该指望 Checkpointer + interrupt 单独搞定

  • 跨用户、跨会话的偏好与事实库:用 Store,不是把一切塞进线程 checkpoint。
  • 无 checkpointer 的「纯函数图」:interrupt 无法持久等待。
  • 把人审当成同步 HTTP 里死等:应把暂停态暴露给 UI,审批回调再 resume。
  • 用裸 MemorySaver / InMemorySaver 冒充生产:重启即丢。
  • 开放式、无法 JSON 化的「把整个 UI 组件塞进 interrupt」:序列化会失败。

风险提示

Postgres 下 thread_id 过长会撞列宽,官方建议控制在 255 字符内,或用 UUID / 哈希。长对话 checkpoint 会膨胀,延迟与存储成本上升,需保留策略或定期清理。子图有独立 checkpoint 命名空间时,父图未必立刻看见子图写入;跨图共享数据优先考虑 Store。不可信来源的图定义同样可能诱导危险工具调用,人审节点不能省略权限模型。

机制:Checkpointer 与 interrupt 如何咬合

Checkpointer:线程级快照

编译时传入 checkpointer:

python 复制代码
from langgraph.checkpoint.memory import InMemorySaver  # 仅开发
# 生产见官方:PostgresSaver / SqliteSaver / Redis / Mongo 等

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "approval-123"}}

每次步进后状态可落盘。人审暂停时,运行时保存当前图状态 ,之后即使进程退出,只要 checkpointer 还在且 thread_id 不变,就能续。官方 Persistence 文档把 Checkpointer 与 Store 对照成「短线程记忆 vs 长跨线程记忆」,HITL 首先钉前者。

生产示例骨架(Postgres,需安装对应包并 setup()):

python 复制代码
from langgraph.checkpoint.postgres import PostgresSaver

with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    # checkpointer.setup()  # 首次
    graph = builder.compile(checkpointer=checkpointer)

interrupt:动态断点

python 复制代码
from langgraph.types import interrupt, Command

def approval_node(state):
    decision = interrupt({
        "question": "Approve this action?",
        "details": state["action_details"],
    })
    return {"approved": bool(decision)}

发生了什么:执行挂起 → checkpoint 写入 → payload 出现在 stream.interrupts(或 invoke__interrupt__)→ 等待 → Command(resume=...) 把值送回 interrupt()

推荐驱动方式(官方强调 event streaming):

python 复制代码
stream = graph.stream_events(inputs, config=config, version="v3")
_ = stream.output
if stream.interrupted:
    # 把 stream.interrupts 展示给 UI
    resumed = graph.stream_events(
        Command(resume=True), config=config, version="v3"
    )
    final = resumed.output

交互式 HITL 循环可以一边消费 stream.messages 做 token 展示,一边在 paused 时收集人输入再 resume,直到 stream.interrupted 为假。

为何「节点会整段重跑」

恢复不是从 interrupt 下一行字节码续跑,而是重新进入该节点 。因此 interrupt 之前的代码会再执行一遍。这直接导出官方 Rules of interrupts:副作用必须幂等或后移;多个 interrupt 的顺序必须稳定;不能用裸 except 吞掉暂停用的特殊异常。

审批、审改、工具内暂停

常见模式:

  1. 批准/拒绝interrupt 返回布尔或枚举,节点用 Command(goto="proceed"|"cancel") 路由。
  2. 审改状态interrupt 返回编辑后的文本,写回 state。
  3. 工具内 interrupt :在 @tool 里暂停,审批时可覆盖 to/subject/body 再真正发送。
  4. 校验人输入 :每节点只调用一次 interrupt,非法则更新 pending_question,条件边绕回;禁止单节点 while True 多段 interrupt。

与静态断点的区别

类型 触发 用途
interrupt() 代码内、可条件 生产 HITL、按业务暂停
interrupt_before/after 编译或调用时按节点名 调试单步

步骤:最小可恢复审批图

  1. 选持久化 checkpointer ,首次调用 setup()(Postgres 等需迁移)。
  2. State 只放可序列化字段;审批细节用 dict,不要塞函数对象或处理器实例。
  3. 审批节点 里先 interrupt,再根据返回值路由或写状态。
  4. 副作用 :发邮件、插审计日志尽量放在 interrupt 之后,或做成幂等 upsert。
  5. 驱动循环 :消费 stream_events,未 interrupted 则结束;否则收集人输入再 Command(resume=...)
  6. 并行多 interrupt :resume 时传 {interrupt_id: value} 映射,避免对错问题。
  7. 观测 :用 graph.get_state(config) / get_state_history 核对暂停时的 valuesnext,便于排障。

示意(结构对齐官方 Full example,省略业务细节):

python 复制代码
def approval_node(state):
    decision = interrupt({
        "question": "Approve this action?",
        "details": state["action_details"],
    })
    return Command(goto="proceed" if decision else "cancel")

可验证检查清单

检查项 通过标准
持久化 杀掉进程后,同 thread_id 仍能 resume 到待审状态
表面 stream.interrupted is True 且 payload 与传入一致
续跑 resume 后 interrupt() 返回值等于 Command(resume=...)
幂等 故意多次走 interrupt 前路径,外部副作用不重复脏写
规则 无裸 except 包 interrupt;同节点 interrupt 顺序稳定
边界 thread_id 长度合规;开发/生产 checkpointer 类型可区分
历史 get_state_history 能看到暂停前后的快照链

本地可用 Sqlite 文件型 checkpointer 做冒烟:跑到 interrupt → 退出解释器 → 新进程加载同 DB 与 thread → resume。把通过/失败记成表格,比口头「我觉得可以恢复」更有用。

踩坑

踩坑 后果 改法
生产用 InMemory 重启丢审 换 Postgres/Sqlite/Redis 等
try/except Exception 包 interrupt 暂停异常被吞,图「假继续」 只捕业务异常类型
interrupt 前 create 非幂等记录 每次 resume 复制脏数据 upsert 或副作用后移
条件跳过某个 interrupt resume 索引错位 顺序固定或拆节点
换新 thread_id 当「重试」 另起空线程 重试必须复用原 id
把 HITL 做成静态 interrupt_before 难按业务条件暂停 改用 interrupt()
checkpoint 永不清理 存储与延迟恶化 保留策略 / 定期删除旧线程
resume 时传 Command(update=...) 当输入 语义不符官方约定 人审续跑用 Command(resume=...)

生产编排补充:把暂停态交给外部系统

只在笔记本里 invoke 两次,还不算生产。常见编排是:

  1. API 收到用户请求,创建或复用 thread_id,启动 stream_events
  2. interrupted,把 interrupts payload 写入任务表,状态标为 waiting_human,HTTP 立刻返回「待审」。
  3. 审核员在后台点同意/拒绝/编辑,回调服务用同一 thread_idCommand(resume=...)
  4. 续跑完成后再通知用户;失败则保留 checkpoint,禁止「换个 id 重试」装作同一单。

这样人审等待可以是小时或天级,而不用占着工作进程。Checkpointer 此时就是工单系统的状态后端之一。注意:UI 展示的文案应来自你传入 interrupt 的 JSON,而不是事后拼出来的模糊提示,否则审核员看不到模型当时真正要做什么。

并发方面,同一 thread_id 上应串行化 resume,避免两个审核员同时提交导致状态竞争。并行节点上的多个 interrupt,则按官方所述用 id→值映射一次性恢复,减少半恢复态。

和「自己写个审批表」比,图状态机赢在哪

自己做审批表也能存「待同意」,但很难自动对齐「图跑到哪一节点、通道版本、待执行任务」这些运行时细节。LangGraph 把这些放进 checkpoint 元数据,恢复时运行时知道 next 是谁。对 Agent 这种分支多、工具多的系统,用图状态机比把审批逻辑散落在各处 if 更不容易漏边。代价是你必须遵守 interrupt 规则,并把序列化边界划清。

若业务只需一次性人工确认且无复杂分支,轻量工单可能更简单;一旦出现「改参数再跑工具」「多专家会签」「失败后从中段续」,Checkpointer + interrupt 的收益会明显高于散装 if。

小抄:上线前十问

  1. 生产 checkpointer 类型是什么,连接串是否在密钥管理里?
  2. setup() / 迁移是否纳入发布流程?
  3. 每个业务单的 thread_id 如何生成,是否可能超过长度限制?
  4. 暂停态如何暴露给前端,超时如何提醒审核员?
  5. resume 接口是否鉴权到「只能审自己的单」?
  6. interrupt payload 是否避免泄露敏感全文到日志?
  7. 节点内 interrupt 前是否还有非幂等写?
  8. 并行 interrupt 是否测试过 id 映射 resume?
  9. checkpoint 保留多久,谁有权 delete_thread
  10. 故障演练:杀进程后能否从待审态恢复?

十问都有书面答案,再宣布「我们支持人审」不迟。

总结

生产级人审不是「多弹一个确认框」,而是可恢复状态机 :Checkpointer 记住线程快照,interrupt() 在业务点暂停,同 thread_id + Command(resume=...) 把人的决定注回图。先把持久化与四条 interrupt 规则做对,再谈 UI 与并行审批。文档入口:LangGraph Persistence、Human-in-the-loop(Interrupts)、Memory。把检查清单跑通一次,比堆更多节点名更接近「生产级」。

把人审做成可恢复状态机之后,产品经理与安全同学才有共同语言:暂停态可查询,恢复动作可审计,线程标识可追踪。这比在演示视频里「人工点一下继续」更接近可运营系统。

选工具时也可以把本文当验收单:候选框架若声称支持 HITL,就问三句。第一,暂停态是否写入可独立于进程的存储。第二,恢复是否保证业务 id 不变。第三,节点重入时如何避免重复副作用。三句都答得清,再谈可视化与模板。答不清,多半只是演示级确认框。

相关推荐
凤城老人43 分钟前
基于 Flask 的企业级 CMS 架构设计与实现
后端·python·flask
所念皆星海9111 小时前
Python学习---DAY10函数
开发语言·python·学习
刘天远1 小时前
企业 Agent 需求怎么写:数据结构、流程图与 Python 校验
数据结构·人工智能·python·流程图
Java后端的Ai之路1 小时前
23、Python - 策略模式
linux·python·策略模式
旖旎夜光1 小时前
【LangChain实战】LangChain 学习笔记(一):从定义大模型到工具调用
人工智能·笔记·python·学习·langchain
临沂GEO1 小时前
用好地域流量,提升内容自然搜索曝光
网络·python
问天_观心10 小时前
大模型微调学习(二)
人工智能·python·深度学习·学习·语言模型·transformer
洋洋不叫杨杨10 小时前
揭秘当下知名的SEO优化渠道,你知道几个?
大数据·python
阿童木写作10 小时前
跨境图片翻译工具推荐:批量处理视频字幕与智能抠图
python·音视频