人机交互(Human-in-the-Loop)
人机交互(Human-in-the-Loop,HITL)是 LangGraph 的杀手级特性。它允许 Agent 在执行过程中暂停,等待人类审批、修改或提供额外信息,然后恢复执行。这在生产环境中至关重要——你不会希望 AI 在没有人类确认的情况下发送邮件、执行代码或修改数据库。
为什么需要 HITL
AI Agent 的典型风险场景:
Agent: "我找到了所有符合条件的用户,准备删除他们的账号。"
↓
[没有 HITL] → 直接执行,无法撤回
[有 HITL] → 暂停,等待人类确认HITL 的核心价值:
| 场景 | 说明 |
|---|---|
| 安全审批 | 删除数据、发送邮件、执行代码前确认 |
| 质量控制 | AI 生成的内容需要人工审核 |
| 信息补充 | AI 需要人类提供缺失信息 |
| 纠错 | 人类发现 AI 走偏时及时纠正 |
| 合规 | 法律/政策要求人工审核某些操作 |
三种 HITL 机制
LangGraph 提供三种不同粒度的人机交互方式:
1. interrupt_before — 节点执行前暂停
在某个节点执行之前暂停,等待人类确认:
python
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("dangerous_action", delete_node)
graph.add_edge(START, "agent")
graph.add_edge("agent", "dangerous_action")
graph.add_edge("dangerous_action", END)
# 在 dangerous_action 执行前暂停
app = graph.compile(
checkpointer=MemorySaver(),
interrupt_before=["dangerous_action"], # ← 关键
)2. interrupt_after — 节点执行后暂停
在某个节点执行之后暂停,检查结果:
python
# 在 agent 节点执行后暂停(查看 AI 的输出再决定是否继续)
app = graph.compile(
checkpointer=MemorySaver(),
interrupt_after=["agent"], # ← 关键
)3. interrupt() 函数 — 动态暂停(推荐)
在节点内部根据条件动态决定是否暂停:
python
from langgraph.types import interrupt
def agent_node(state: AgentState) -> dict:
response = llm.invoke(state["messages"])
# 检查是否有工具调用需要审批
if response.tool_calls:
for tc in response.tool_calls:
if tc["name"] in ["delete_file", "send_email", "execute_code"]:
# 动态暂停,等待人类审批
human_decision = interrupt({
"type": "approval",
"action": tc["name"],
"args": tc["args"],
"message": f"Agent 想要执行 {tc['name']},是否允许?",
})
if human_decision == "deny":
# 人类拒绝,取消工具调用
response.tool_calls = [
t for t in response.tool_calls if t != tc
]
return {"messages": [response]}interrupt() 详解
interrupt() 是 LangGraph 最灵活的 HITL 机制。它的工作方式:
- 暂停:执行到
interrupt()时,图的执行暂停 - 保存状态:Checkpointer 自动保存当前状态
- 返回给调用者:调用者收到暂停信号和
interrupt()传入的值 - 人类操作:人类查看信息、做出决策
- 恢复执行:调用者传入人类的决策,图从暂停点恢复
interrupt() 的返回值
python
def my_node(state: AgentState) -> dict:
# interrupt() 的参数会返回给调用者(作为暂停的原因)
human_input = interrupt("请输入你的名字:")
# 当图恢复执行时,human_input 就是人类传入的值
return {"messages": [("assistant", f"你好,{human_input}!")]}与调用者交互
python
# 1. 运行图(会暂停)
config = {"configurable": {"thread_id": "1"}}
result = app.invoke({"messages": [("user", "你好")]}, config=config)
# 2. 检查是否暂停
state = app.get_state(config)
if state.next and "__interrupt__" in str(state.next):
# 获取 interrupt 的值
interrupt_value = state.tasks[0].interrupts[0].value
print(interrupt_value) # "请输入你的名字:"
# 3. 人类输入后恢复
app.invoke(Command(resume="张三"), config=config)Command 对象
Command 是 LangGraph 用于恢复中断的特殊对象:
python
from langgraph.types import Command
# 恢复执行,传入人类的输入
app.invoke(Command(resume="人类的回答"), config=config)
# 恢复执行,传入复杂对象
app.invoke(Command(resume={
"approved": True,
"reason": "确认执行",
"modifications": {"limit": 100},
}), config=config)完整 HITL 工作流
下面是一个完整的审批工作流示例:
python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
# 状态
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
approved_actions: list
# 需要审批的工具
@tool
def delete_user(user_id: str) -> str:
"""删除用户账号"""
return f"用户 {user_id} 已删除"
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""发送邮件"""
return f"邮件已发送到 {to}"
# 工具定义(带审批)
DANGEROUS_TOOLS = {"delete_user", "send_email"}
# Agent 节点
def agent_node(state: AgentState) -> dict:
llm = ChatOpenAI(model="gpt-4o").bind_tools([delete_user, send_email])
response = llm.invoke(state["messages"])
return {"messages": [response]}
# 审批节点
def approval_node(state: AgentState) -> dict:
last_message = state["messages"][-1]
if not last_message.tool_calls:
return {}
approved_calls = []
for tc in last_message.tool_calls:
if tc["name"] in DANGEROUS_TOOLS:
# 需要审批
decision = interrupt({
"type": "tool_approval",
"tool": tc["name"],
"args": tc["args"],
"message": f"Agent 想要调用 `{tc['name']}`,参数:{tc['args']}。是否允许?",
})
if decision.get("approved"):
approved_calls.append(tc)
else:
# 安全工具,直接执行
approved_calls.append(tc)
# 更新消息中的 tool_calls(只保留已审批的)
return {
"messages": [last_message.model_copy(update={"tool_calls": approved_calls})],
"approved_actions": [tc["name"] for tc in approved_calls],
}
# 工具执行节点
def tool_node(state: AgentState) -> dict:
last_message = state["messages"][-1]
results = []
tools_map = {"delete_user": delete_user, "send_email": send_email}
for tc in last_message.tool_calls:
tool = tools_map.get(tc["name"])
if tool:
result = tool.invoke(tc["args"])
results.append({"role": "tool", "content": result, "tool_call_id": tc["id"]})
return {"messages": results}
# 路由
def should_continue(state: AgentState) -> str:
last_message = state["messages"][-1]
if last_message.tool_calls:
return "approval"
return END
# 构建图
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("approval", approval_node)
graph.add_node("tools", tool_node)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue, {
"approval": "approval",
END: END,
})
graph.add_edge("approval", "tools")
graph.add_edge("tools", "agent")
# 编译
app = graph.compile(checkpointer=MemorySaver())
# 使用
config = {"configurable": {"thread_id": "demo"}}
# 发送请求
result = app.invoke({
"messages": [("user", "帮我删除用户 12345")]
}, config=config)
# 检查是否需要审批
state = app.get_state(config)
if state.next:
interrupt_info = state.tasks[0].interrupts[0].value
print(interrupt_info["message"])
# "Agent 想要调用 `delete_user`,参数:{'user_id': '12345'}。是否允许?"
# 人类审批
app.invoke(Command(resume={"approved": True}), config=config)使用 create_react_agent 的 HITL
create_react_agent 也支持 HITL:
python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
agent = create_react_agent(
model=llm,
tools=[delete_user, send_email],
checkpointer=MemorySaver(),
interrupt_before=["tools"], # 在工具执行前暂停
)
# 运行
config = {"configurable": {"thread_id": "1"}}
result = agent.invoke({"messages": [("user", "删除用户123")]}, config=config)
# 检查暂停
state = agent.get_state(config)
print(state.next) # ('tools',) → 在 tools 节点前暂停
# 人类确认后恢复
agent.invoke(Command(resume=True), config=config) # 简单的确认模式多轮 HITL
复杂的审批流程可能需要多轮交互:
python
def complex_approval(state: AgentState) -> dict:
last_message = state["messages"][-1]
for tc in last_message.tool_calls:
# 第一轮:确认操作
confirm = interrupt({
"step": "confirm",
"message": f"确认执行 {tc['name']}?",
})
if not confirm.get("approved"):
continue
# 第二轮:确认参数
params_ok = interrupt({
"step": "verify_params",
"message": f"参数确认:{tc['args']}",
})
if not params_ok.get("approved"):
# 允许修改参数
new_args = params_ok.get("modified_args", tc["args"])
tc["args"] = new_args
return {"messages": [last_message]}HITL 与流式输出
结合流式输出,可以实时显示 Agent 的思考过程:
python
config = {"configurable": {"thread_id": "stream-demo"}}
# 流式执行
for event in app.stream(input_data, config=config):
for node, output in event.items():
print(f"[{node}]", output)
# 检查是否中断
if node == "__interrupt__":
print("需要人类介入!")
# 人类操作后继续...最佳实践
- 明确中断点:不要在每个节点都加中断,只在关键操作前加
- 提供足够信息:
interrupt()的参数要包含足够的信息供人类决策 - 处理拒绝:人类拒绝后要有合理的降级逻辑
- 超时处理:生产环境要考虑中断超时
- 日志记录:记录所有人工决策,便于审计
python
# ✅ 好的实践
decision = interrupt({
"action": "delete_user",
"target": "user_12345",
"reason": "用户请求注销",
"risk_level": "high",
"irreversible": True,
"estimated_impact": "该用户的3个订单将被取消",
})
# ❌ 不好的实践
decision = interrupt("确认?") # 信息太少小结
| 机制 | 说明 | 适用场景 |
|---|---|---|
interrupt_before | 节点执行前暂停 | 已知的危险节点 |
interrupt_after | 节点执行后暂停 | 审查 AI 输出 |
interrupt() | 动态暂停 | 复杂的条件审批 |
Command(resume=...) | 恢复执行 | 传入人类决策 |
下一篇
→ 多 Agent 协作:Supervisor、Swarm、子图嵌套、Agent 通信模式。