上一篇介绍 Persistence 时,我们已经看到,LangGraph 可以通过 Checkpointer 把执行过程中的 State 保存下来。
状态能够保存,并不等于任务已具备“暂停后继续执行”的能力。
Agent 开发中有一类场景,需要让 Agent 暂停下来,等待外部的决定。例如,发送邮件前先等确认,执行数据库变更前先等审批,或者生成内容后交由用户修改。这个等待可能是几秒,也可能是几个小时。
LangGraph 为这类场景提供了 Interrupt。它与 Persistence 配合,把一次原本必须连续完成的 Graph Run,拆成可以跨时间继续执行的多个阶段。
一、暂停执行的需求
假设一个 Agent 准备执行下面的操作:
向生产环境发布版本 v2.0
生成发布命令之后直接执行,技术上没有问题。但生产发布通常需要增加一道人工确认:
Agent 生成发布操作
↓
等待人工确认
↓
批准或拒绝
↓
继续执行
如果只是一个普通的命令行程序,可以使用 input() 等待用户输入。
然而,Agent 很难采用这种方式。
审批可能发生在十分钟以后,也可能发生在几个小时以后。最初的 HTTP 请求早已结束,运行任务的进程也可能已经重启。让线程一直阻塞在那里等待人工输入,同样不适合长时间运行的服务。
因此,Graph 暂停时需要留下几类信息:
当前 State
执行到了哪个位置
正在等待什么输入
后续应该从哪里继续
这正是 Persistence 和 Interrupt 配合的地方。
Persistence 保存 Graph 的状态,Interrupt 让 Graph 在指定位置暂停。外部系统收到人工输入以后,再通过 Resume 将结果送回原来的执行过程。
二、中断与恢复机制
LangGraph 通过 interrupt() 触发动态中断。
它可以写在普通 Node 中:
from langgraph.types import interrupt
approved = interrupt("是否批准这次操作?")
执行到这里时,Graph 会暂停,并把 "是否批准这次操作?" 交给调用 Graph 的程序。
调用方可以把这段信息显示在网页、后台管理系统或者审批界面中。
用户完成审批后,原程序中再次使用 invoke() 调用 Graph:
from langgraph.types import Command
graph.invoke(
Command(resume=True),
config=config,
)
其中的resume 的 True 会返回给原来的 interrupt():
approved = interrupt("是否批准这次操作?")
恢复之后:
approved == True
因此,一次完整的中断过程包含两个方向的数据传递:
Graph
│
│ interrupt(payload)
▼
外部应用
│
│ Command(resume=value)
▼
Graph
interrupt() 中的参数描述“当前正在等待什么”,Command(resume=...) 中的参数则是外部系统给出的结果。
三、相关语法
由于 Interrupt 与 Persistence 结合起来实现暂停后继续执行这一机制,下面把所有相关的内容都一并列出来。它们各自承担的职责比较明确。
| 写法 | 作用 | 说明 |
|---|---|---|
interrupt(value) | 暂停 Graph | value 会交给 Graph 调用方 |
Command(resume=value) | 恢复 Graph | value 成为 interrupt() 的返回值 |
InMemorySaver() | 保存 Checkpoint | 只保存在当前进程内存中 |
thread_id | 标识一条 Thread | 恢复时需要继续使用原来的 ID |
result["__interrupt__"] | 获取中断信息 | invoke() 模式下可读取中断 Payload |
其中,thread_id 与 Checkpointer 的关系尤其重要。
配置方式与上一篇 Persistence 中使用的一样:
config = {
"configurable": {
"thread_id": "deploy-001"
}
}
Graph 第一次执行时使用:
graph.invoke(
initial_state,
config=config,
)
恢复时仍然使用同一个配置:
graph.invoke(
Command(resume=True),
config=config,
)
Checkpointer 会根据 thread_id 找到这条 Thread 已保存的执行状态。
如果第一次使用:
deploy-001
恢复时改成:
deploy-002
这已经是另一条 Thread,无法接着刚才的执行过程运行。
因此,可以把 thread_id 看成一条持久化执行上下文的标识。
本文示例还会使用:
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
并在编译 Graph 时传入:
graph = builder.compile(
checkpointer=checkpointer
)
InMemorySaver 很适合本地学习和测试,但数据只存在于当前进程的内存中。进程结束后,其中的 Checkpoint 也随之丢失。生产环境需要换成真正的持久化 Checkpointer。
四、完整执行过程
下面用一个最小示例把这些对象连接起来。
这个 Graph 只有两个 Node:
START
↓
review
↓
finish
↓
END
review 负责人工审核,finish 根据审核结果更新状态。
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
class ApprovalState(TypedDict):
action: str
approved: bool | None
status: str
def review(state: ApprovalState):
print("进入 review 节点")
approved = interrupt(
{
"type": "approval",
"action": state["action"],
}
)
return {"approved": approved}
def finish(state: ApprovalState):
if state["approved"]:
return {"status": "approved"}
return {"status": "rejected"}
builder = StateGraph(ApprovalState)
builder.add_node("review", review)
builder.add_node("finish", finish)
builder.add_edge(START, "review")
builder.add_edge("review", "finish")
builder.add_edge("finish", END)
graph = builder.compile(
checkpointer=InMemorySaver()
)
config = {
"configurable": {
"thread_id": "deploy-001"
}
}
result = graph.invoke(
{
"action": "向生产环境发布版本 v2.0",
"approved": None,
"status": "pending",
},
config=config,
)
print(result["__interrupt__"][0].value)
result = graph.invoke(
Command(resume=True),
config=config,
)
print(result["status"])
运行结果如下:
进入 review 节点
{'type': 'approval', 'action': '向生产环境发布版本 v2.0'}
进入 review 节点
approved
第一次进入 review 时,执行到 interrupt() 后暂停,因此 finish 此时还没有运行。
使用 invoke() 时,中断数据可以从 result["__interrupt__"] 获取到。其中:
result["__interrupt__"][0].value
就是传给 interrupt() 的字典。
应用完成人工确认后,再传入:
Command(resume=True)
approved 得到 True,review 返回:
{"approved": True}
State 更新完成后,Graph 继续执行 finish,最终得到:
approved
当前 invoke() API 仍然支持这种方式。对于需要同时处理模型流式输出、状态变化和人工中断的交互式应用,LangGraph 也提供 stream_events(..., version="v3")。本文重点放在 Interrupt 的执行机制上,因此继续使用更直观的 invoke()。
运行结果里还有一个很重要的现象:
进入 review 节点
输出了两次。
理解这个细节,才能正确处理 Interrupt 前后的业务代码。
五、恢复时的重放
很多人第一次使用 Interrupt 时,容易把 Resume 当成普通程序里的“从暂停位置继续”。
LangGraph 的执行方式有所不同。
假设 Node 是:
def review(state):
print("A")
approved = interrupt("是否批准?")
print("B")
return {"approved": approved}
第一次运行到 Interrupt:
A
随后 Graph 暂停。
执行:
Command(resume=True)
之后,输出变成:
A
B
原因在于,Resume 时,中断所在的 整个 Node 会从头重新执行。
执行过程实际是:
第一次运行
Node 开始
↓
执行 A
↓
interrupt
↓
暂停
恢复运行
Node 重新开始
↓
再次执行 A
↓
interrupt 获得 Resume Value
↓
继续执行 B
LangGraph 保存的是 Graph 的执行状态,并不会冻结一个 Python 函数的调用栈,然后从某一行指令恢复。
注意这一点尤为重要。比如:
def review(state):
create_order()
approved = interrupt("是否批准订单?")
return {"approved": approved}
create_order() 位于 Interrupt 前面。
Node 恢复时,这段代码还会再执行一次。如果 create_order() 每次都创建新记录,就可能产生两个订单。
发送邮件、扣减库存、调用支付接口、插入数据库记录,也有类似问题。
工程中常见的处理方式有两种。
一种是让 Interrupt 前面的操作具备幂等性。例如:
db.upsert_order(
order_id=state["order_id"],
status="pending",
)
同一个 order_id 重复执行不会产生多条订单。
另一种方式更直观:把多次执行受影响的操作放到审核后的 Node。
review
↓
Interrupt
↓
人工批准
↓
execute
↓
调用外部系统
对于审批类流程,这种设计通常更清楚。review 只负责取得决定,execute 才负责执行真正的业务操作。
六、常见人工介入
Interrupt 并不只用于“批准或拒绝”。
从 Graph 的角度看,它所做的是:运行到某个位置后,需要等待外部输入。
以下我们看几种常见的场景。
批准或拒绝
例如数据库变更、生产发布和高风险 Tool 调用:
approved = interrupt(
{
"type": "approval",
"action": state["action"],
}
)
批准:
Command(resume=True)
拒绝:
Command(resume=False)
Node 根据布尔值决定后续处理。
审核并修改
例如 Agent 已经生成了一封邮件:
edited_email = interrupt(
{
"type": "email_review",
"email": state["email"],
}
)
审核人员可以修改邮件,再把完整内容传回:
Command(
resume={
"to": "user@example.com",
"subject": "项目进度调整通知",
"body": "修改后的正文",
}
)
此时 edited_email 得到人工修改后的字典,再由 Node 写入 State。
补充缺失信息
Graph 运行过程中也可能缺少某个业务参数:
environment = interrupt(
{
"type": "select_environment",
"options": [
"development",
"staging",
"production",
],
}
)
用户选择生产环境:
Command(resume="production")
恢复后:
environment == "production"
因此,Human-in-the-loop 的范围比审批更广。审核、修改、补充参数以及需要人工判断的业务决策,都可以通过相同的中断机制实现。
七、工程中的边界
Interrupt 的 API 很少,实际项目中的问题更多来自执行语义。
除了前面提到中断所在的整个 Node 会从头重新执行,还有几条规则应当提前了解。
异常处理
不要使用宽泛的 try/except 包住 interrupt():
try:
approved = interrupt("是否批准?")
except Exception:
...
interrupt() 会通过内部的特殊异常把暂停信号交给 LangGraph Runtime。宽泛捕获异常可能把这个信号一起截获。
业务代码需要异常处理时,可以缩小 try 的范围,或者只捕获明确的异常类型。
多个 Interrupt
一个 Node 可以包含多个 Interrupt:
name = interrupt("姓名")
age = interrupt("年龄")
city = interrupt("城市")
恢复时,LangGraph 会按照 Interrupt 的调用顺序匹配 Resume Value,因此这些调用的顺序应保持稳定。
如果 Node 根据不稳定条件跳过其中某个 Interrupt,就容易造成恢复值与调用位置不一致。
为了简单清晰,一次 Node 调用仅包含一个 Interrupt,执行关系会更容易维护。
输入校验
需要反复要求用户重新输入时,也不适合在 Node 中写:
while True:
value = interrupt(...)
更稳妥的方式是把校验结果写入 State,再通过 Conditional Edge 回到收集输入的 Node:
collect_input
↓
validate
↙ ↘
无效 有效
↓ ↓
重新输入 后续流程
这样每一次输入都对应一个明确的 Graph Step,状态历史也更清楚。
总结
本文介绍了 LangGraph 中 Interrupt、Resume 与 Persistence 的配合方式。Graph 可以在关键步骤通过 interrupt() 暂停,由 Checkpointer 保存状态,再通过相同的 thread_id 和 Command(resume=...) 恢复执行,因此适合人工审批、参数补充和高风险操作确认等场景。
实际使用时,应把人工介入放在高风险或不可逆的操作前,同时注意 Resume 会重新执行中断所在的 Node。Interrupt 前应尽量避免执行可能重复产生结果的外部操作,必要时采用幂等设计,或把真正的业务执行放到确认后的 Node 中。
社区讨论
参与讨论
有问题或想法?欢迎继续讨论。