让 Agent 在关键节点暂停,等待人类审批后再继续执行。
理解断点是掌握人机协作 AI 系统的第一步。
在 Module 1 和 Module 2 中,我们构建的 Agent 是完全自动运行的:从 START 到 END,LLM 自主决策,工具自动调用,一气呵成。但在真实的生产场景中,让 Agent 完全自主并不总是安全的。
LangGraph 的 Human-in-the-Loop(人机协作)设计解决了三类核心问题:
| 动机 | 场景描述 | 具体例子 |
|---|---|---|
| 审批(Approval) | Agent 想执行某个操作之前,需要人类确认是否放行 | 发送邮件前确认收件人、执行数据库写操作前审核、调用付费 API 前批准 |
| 调试(Debugging) | 能够回溯到某个历史状态,重新执行来定位问题 | Agent 给出了错误答案,回到中间某步重新运行;检查某个工具调用的参数是否正确 |
| 编辑(Editing) | 在图执行过程中修改状态,纠正 Agent 的中间结果 | LLM 提取的关键信息有误,人工修正后继续;修改工具调用参数再执行 |
Lesson 2 聚焦于第一类:审批(Approval)。通过在特定节点设置断点(Breakpoint),让图在那个节点前后暂停,等待人类决策后再继续。这是实现 Human-in-the-Loop 最简洁、最直接的方式。
想象一个财务报销 Agent:它可以自动分析收据、计算金额、填写报销单,但最终提交这一步必须由人类财务主管审批。断点就是你插入那个"等待审批"的卡口。
断点(Breakpoint)是在 编译图时(compile 阶段)就确定的、固定的中断位置。它告诉 LangGraph:"每次执行到这个节点的前面(或后面),自动暂停,不要继续往下走,等待外部指令。"
断点通过 compile() 的两个参数来设置:
# interrupt_before:在节点执行【之前】暂停
graph = builder.compile(
interrupt_before=["tools"], # 在 tools 节点运行前暂停
checkpointer=memory
)
# interrupt_after:在节点执行【之后】暂停
graph = builder.compile(
interrupt_after=["tools"], # 在 tools 节点运行后暂停
checkpointer=memory
)
interrupt_before 和 interrupt_after 都接受一个 节点名称的列表(字符串)。你可以同时在多个节点设置断点,例如 interrupt_before=["tools", "send_email"]。
"静态"意味着断点在 compile() 时就已经硬编码——每次执行到那个节点,一定会暂停,没有条件判断。这与 Lesson 4 将学的动态中断(NodeInterrupt)不同,动态中断可以根据运行时状态决定是否暂停。
| 类型 | 设置时机 | 是否有条件 | 使用场景 |
|---|---|---|---|
| 静态断点(本课) | compile() 时 | 无条件,每次都暂停 | 所有工具调用都需要审批、固定的审查点 |
| 动态中断(Lesson 4) | 节点运行时 | 可以根据状态有条件触发 | 只有当金额超过阈值时才需要审批 |
断点的工作原理是:图执行到断点时,把当前状态保存下来,然后停止执行。等人类发出"继续"的指令后,从保存的状态恢复执行。这个"保存和恢复"的机制就是 Checkpointer(检查点器)。
没有 Checkpointer,断点无法工作。 如果你设置了 interrupt_before 但没有传入 checkpointer,LangGraph 会在运行时抛出错误。检查点器是断点功能的底层支撑。
MemorySaver 是 LangGraph 内置的内存检查点器,它将所有状态保存在进程内存中。适合开发、测试和单机场景。
from langgraph.checkpoint.memory import MemorySaver
# 创建检查点器实例
memory = MemorySaver()
# 编译图时传入 checkpointer 和断点配置
graph = builder.compile(
interrupt_before=["tools"],
checkpointer=memory # 必须提供!
)
检查点器用 thread_id 来区分不同的对话/运行会话。每次运行图时,你需要提供一个配置字典:
# config:指定这次运行使用哪个 thread
thread = {"configurable": {"thread_id": "1"}}
# 同一个 thread_id = 同一个对话上下文
# 不同 thread_id = 独立的、互不干扰的对话
每次图执行完一个节点,MemorySaver 都会自动将当前完整状态(State)保存为一个 检查点(checkpoint)。当断点触发、图暂停时,最新的检查点就记录了"暂停前的那一刻"。调用 invoke(None, config) 恢复执行时,LangGraph 从该检查点读取状态继续运行。
| 检查点器 | 存储位置 | 适用场景 |
|---|---|---|
MemorySaver |
进程内存(重启后丢失) | 开发调试、教学示例、原型验证 |
SqliteSaver |
本地 SQLite 文件 | 单机生产、轻量持久化 |
PostgresSaver |
PostgreSQL 数据库 | 分布式生产系统、跨进程持久化 |
以下是 Notebook 中的完整示例。我们构建一个带有数学工具(加法、乘法、除法)的 Agent,并在 tools 节点前设置断点,让人类在工具调用之前进行审批。
from langchain_deepseek import ChatDeepSeek
# 定义三个数学工具函数
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
def add(a: int, b: int) -> int:
"""Adds a and b."""
return a + b
def divide(a: int, b: int) -> float:
"""Divide a by b."""
return a / b
# 工具列表
tools = [add, multiply, divide]
# 绑定工具到 LLM(LLM 可以选择调用哪个工具)
llm = ChatDeepSeek(model="deepseek-v4-pro")
llm_with_tools = llm.bind_tools(tools)
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.prebuilt import tools_condition, ToolNode
from langchain_core.messages import SystemMessage
# 系统提示
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic on a set of inputs.")
# 定义 assistant 节点:调用带工具的 LLM
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
# 构建图
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
"assistant",
tools_condition, # 如果 LLM 要调用工具 → tools;否则 → END
)
builder.add_edge("tools", "assistant") # 工具结果返回给 LLM
# 关键:创建检查点器 + 编译时指定断点
memory = MemorySaver()
graph = builder.compile(
interrupt_before=["tools"], # 在 tools 节点执行前暂停
checkpointer=memory # 必须提供检查点器
)
MessagesState 是 LangGraph 内置的状态类型,它包含一个带 Reducer 的 messages 字段(自动追加而非覆盖)。ToolNode 是内置的工具执行节点,它自动处理 LLM 返回的 tool_calls,执行对应函数并将结果以 ToolMessage 追加到状态中。
from langchain_core.messages import HumanMessage
# 初始输入:让 Agent 计算 2 乘以 3
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
# 指定 thread(对话上下文)
thread = {"configurable": {"thread_id": "1"}}
# 以流式模式运行图,直到遇到断点自动停止
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
图输出了 Human Message 和 AI Message(AI 决定调用 multiply 工具),然后自动暂停——没有继续执行 tools 节点。此时程序控制权返回给你,你可以查看状态、做出决策。
图暂停后,你可以随时用 graph.get_state(config) 来查看当前的完整状态快照。这个方法返回一个 StateSnapshot 对象,包含了当前的 State 内容以及许多有用的元数据。
# 获取当前状态快照
state = graph.get_state(thread)
# 查看下一个待执行的节点
print(state.next)
state.next 返回一个元组,包含图下一步准备执行的节点名称。当它显示 ('tools',) 时,意味着:图确实已经暂停在 tools 节点执行之前,这正是我们设置 interrupt_before=["tools"] 的效果。
# state 对象包含多个有用属性
state = graph.get_state(thread)
state.values # 当前 State 的全部字段值(MessagesState 里就是 messages 列表)
state.next # 下一步要执行的节点(元组),暂停时非空,结束时为空元组
state.config # 当前检查点的配置信息(包含 checkpoint_id)
state.metadata # 元数据(step 计数、source 等)
state.created_at # 检查点创建时间
state.parent_config # 上一个检查点的配置(用于时间旅行)
# 查看所有消息
for msg in state.values["messages"]:
msg.pretty_print()
state.next == ('tools',) → 图已暂停,等待审批工具调用
state.next == () → 图已执行完毕(到达 END)
这个属性是程序化判断"图是否还在运行中"的标准方法。
暂停后,你可以深入检查 AI 打算调用什么工具、传什么参数,这是做出审批决策的依据:
# 获取最后一条消息(AI 的工具调用请求)
last_msg = state.values["messages"][-1]
# 查看工具调用详情
if hasattr(last_msg, 'tool_calls'):
for tc in last_msg.tool_calls:
print(f"工具名称: {tc['name']}")
print(f"调用参数: {tc['args']}")
print(f"调用 ID: {tc['id']}")
现在你知道 Agent 打算调用 multiply(a=2, b=3)。作为人类审批者,你可以判断这个调用是否合理,然后决定是否放行。
这是断点机制中最优雅的设计之一:把 None 作为 input 传给 invoke 或 stream,图就会从上一个检查点继续执行,不需要重新输入任何数据。
# 人类审批后,继续执行图
# 传入 None 表示"从检查点恢复,不提供新输入"
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
注意输出的顺序:
multiply(2, 3) 返回了 6当你传入 None 时,LangGraph 知道这不是新的用户输入,而是"恢复信号"。它会从指定 thread_id 对应的最新检查点读取保存的状态,然后继续从 state.next 所指向的节点(即 tools)开始执行。整个历史消息链都被完整保留。
如果人类审批者决定拒绝这次工具调用,只需不调用 invoke(None, ...),直接结束流程即可。这次对话的检查点仍然保留在 MemorySaver 中,但不会再有任何继续执行。
# 完整的审批流程示例
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
thread = {"configurable": {"thread_id": "2"}}
# 运行直到断点
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
# 等待人类审批
user_approval = input("Do you want to call the tool? (yes/no): ")
if user_approval.lower() == "yes":
# 审批通过:继续执行
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
else:
# 审批拒绝:不继续执行
print("Operation cancelled by user.")
| 参数 | 暂停时机 | 此时的 state.values | 适用场景 |
|---|---|---|---|
interrupt_before=["tools"] |
tools 节点执行之前 | 包含 AI 的工具调用请求(AIMessage with tool_calls),但还没有 ToolMessage | 在工具执行前审批:看 AI 打算调用什么、传什么参数,决定是否放行 |
interrupt_after=["tools"] |
tools 节点执行之后 | 包含 AI 的工具调用请求 + ToolMessage(工具已经执行完并返回了结果) | 在工具执行后审核结果:审查工具的返回值是否合理,再决定是否让 LLM 继续处理 |
# 在 send_email 节点执行前暂停
graph = builder.compile(
interrupt_before=["send_email"],
checkpointer=memory
)
# 图暂停时,state 里包含 AI 拟好的邮件内容(工具调用参数)
# 人类可以查看收件人、主题、正文,决定是否发送
state = graph.get_state(thread)
tool_call = state.values["messages"][-1].tool_calls[0]
print(f"收件人: {tool_call['args']['to']}")
print(f"主题: {tool_call['args']['subject']}")
# → 人类确认后:graph.invoke(None, thread)
# 在 web_search 节点执行后暂停
graph = builder.compile(
interrupt_after=["web_search"],
checkpointer=memory
)
# 图暂停时,搜索已经完成,state 里有 ToolMessage(搜索结果)
# 人类可以检查来源、筛选掉不可信的结果
state = graph.get_state(thread)
# 最后一条是 ToolMessage(工具执行结果)
search_result = state.values["messages"][-1].content
print(f"搜索结果: {search_result}")
# → 确认结果可信后:graph.invoke(None, thread) 让 LLM 继续总结
使用 interrupt_before:当你想在"危险操作"发生之前拦截。例如:写数据库、发邮件、调用付费 API、执行代码——这类操作不可撤销,必须在执行前审批。
使用 interrupt_after:当你想审核"中间结果"的质量。例如:网页搜索完了,审核来源是否可信;LLM 提取了信息,确认提取准确后再继续。
将前面所有知识整合成一个完整可用的审批流程模板:
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_core.messages import HumanMessage
# ─── 构建图(假设 tools、llm_with_tools、assistant 已定义)───
memory = MemorySaver()
graph = builder.compile(
interrupt_before=["tools"],
checkpointer=memory
)
def run_with_approval(user_question: str, thread_id: str):
"""完整的带审批的 Agent 执行函数"""
thread = {"configurable": {"thread_id": thread_id}}
initial_input = {"messages": HumanMessage(content=user_question)}
print("=== Agent 开始执行 ===")
# 第一阶段:运行到断点
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
# 检查是否暂停在断点(如果 next 非空,说明还有待执行的节点)
state = graph.get_state(thread)
if not state.next:
print("=== 图已执行完毕(无需工具调用)===")
return
print(f"\n=== 图已暂停,下一步: {state.next} ===")
# 显示工具调用详情供人类审查
last_msg = state.values["messages"][-1]
if hasattr(last_msg, 'tool_calls'):
print("\n待审批的工具调用:")
for tc in last_msg.tool_calls:
print(f" 工具: {tc['name']}")
print(f" 参数: {tc['args']}")
# 等待人类审批
approval = input("\n是否批准执行工具?(yes/no): ")
if approval.lower() == "yes":
print("\n=== 审批通过,继续执行 ===")
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
else:
print("=== 操作已被用户取消 ===")
# 使用示例
run_with_approval("Multiply 2 and 3", thread_id="session-001")
在生产环境中,图通常部署为 LangGraph 服务(通过 langgraph dev 启动本地服务器,或部署到 LangSmith Studio)。此时你通过 LangGraph SDK 与图交互,断点的逻辑完全一致:
from langgraph_sdk import get_client
from langchain_core.messages import HumanMessage
# 连接本地 LangGraph 开发服务器
client = get_client(url="http://127.0.0.1:2024")
# 创建新 thread
thread = await client.threads.create()
# 流式运行图,并在 API 层面指定断点
# 注意:也可以直接在 compile() 里设置,这里是另一种方式
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input=initial_input,
stream_mode="values",
interrupt_before=["tools"], # 在 API 层面设置断点
):
print(f"Event type: {chunk.event}")
messages = chunk.data.get('messages', [])
if messages:
print(messages[-1])
# 审批后继续(传入 None)
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=None, # None = 从检查点恢复
stream_mode="values",
interrupt_before=["tools"],
):
messages = chunk.data.get('messages', [])
if messages:
print(messages[-1])
在模块的 /studio 目录下运行 langgraph dev,启动本地开发服务器后会得到:
API: http://127.0.0.1:2024
Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
在 Studio UI 中,你可以可视化地看到图的执行状态、断点触发和手动恢复,非常适合调试。
compile(interrupt_before=[...])compile(interrupt_after=[...])checkpointergraph.get_state(config)state.next 查看待执行节点state.values 查看完整状态graph.invoke(None, config)graph.stream(None, config)None 表示"从检查点恢复"interrupt_before 在动刀前确认,interrupt_after 在做完后审核。MemorySaver 是那个"记住图执行到哪里了"的笔记本。invoke(None) 是"好,继续吧"的指令。掌握了静态断点后,Lesson 3(Edit State & Human Feedback)将教你在图暂停时修改状态——不只是"放行"或"拒绝",而是"我改一下参数再继续"。Lesson 4(Dynamic Breakpoints)则介绍如何根据运行时条件有选择地触发中断,而不是每次都暂停。