- 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
- 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
- 培歌行学LangGraph(3):编译、执行与可视化
- 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
- 培歌行学LangGraph(5):状态管理与
graph.invoke入参深度解析 - 培歌行学LangGraph(6):Multi Schema多状态管理详解
- 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
- 培歌行学LangGraph(8):控制流详解
- 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
- 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
- 培歌行学LangGraph(11):用循环结构实现 ReAct Agent
- 培歌行学LangGraph(12):递归限制——别让你的图无限跑下去
- 培歌行学LangGraph(13):节点执行与容错机制
- 培歌行学LangGraph(14):启用可恢复执行(Checkpoint)
- 培歌行学LangGraph(15):持久化模式——何时保存检查点?
- 培歌行学LangGraph(16):持久化模式——查看历史检查点——你的图走过的每一步都有记录
- 培歌行学LangGraph(17):检查点的四大使用场景
- 培歌行学LangGraph(18):图记忆管理-让你的 AI 记住该记的事
一、什么是动态中断?
中断就是让你的图在执行过程中暂停,等外部输入(比如人类的审批、修改)之后再继续。
打个比方:
你写了一个自动写诗的图,写到一半停下来问用户:"这首诗行不行?要不要改?"用户点头了再继续。
这就是人在环(HITL) ——人和机器一起干活,机器干到关键步骤停下来等人拍板。
中断怎么用?
就两步:
- 在节点里调用
interrupt():告诉图"停在这里,等消息" - 恢复时传
Command(resume=...):把用户输入传给图
二、中断的七大使用模式(完整代码+人工输入)
模式1:基础 HITL(人在环)
最简单的用法:图停下来问用户一句话,用户回答了再继续。
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
class OverAllState(TypedDict):
username: str
def node_a(state: OverAllState) -> OverAllState:
username = interrupt("请输入您的姓名")
return {"username": username}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "basic_hitl"}}
# 第一步:触发中断
interrupt_res = graph.invoke({}, config=config)
print("中断信息:", interrupt_res)
# 第二步:人工输入(真正的 input 等待)
user_input = input("请输入您的姓名:") # ← 这里真的停下来等用户打字
# 第三步:恢复运行
resumed_res = graph.invoke(Command(resume=user_input), config=config)
print("最终结果:", resumed_res)
运行过程:
中断信息: {'__interrupt__': [Interrupt(value='请输入您的姓名', id='...')]}
请输入您的姓名:小黄 ← 用户手动输入
最终结果: {'username': '小黄'}
模式2:多个并行中断
两个节点同时跑,各自都要问用户问题。
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
class OverAllState(TypedDict):
username: str
age: int
def node_a(state: OverAllState) -> OverAllState:
username = interrupt("请输入您的姓名")
return {"username": username}
def node_b(state: OverAllState) -> OverAllState:
age = interrupt("请输入您的年龄")
return {"age": age}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "parallel_interrupt"}}
# 第一步:触发中断(两个并行中断)
interrupt_res = graph.invoke({}, config=config)
print("中断信息:", interrupt_res)
# 第二步:遍历所有中断,逐一让用户输入
resume_map = {}
for i in interrupt_res['__interrupt__']:
user_input = input(f"{i.value}: ") # ← 每个中断都等用户输入
if "年龄" in i.value:
resume_map[i.id] = int(user_input) # 年龄转整数
else:
resume_map[i.id] = user_input
# 第三步:恢复运行
resumed_res = graph.invoke(Command(resume=resume_map), config=config)
print("最终结果:", resumed_res)
运行过程:
中断信息: {'__interrupt__': [Interrupt(value='请输入您的年龄', id='...'), Interrupt(value='请输入您的姓名', id='...')]}
请输入您的年龄: 19 ← 用户手动输入
请输入您的姓名: 小黄 ← 用户手动输入
最终结果: {'username': '小黄', 'age': 19}
模式3:审批模式
图停下来问用户"批不批准?",根据答案走不同分支。
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={"thinking": {"type": "disabled"}}
)
class OverAllState(TypedDict):
topic: str
poem: str
is_approved: bool
def approve_node(state: OverAllState) -> Command[Literal["llm_node", "default_node"]]:
is_approved = interrupt("是否同意调用模型生成诗句?")
goto = "llm_node" if is_approved else "default_node"
return Command(goto=goto, update={"is_approved": is_approved})
def llm_node(state: OverAllState) -> OverAllState:
topic = state["topic"]
res = model.invoke([HumanMessage(content=f"帮我写一首关于{topic}的七言绝句,只写诗句,不需要赏析")]).content
return {"poem": res}
def default_node(state: OverAllState) -> OverAllState:
return {"poem": "请求被拒绝"}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("approve_node", approve_node)
builder.add_node("llm_node", llm_node)
builder.add_node("default_node", default_node)
builder.add_edge(START, "approve_node")
builder.add_edge("llm_node", END)
builder.add_edge("default_node", END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "approval_mode"}}
# 第一次:触发审批中断
interrupt_res = graph.invoke({"topic": "菊花"}, config=config)
print("中断信息:", interrupt_res)
# 人工审批
user_input = input("是否同意调用模型?(y/n): ").strip().lower() # ← 等用户输入
is_approved = (user_input == 'y')
# 恢复运行
approved_res = graph.invoke(Command(resume=is_approved), config=config)
print("审批结果:", approved_res)
# 第二次:再试一次,这次拒绝
config2 = {"configurable": {"thread_id": "approval_mode_2"}}
interrupt_res2 = graph.invoke({"topic": "牡丹花"}, config=config2)
print("\n中断信息:", interrupt_res2)
user_input2 = input("是否同意调用模型?(y/n): ").strip().lower() # ← 等用户输入
is_approved2 = (user_input2 == 'y')
denied_res = graph.invoke(Command(resume=is_approved2), config=config2)
print("审批结果:", denied_res)
运行过程:
中断信息: {'topic': '菊花', '__interrupt__': [Interrupt(value='是否同意调用模型生成诗句?', id='...')]}
是否同意调用模型?(y/n): y ← 用户手动输入
审批结果: {'topic': '菊花', 'poem': '《咏菊》\n西风猎猎卷霜华...', 'is_approved': True}
中断信息: {'topic': '牡丹花', '__interrupt__': [Interrupt(value='是否同意调用模型生成诗句?', id='...')]}
是否同意调用模型?(y/n): n ← 用户手动输入
审批结果: {'topic': '牡丹花', 'poem': '请求被拒绝', 'is_approved': False}
模式4:审核与编辑模式
模型生成内容后,停下来让人类审核修改。
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={"thinking": {"type": "disabled"}}
)
class OverAllState(TypedDict):
topic: str
poem: str
reviewed_poem: str
def llm_node(state: OverAllState) -> OverAllState:
topic = state['topic']
res = model.invoke([HumanMessage(content=f"帮我写一首关于{topic}的七言绝句,只给出诗句,不要赏析")]).content
return {"poem": res}
def review_node(state: OverAllState) -> OverAllState:
reviewed_poem = interrupt({
"instruction": "请审核并修改大模型生成的七言绝句",
"poem": state['poem']
})
return {"reviewed_poem": reviewed_poem}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("llm_node", llm_node)
builder.add_node("review_node", review_node)
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", "review_node")
builder.add_edge("review_node", END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "review_mode"}}
# 触发中断
interrupt_res = graph.invoke({"topic": "布偶猫"}, config=config)
print("中断信息:", interrupt_res)
# 显示原始诗句
original_poem = interrupt_res['__interrupt__'][0].value['poem']
print("\n原始诗句:")
print(original_poem)
# 人工审核修改
user_input = input("\n请审核并修改诗句(直接回车保留原诗): ") # ← 等用户输入
if not user_input.strip():
user_input = original_poem # 用户没改,保留原诗
# 恢复运行
reviewed_res = graph.invoke(Command(resume=user_input), config=config)
print("\n最终结果:")
print(reviewed_res)
运行过程:
中断信息: {'topic': '布偶猫', 'poem': '《布偶猫》\n冰眸玉骨雪团身...', '__interrupt__': [...]}
原始诗句:
《布偶猫》
冰眸玉骨雪团身,蓝影衔春卧月轮。
偶启朱唇呼入梦,云鬟一枕醉红尘。
请审核并修改诗句(直接回车保留原诗): 已审核,诗句没问题 ← 用户手动输入
最终结果:
{'topic': '布偶猫', 'poem': '《布偶猫》\n冰眸玉骨雪团身...', 'reviewed_poem': '已审核,诗句没问题'}
模式5:工具执行审批模式
调用外部工具之前,先问问用户同不同意。
from typing import Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
from langgraph.graph.message import MessagesState
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.messages import HumanMessage, ToolMessage
from langchain.tools import tool
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
@tool
def get_weather(city: str) -> str:
"""查询指定城市的当日天气"""
is_approved = interrupt({
"action": "get_weather",
"question": f"是否同意查询{city}的天气?"
})
if is_approved:
return f"{city} 今天天气不错"
else:
return "用户拒绝查询天气"
tools_by_name = {"get_weather": get_weather}
model = ChatDeepSeek(model="deepseek-v4-flash")
model_with_tools = model.bind_tools([get_weather])
def llm_node(state: MessagesState) -> MessagesState:
messages = state['messages']
response = model_with_tools.invoke(messages)
return {"messages": [response]}
def tool_node(state: MessagesState) -> MessagesState:
last_msg = state['messages'][-1]
tool_msgs = []
for tool_call in last_msg.tool_calls:
tool = tools_by_name[tool_call["name"]]
tool_res = tool.invoke(tool_call["args"])
tool_msg = ToolMessage(
name=tool_call["name"],
content=tool_res,
tool_call_id=tool_call["id"]
)
tool_msgs.append(tool_msg)
return {"messages": tool_msgs}
def router(state: MessagesState) -> Literal["tool_node", END]:
if state['messages'][-1].tool_calls:
return "tool_node"
return END
builder = StateGraph(state_schema=MessagesState)
builder.add_node("llm_node", llm_node)
builder.add_node("tool_node", tool_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges("llm_node", router, path_map=["tool_node", END])
builder.add_edge("tool_node", "llm_node")
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "tool_approval"}}
# 触发中断(工具调用前)
interrupt_res = graph.invoke({"messages": [HumanMessage("今天北京天气如何?")]}, config=config)
print("中断信息:", interrupt_res['__interrupt__'])
# 人工确认
user_input = input("是否同意查询天气?(y/n): ").strip().lower() # ← 等用户输入
is_approved = (user_input == 'y')
# 恢复运行
approved_res = graph.invoke(Command(resume=is_approved), config=config)
print("\n最终回复:")
for msg in approved_res['messages']:
if hasattr(msg, 'pretty_print'):
msg.pretty_print()
运行过程:
中断信息: [Interrupt(value={'action': 'get_weather', 'question': '是否同意查询北京的天气?'}, id='...')]
是否同意查询天气?(y/n): y ← 用户手动输入
最终回复:
================================ Human Message =================================
今天北京天气如何?
================================== Ai Message ==================================
今天北京的天气不错哦!☀️
模式6:单节点串行中断
同一个节点里连续问好几个问题,一个一个来。
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command
class OverAllState(TypedDict):
username: str
age: int
gender: Literal["male", "female"]
def get_info_node(state: OverAllState) -> OverAllState:
username = interrupt("请输入您的用户名:")
age = interrupt("请输入您的年龄:")
gender = interrupt("请输入您的性别:(male/female)")
return {
"username": username,
"age": age,
"gender": gender
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("get_info_node", get_info_node)
builder.add_edge(START, "get_info_node")
builder.add_edge("get_info_node", END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "serial_interrupt"}}
# 第一次中断:问用户名
username_res = graph.invoke({}, config=config)
print("中断信息:", username_res)
user_name = input("请输入您的用户名:") # ← 等用户输入
# 第二次中断:问年龄
age_res = graph.invoke(Command(resume=user_name), config=config)
print("\n中断信息:", age_res)
user_age = input("请输入您的年龄:") # ← 等用户输入
# 第三次中断:问性别
gender_res = graph.invoke(Command(resume=int(user_age)), config=config)
print("\n中断信息:", gender_res)
user_gender = input("请输入您的性别:(male/female): ") # ← 等用户输入
# 最终恢复
final_res = graph.invoke(Command(resume=user_gender), config=config)
print("\n最终结果:", final_res)
运行过程:
中断信息: {'__interrupt__': [Interrupt(value='请输入您的用户名:', id='...')]}
请输入您的用户名:小黄 ← 用户手动输入
中断信息: {'__interrupt__': [Interrupt(value='请输入您的年龄:', id='...')]}
请输入您的年龄:15 ← 用户手动输入
中断信息: {'__interrupt__': [Interrupt(value='请输入您的性别:(male/female)', id='...')]}
请输入您的性别:(male/female): male ← 用户手动输入
最终结果: {'username': '小黄', 'age': 15, 'gender': 'male'}
三、使用规范(避坑指南)
规范1:别用 try/catch 包 interrupt
# ❌ 错误:interrupt 是靠抛异常实现的,catch 了就失效了
def node_a(state):
try:
name = interrupt("请输入姓名")
except Exception as e:
print(e)
# ✅ 正确:interrupt 单独在外面
def node_a(state):
name = interrupt("请输入姓名")
try:
fetch_data() # 这里可以 catch
except Exception as e:
print(e)
规范2:别改变 interrupt 的顺序和数量
# ❌ 错误:根据条件跳过中断
def node_a(state):
name = interrupt("请输入姓名")
if state.get("needs_age"): # 条件可能变化
age = interrupt("请输入年龄") # 有时触发,有时不触发
city = interrupt("请输入城市")
# 恢复时顺序乱了,数据对不上
# ✅ 正确:中断顺序固定
def node_a(state):
name = interrupt("请输入姓名")
age = interrupt("请输入年龄")
city = interrupt("请输入城市")
规范3:别传复杂类型给 interrupt
# ❌ 错误:传函数
interrupt({"validator": validate_input}) # 函数不能 JSON 序列化
# ❌ 错误:传类实例
interrupt({"processor": DataProcessor()}) # 自定义类不能 JSON 序列化
# ✅ 正确:传基本类型或简单字典
interrupt("请输入姓名")
interrupt({"question": "请输入姓名", "hint": "中文名"})
规范4:中断前的副作用操作必须是幂等的
# ❌ 错误:中断前插入数据(不幂等)
def node_a(state):
db.insert_log("pending_approval") # 恢复时会再插一条
approved = interrupt("是否批准?")
# ✅ 正确:用 upsert(幂等)
def node_a(state):
db.upsert_log(status="pending_approval") # 多次执行结果一样
approved = interrupt("是否批准?")
# ✅ 正确:副作用放中断后面
def node_a(state):
approved = interrupt("是否批准?")
if approved:
db.send_notification() # 只执行一次
# ✅ 正确:副作用放另一个节点
def approval_node(state):
approved = interrupt("是否批准?")
return {"approved": approved}
def notify_node(state):
if state["approved"]:
db.send_notification() # 独立节点,不受中断影响
四、面试题
面试题1:interrupt() 是怎么实现暂停的?
答:interrupt()通过抛出 GraphInterrupt异常来实现暂停。所以不能用 try/catch包裹它,否则异常被捕获,中断就失效了。
面试题2:中断恢复时,节点函数会重新执行吗?
答:会。整个节点函数会从头重新运行。之前已经恢复的中断不会重复触发,因为检查点记住了历史 resume值。
面试题3:多个并行中断怎么恢复?
答:用字典按中断 ID 分别传值:
resume_map = {
"中断ID_1": "小黄",
"中断ID_2": 19
}
graph.invoke(Command(resume=resume_map), config=config)
面试题4:中断前的副作用操作要注意什么?
答:必须是幂等的,即执行多次和一次效果一样。否则中断恢复时会重复执行,导致数据重复。
面试题5:同一个超步里,一个任务中断了,其他并行任务会怎样?
答:其他任务不受影响,会正常执行完毕。已完成任务的结果会被保存,恢复时复用。
中断就是给你的图装了个"暂停键"。
- 需要人审批?停一下,等人按键
- 需要人修改?停一下,等人打字
- 需要人确认?停一下,等人点头
用好中断,你的图就不再是"闷头干到底"的自动化脚本,而是能和人类配合默契的好帮手。