大家好,今天给大家分享LangGraph中一个非常实用的功能------Interrupts(中断)。在LangGraph构建复杂智能体或工作流时,我们经常需要在特定节点暂停执行,等待外部输入(比如人工审批、用户反馈)后再继续,而Interrupts正是为这个场景量身打造的核心能力。
本文将从核心概念、使用方法、常见场景、注意事项四个维度,结合可直接运行的Python示例,帮大家快速掌握Interrupts的用法,避免踩坑,轻松落地到实际项目中。
一、核心概念:什么是LangGraph Interrupts?
Interrupts(中断)是LangGraph提供的动态暂停机制,允许我们在图的任意节点、任意代码位置暂停执行,等待外部输入(如人工确认、用户输入)后再恢复执行。其核心价值在于实现"人机协同"(Human-in-the-Loop, HITL)工作流,让智能体不再是"黑盒执行",而是可以在关键步骤接受人类干预。
和静态断点(固定在节点前后暂停)不同,Interrupts具备以下特性:
- 动态性:可放在节点内任意代码位置,支持根据业务逻辑条件触发(比如只有金额大于1000时才触发审批中断)。
- 状态持久化:暂停时,LangGraph会通过Checkpointer(检查点)保存当前图的完整状态,即使服务重启也能恢复执行。
- 灵活恢复:恢复时可传入任意JSON可序列化的值,该值会作为中断函数的返回值,供节点继续执行。
关键前提:使用Interrupts必须配置Checkpointer(如InMemorySaver、SqliteSaver),用于持久化图状态;同时需要指定thread_id,作为恢复执行的"唯一标识"(相当于执行上下文的ID)。
Interrupts核心工作流程示意图(清晰展示"执行-暂停-恢复"全流程):
执行节点代码
保存状态(Checkpointer)
接收外部输入(Command(resume=...))
节点执行完成
图开始执行
触发interrupt
暂停执行,等待外部输入
恢复执行,继续节点代码
图继续运行/结束
说明:图示中,Checkpointer负责在暂停时保存状态,thread_id作为"状态指针",确保恢复时能找到对应的暂停节点。
二、快速上手:Interrupts基础使用流程
使用Interrupts只需3步:配置Checkpointer → 调用interrupt()函数暂停 → 用Command(resume=...)恢复执行。下面结合最简示例,带大家快速跑通流程。
基础使用三步流程图:
示例:InMemorySaver/SqliteSaver
传入提示信息,触发暂停
传入外部输入,返回interrupt()结果
1.配置Checkpointer
2.调用interrupt暂停
3.用Command resume=...恢复
节点继续执行,图完成运行
2.1 环境准备
首先安装所需依赖(确保LangGraph版本≥1.1,支持v2版本的流模式):
bash
pip install langgraph langchain_anthropic # anthropic可选,用于后续复杂示例
2.2 基础示例:简单暂停与恢复
需求:构建一个简单的图,在节点中暂停,等待用户输入"是否批准"后,继续执行并返回结果。
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from typing import TypedDict
# 1. 定义图的状态(存储执行过程中的数据)
class State(TypedDict):
approved: bool # 存储审批结果
# 2. 定义节点函数(包含中断逻辑)
def approval_node(state: State):
# 触发中断,暂停执行,向外部请求输入(JSON可序列化值均可)
approved = interrupt("Do you approve this action?")
# 恢复执行后,approved即为外部传入的resume值
return {"approved": approved}
# 3. 构建图并配置Checkpointer(必须配置,否则无法持久化状态)
builder = StateGraph(State)
builder.add_node("approval", approval_node) # 添加节点
builder.add_edge(START, "approval") # 起始节点→审批节点
builder.add_edge("approval", END) # 审批节点→结束节点
# 编译图,使用内存Checkpointer(生产环境建议用SqliteSaver、PostgresSaver等持久化存储)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 4. 执行图(触发中断)
# thread_id:唯一标识,恢复时必须使用相同的ID
config = {"configurable": {"thread_id": "thread-1001"}}
# 首次执行,触发中断并暂停
initial_result = graph.invoke({"approved": False}, config=config, version="v2")
# 查看中断信息(interrupts字段存储中断请求)
print("中断请求:", initial_result.interrupts)
# 输出:中断请求:(Interrupt(value='Do you approve this action?'),)
# 5. 恢复执行(传入外部输入)
resumed_result = graph.invoke(Command(resume=True), config=config, version="v2")
print("恢复执行结果:", resumed_result)
# 输出:恢复执行结果: {'approved': True}
核心说明:
- interrupt()函数:传入任意JSON可序列化值(字符串、字典、列表等),作为中断请求的"提示信息",供外部感知需要输入什么。
- Command(resume=...):恢复执行时必须传入该对象,resume的值会作为interrupt()的返回值,供节点继续执行。
- version="v2":LangGraph 1.1+推荐使用v2版本,中断信息会放在result.interrupts中,更易获取。
三、实战场景:Interrupts常见用法(附完整示例)
Interrupts的核心应用场景是"需要外部干预"的工作流,下面结合4个高频场景,给出可直接复用的代码示例。
Interrupts四大高频应用场景汇总:
适用:转账、发邮件等敏感操作
适用:并行工作流,多节点同时暂停
适用:工具调用安全校验
适用:用户输入合法性校验
Interrupts高频场景
场景1:关键操作审批
场景2:多并行节点中断
场景3:工具调用前中断
场景4:用户输入校验
人工审批后执行
一次性恢复所有中断
审核参数后执行工具
循环中断直到输入有效
场景1:关键操作审批(最常用)
需求:在执行敏感操作(如转账、发送邮件)前,暂停并请求人工审批,审批通过则执行,拒绝则取消。
关键操作审批流程图:
暂停,等待人工输入
批准
拒绝
开始
审批节点:触发中断请求审批
人工审批结果
执行操作节点:更新状态为approved
取消操作节点:更新状态为rejected
结束
python
from typing import Literal, TypedDict
from langgraph.checkpoint.sqlite import SqliteSaver # 生产环境用SQLite持久化
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
# 定义状态
class ApprovalState(TypedDict):
action_details: str # 操作详情
status: Literal["pending", "approved", "rejected"] # 审批状态
# 审批节点:触发中断,根据审批结果路由
def approval_node(state: ApprovalState) -> Command[Literal["proceed", "cancel"]]:
# 中断时传入操作详情,方便人工判断
decision = interrupt({
"question": "是否批准该操作?",
"action_details": state["action_details"]
})
# 根据审批结果,路由到不同节点
if decision:
return Command(goto="proceed") # 批准→执行操作
else:
return Command(goto="cancel") # 拒绝→取消操作
# 执行操作节点
def proceed_node(state: ApprovalState):
return {"status": "approved"}
# 取消操作节点
def cancel_node(state: ApprovalState):
return {"status": "rejected"}
# 构建图
builder = StateGraph(ApprovalState)
builder.add_node("approval", approval_node)
builder.add_node("proceed", proceed_node)
builder.add_node("cancel", cancel_node)
builder.add_edge(START, "approval")
builder.add_edge("proceed", END)
builder.add_edge("cancel", END)
# 编译图(使用SQLite持久化状态,避免服务重启丢失)
checkpointer = SqliteSaver("approval.db")
graph = builder.compile(checkpointer=checkpointer)
# 执行流程
config = {"configurable": {"thread_id": "transfer-20240501"}}
# 首次执行:触发审批中断
initial = graph.invoke(
{"action_details": "转账500元给用户A", "status": "pending"},
config=config
)
print("中断请求:", initial["__interrupt__"]) # v1版本获取中断信息
# 恢复执行(批准)
resumed_approved = graph.invoke(Command(resume=True), config=config)
print("批准结果:", resumed_approved["status"]) # 输出:approved
# 重新执行(拒绝,用新的thread_id)
config_new = {"configurable": {"thread_id": "transfer-20240502"}}
initial_new = graph.invoke(
{"action_details": "转账1000元给用户B", "status": "pending"},
config=config_new
)
resumed_rejected = graph.invoke(Command(resume=False), config=config_new)
print("拒绝结果:", resumed_rejected["status"]) # 输出:rejected
场景2:多并行节点中断(处理并行工作流)
需求:图中两个并行节点同时触发中断,需要一次性恢复所有中断,分别传入对应输入。
多并行节点中断流程图:
暂停,等待输入
暂停,等待输入
一次性传入所有中断的恢复值
开始
并行节点A:触发中断1
并行节点B:触发中断2
收集所有中断请求
节点A、B同时恢复执行
合并结果,图结束
python
from typing import Annotated, TypedDict
import operator
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
# 定义状态(使用operator.add实现并行节点结果合并)
class State(TypedDict):
vals: Annotated[list[str], operator.add]
# 并行节点A
def node_a(state):
answer = interrupt("请输入节点A的答案:")
return {"vals": [f"a:{answer}"]}
# 并行节点B
def node_b(state):
answer = interrupt("请输入节点B的答案:")
return {"vals": [f"b:{answer}"]}
# 构建并行图(START同时指向两个节点)
graph = (
StateGraph(State)
.add_node("a", node_a)
.add_node("b", node_b)
.add_edge(START, "a")
.add_edge(START, "b")
.add_edge("a", END)
.add_edge("b", END)
.compile(checkpointer=InMemorySaver())
)
config = {"configurable": {"thread_id": "parallel-1"}}
# 首次执行:两个节点同时触发中断
interrupted_result = graph.invoke({"vals": []}, config)
print("并行中断信息:", interrupted_result["__interrupt__"])
# 输出包含两个中断,各有唯一id
# 一次性恢复所有中断(用中断id映射对应输入)
resume_map = {
i.id: f"答案:{i.value.split(':')[1]}"
for i in interrupted_result["__interrupt__"]
}
final_result = graph.invoke(Command(resume=resume_map), config)
print("并行执行结果:", final_result["vals"])
# 输出:['a:答案:节点A的答案', 'b:答案:节点B的答案']
场景3:工具调用前中断(安全校验)
需求:在调用工具(如发送邮件、调用API)前,暂停并让人工审核工具参数,可修改参数后再执行。
工具调用前中断流程图:
暂停,等待人工审核
批准(可修改参数)
拒绝
开始
智能体节点:准备调用工具
工具函数:触发中断,展示参数
人工审核结果
执行工具调用,返回结果
取消工具调用,返回提示
图结束
python
from langchain.tools import tool
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
from typing import TypedDict
# 定义状态
class AgentState(TypedDict):
messages: list[dict]
# 定义带中断的工具(发送邮件前需审批)
@tool
def send_email(to: str, subject: str, body: str):
"""发送邮件工具"""
# 中断:展示邮件详情,请求审批(可修改参数)
response = interrupt({
"action": "send_email",
"to": to,
"subject": subject,
"body": body,
"message": "是否批准发送邮件?可修改收件人、主题和正文"
})
# 审批通过:执行发送(可使用修改后的参数)
if response.get("action") == "approve":
final_to = response.get("to", to)
final_subject = response.get("subject", subject)
final_body = response.get("body", body)
# 实际发送逻辑(此处省略)
return f"邮件已发送至:{final_to},主题:{final_subject}"
# 审批拒绝:取消发送
return "邮件发送已取消"
# 智能体节点(调用工具)
def agent_node(state: AgentState):
# 此处可结合LLM判断是否调用工具,简化示例直接调用
tool_result = send_email(to="alice@example.com", subject="会议通知", body="明天10点开会")
return {"messages": state["messages"] + [{"role": "assistant", "content": tool_result}]}
# 构建图
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_edge(START, "agent")
builder.add_edge("agent", END)
# 编译图
checkpointer = SqliteSaver("tool_interrupt.db")
graph = builder.compile(checkpointer=checkpointer)
# 执行流程
config = {"configurable": {"thread_id": "email-1001"}}
initial = graph.invoke(
{"messages": [{"role": "user", "content": "发送会议通知邮件"}]},
config=config
)
print("工具中断请求:", initial["__interrupt__"])
# 恢复执行(批准并修改主题)
resumed = graph.invoke(
Command(resume={"action": "approve", "subject": "紧急会议通知"}),
config=config
)
print("工具执行结果:", resumed["messages"][-1]["content"])
场景4:用户输入校验(循环中断)
需求:收集用户年龄,若输入无效(非数字、负数),则重复中断请求,直到输入有效为止。
用户输入校验(循环中断)流程图:
暂停,接收用户输入
无效(非数字/负数)
有效(正整数)
开始
收集年龄节点:触发中断,请求输入
输入是否有效?
更新提示信息,重新触发中断
返回年龄,节点执行完成
图结束
python
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
from typing import TypedDict
# 定义状态
class FormState(TypedDict):
age: int | None # 存储用户年龄
# 收集年龄节点(循环中断校验)
def get_age_node(state: FormState):
prompt = "请输入你的年龄(正整数):"
while True:
# 触发中断,请求用户输入
answer = interrupt(prompt)
# 校验输入有效性
if isinstance(answer, int) and answer > 0:
break # 输入有效,退出循环
# 输入无效,重新提示
prompt = f"'{answer}' 不是有效的年龄,请输入正整数:"
return {"age": answer}
# 构建图
builder = StateGraph(FormState)
builder.add_node("collect_age", get_age_node)
builder.add_edge(START, "collect_age")
builder.add_edge("collect_age", END)
# 编译图
checkpointer = SqliteSaver("form.db")
graph = builder.compile(checkpointer=checkpointer)
# 执行流程
config = {"configurable": {"thread_id": "form-1001"}}
# 第一次中断:输入无效
first = graph.invoke({"age": None}, config=config)
print("第一次中断:", first["__interrupt__"]) # 提示:请输入你的年龄(正整数)
# 恢复执行:输入无效值(字符串)
retry = graph.invoke(Command(resume="三十"), config=config)
print("第二次中断:", retry["__interrupt__"]) # 提示:'三十' 不是有效的年龄...
# 恢复执行:输入有效值(整数)
final = graph.invoke(Command(resume=30), config=config)
print("最终结果:", final["age"]) # 输出:30
四、避坑指南:Interrupts使用注意事项
使用Interrupts时,有4个关键规则必须遵守,否则会出现状态错乱、中断失效等问题。
Interrupts四大避坑要点汇总:
原因:捕获中断异常,导致中断失效
原因:索引匹配错乱,状态异常
原因:Checkpointer无法序列化,报错
原因:重复执行副作用,数据异常
Interrupts避坑要点
避坑1:不包裹try/except
避坑2:不条件跳过interrupt
避坑3:传入值需JSON可序列化
避坑4:中断前副作用需幂等
正确:分离中断与异常处理
正确:保持中断顺序/数量固定
正确:使用字符串/字典等简单类型
正确:用幂等操作或后置副作用
- 不要用try/except包裹interrupt()调用
interrupt()通过抛出特殊异常实现暂停,若用try/except(尤其是裸except)包裹,会捕获该异常,导致中断无法传递给LangGraph runtime,中断失效。
python
# ❌ 错误:用裸except包裹interrupt()
def bad_node(state):
try:
interrupt("请输入信息")
except Exception as e:
print(e)
return state
# ✅ 正确:分离interrupt和异常处理,或捕获特定异常
def good_node(state):
interrupt("请输入信息")
try:
# 其他可能出错的逻辑(如API调用)
fetch_data()
except NetworkError as e: # 捕获特定异常
print(e)
return state
- 不要在节点内条件性跳过interrupt()
LangGraph恢复执行时,会重新运行整个节点,中断的匹配是"索引-based"的。若条件性跳过interrupt(),会导致恢复时输入值与中断不匹配,出现逻辑错乱。
python
# ❌ 错误:条件性跳过interrupt()
def bad_node(state):
name = interrupt("请输入姓名")
if state.get("need_age"): # 条件可能变化,导致中断数量不一致
age = interrupt("请输入年龄")
return {"name": name}
# ✅ 正确:保持interrupt()调用顺序和数量固定
def good_node(state):
name = interrupt("请输入姓名")
age = interrupt("请输入年龄(无需请填0)") # 即使不需要,也不跳过
return {"name": name, "age": age if age != 0 else None}
- interrupt()传入的值必须JSON可序列化
Checkpointer需要序列化中断状态,因此interrupt()的参数不能是函数、类实例等复杂对象,建议使用字符串、字典、列表等简单类型。
python
# ❌ 错误:传入不可序列化的对象
def bad_node(state):
def validator(x):
return x > 0
# 函数不可序列化,会报错
interrupt({"question": "请输入数字", "validator": validator})
return state
# ✅ 正确:传入简单可序列化值
def good_node(state):
interrupt({"question": "请输入数字", "rule": "必须是正整数"})
return state
- 中断前的副作用需保证幂等性
恢复执行时,节点会重新运行,因此中断前的操作(如数据库写入、API调用)必须是幂等的(多次执行结果一致),避免重复创建数据、重复调用API。
python
# ❌ 错误:中断前执行非幂等操作(重复创建记录)
def bad_node(state):
# 每次恢复都会创建新的审计记录,导致重复
db.create_audit_log({"action": "pending"})
interrupt("请审批")
return state
# ✅ 正确:中断前执行幂等操作(或把副作用放在中断后)
def good_node(state):
# 幂等操作:存在则更新,不存在则创建
db.upsert_audit_log({"action": "pending", "id": state["id"]})
approved = interrupt("请审批")
# 副作用放在中断后,只执行一次
if approved:
db.create_audit_log({"action": "approved", "id": state["id"]})
return state
五、调试技巧:静态中断(断点)
除了动态中断(interrupt()函数),LangGraph还支持静态中断(断点),用于调试图的执行流程,可在编译时或运行时指定节点前后暂停。
静态中断与动态中断对比图:
触发方式:代码中调用interrupt()
用途:生产环境,人机协同
触发方式:编译/运行时指定节点
用途:调试阶段,调试图流程
中断类型对比
动态中断interrupt
静态中断_断点
特点:动态、灵活,支持条件触发
示例:审批、输入校验
特点:固定、静态,无需修改代码
示例:节点执行前后暂停
python
# 编译时设置静态中断(断点)
graph = builder.compile(
interrupt_before=["node_a"], # 执行node_a前暂停
interrupt_after=["node_b"], # 执行node_b后暂停
checkpointer=checkpointer
)
# 运行时设置静态中断
config = {"configurable": {"thread_id": "debug-1"}}
# 执行到第一个断点暂停
graph.invoke(inputs, interrupt_before=["node_a"], config=config)
# 恢复执行,直到下一个断点
graph.invoke(None, config=config)
注意:静态中断仅用于调试,生产环境建议使用动态中断(interrupt()函数)实现人机协同。
六、总结
LangGraph Interrupts是实现"人机协同"工作流的核心能力,其核心价值在于:动态暂停图执行、持久化执行状态、灵活接收外部输入。通过本文的讲解,我们掌握了:
- Interrupts的核心概念和基础使用流程;
- 4个高频实战场景(审批、并行中断、工具校验、输入校验)的完整示例;
- 4个关键避坑规则,避免状态错乱和中断失效;
- 静态中断的调试技巧。
LangGraph Interrupts 整体知识框架图:
动态性、状态持久化、灵活恢复
3步:配置→暂停→恢复
4大场景
4大规则
静态中断
LangGraph Interrupts
核心概念
基础流程
实战场景
避坑指南
调试技巧
依赖Checkpointer和thread_id
核心API:interrupt()、Command(resume=...)
审批、并行、工具校验、输入校验
不包裹try/except等
仅用于调试,生产用动态中断
实际项目中,Interrupts可广泛应用于智能体审批、工具安全校验、用户输入交互等场景,让LangGraph构建的工作流更灵活、更安全、更具可交互性。
如果觉得本文有用,欢迎点赞、收藏、关注,后续会分享更多LangGraph实战技巧!