如果 Agent 中的工具只是负责搜索资料、查询数据等类的操作,即使模型判断有偏差,通常也只是返回结果不够准确。
但当 Tool 开始具备发送邮件、修改数据库、退款、删除文件等能力,问题就不同了。模型生成的 Tool Call 会真正改变外部系统,一次判断错误可能直接产生业务后果。
因此,一个可以自主调用工具的 Agent,还需要具备另一种能力:在执行某些操作之前暂停,把决定权交给人。
Human-in-the-loop(HITL,人在回路,或人机协同)提供的就是这层控制。本文重点看清它在 LangChain Agent 中如何工作,以及 HumanInTheLoopMiddleware、Interrupt、Checkpoint、Thread 和 Command(resume=...) 是怎样连接起来的。
一、人工介入边界
假设我们正在开发一个电商客服 Agent。
它可以使用三个 Tool:
get_order
get_refund_policy
issue_refund
用户说:
订单 A1024 的商品有质量问题,帮我退款。
Agent 可以先查询订单:
get_order(order_id="A1024")
然后读取退款规则:
get_refund_policy(order_id="A1024")
这两步主要读取数据,通常也不会修改真实业务状态。
而接下来则不一样:
issue_refund(
order_id="A1024",
amount=899
)
这个 Tool 一旦执行,可能真的向支付系统发起退款。
这里需要区分两个阶段:
模型生成 Tool Call
↓
Tool 真正开始执行
Human-in-the-loop 介入的位置就在两者之间。
完整过程变成:
用户提出退款
↓
Agent 查询订单
↓
Agent 判断可以退款
↓
生成 issue_refund Tool Call
↓
人工审核
↓
批准 / 修改 / 拒绝
↓
根据决定继续运行
因此,HITL 很适合放在高风险、有副作用或者需要业务责任确认的操作之前。
而像查询天气、搜索知识库之类的低风险 Tool 则不需要 HITL 的介入;如果这些也逐次要求确认,会让 Agent 的使用体验变得非常繁琐。
二、审核运行机制
在当前 LangChain Agent 中,这类 Tool 审核可以通过 HumanInTheLoopMiddleware 完成。
它属于 Agent Middleware。
如果前面已经了解过 Middleware,可以把 HITL 看成一个运行在模型输出之后的检查器:
Model
↓
生成 AIMessage
↓
HumanInTheLoopMiddleware
↓
检查其中的 Tool Call
↓
是否需要人工审核
├─ 否 → 继续执行 Tool
└─ 是 → 触发 Interrupt
这个执行位置很重要。
模型此时已经完成推理,并明确生成了准备调用的 Tool 和参数,例如:
issue_refund
order_id = A1024
amount = 899
但 issue_refund 尚未真正运行。
Middleware 会根据事先配置的审核策略检查这次 Tool Call。如果匹配到需要人工处理的操作,就构造一份审核请求并触发 Interrupt。
整个生命周期概括为:
Model
↓
Tool Call
↓
after_model Middleware
↓
HITL 策略检查
↓
Interrupt
↓
Checkpoint 保存状态
↓
等待人工决定
↓
Command(resume=...)
↓
恢复执行
这里的“暂停”也不同于:
input("是否批准?")
input() 会占着当前 Python 进程等待用户输入。
LangGraph 的 Interrupt 会把 Graph State 交给 Checkpointer 保存。当前请求可以结束,过一段时间再根据原来的执行状态继续。
因此,一个退款申请完全可能上午产生,下午才由客服主管批准。
三、中断配置语法
真正使用 HITL 时,我们需要理解 HumanInTheLoopMiddleware 这个类:
from langchain.agents.middleware import HumanInTheLoopMiddleware
它最主要的参数是:
interrupt_on
它是一个字典,Key 是 Tool 名称,Value 决定这个 Tool 是否需要人工审核,以及人工可以采取哪些操作。
最简单的配置如下:
HumanInTheLoopMiddleware(
interrupt_on={
"get_order": False,
"get_refund_policy": False,
"issue_refund": True,
}
)
这里有两种基础写法。
"get_order": False
表示匹配到 get_order 时不触发人工审核,可以正常继续执行。
而:
"issue_refund": True
表示每次调用 issue_refund 都进入审核流程,并使用默认审核配置。
实际项目通常还需要更细的控制,此时可以给 Tool 传入一个配置对象:
"issue_refund": {
"allowed_decisions": ["approve", "edit", "reject"]
}
allowed_decisions 用来限制审核者能够采取哪些决定。
当前内置的四种决定是:
| 类型 | 含义 | 典型场景 |
|---|---|---|
approve | 按原 Tool Call 执行 | 退款金额正确 |
edit | 修改参数后执行 | 将退款 899 元改为 699 元 |
reject | 不执行,并把原因反馈给 Agent | 当前订单不允许退款 |
respond | 人工直接提供 Tool Result | Agent 主动向用户询问信息 |
对于退款、删除数据、付款等具有一定操作风险的 Tool,通常使用前三种。
respond 更适合这样的 Tool:
ask_user("请选择退款原因")
人工输入:
商品质量问题
这段内容会直接作为成功的 Tool Result 返回给 Agent。
因此,如果审核者想阻止退款,应使用 reject。使用 respond 会让 Agent 认为这个 Tool 已经成功获得了一个结果。
除了 interrupt_on,还可以配置:
description_prefix
它负责设置审核提示的统一前缀,例如:
HumanInTheLoopMiddleware(
interrupt_on={...},
description_prefix="以下操作需要人工确认",
)
对于具体 Tool,还可以通过 description 自定义审核描述。
这些描述最终可以展示在 Web、桌面客户端或企业审批页面中,告诉审核者 Agent 准备执行什么。
四、退款审核示例
下面把前面的电商客服场景实现出来。
为了让示例集中在 HITL,这里模拟一个简单的内存订单业务系统。
先定义两个 Tool:
from langchain.tools import tool
ORDERS = {
"A1024": {
"status": "paid",
"amount": 899,
"refunded": False,
}
}
@tool
def get_order(order_id: str) -> dict:
"""查询订单信息。"""
return ORDERS.get(order_id, {"error": "order not found"})
@tool
def issue_refund(order_id: str, amount: int, reason: str) -> str:
"""为订单执行退款。"""
order = ORDERS.get(order_id)
if not order:
return "order not found"
if amount > order["amount"]:
return "refund amount exceeds order amount"
order["refunded"] = True
return f"refund submitted: order={order_id}, amount={amount}"
其中 get_order 只读订单,可以自动执行;issue_refund 会修改订单状态,因此需要审核。
创建 Agent 时,需要引入三个关键对象:
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
HumanInTheLoopMiddleware 负责审核策略。
InMemorySaver 是 Checkpointer,它负责保存暂停时的 Graph State。它适合本地演示和测试;生产环境应换成持久化 Checkpointer。
然后创建 Agent:
agent = create_agent(
model="gpt-5.5",
tools=[get_order, issue_refund],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"get_order": False,
"issue_refund": {
"allowed_decisions": [
"approve",
"edit",
"reject",
]
},
}
)
],
checkpointer=InMemorySaver(),
)
这里可以看到,HITL 并没有改变 Tool 的定义。
Tool 仍然只是:
@tool
def issue_refund(...):
...
审核规则放在 Middleware 中管理。这样业务 Tool 不需要同时承担“谁可以批准我”“什么时候需要确认”这类流程控制职责。
接下来调用 Agent。
HITL 需要能够识别一条暂停中的执行记录,因此调用时还要提供 thread_id:
config = {
"configurable": {
"thread_id": "refund-A1024"
}
}
可以把 thread_id 理解为这次 Agent 会话在持久化系统中的定位标识。
然后发送用户请求:
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": (
"订单 A1024 的商品存在质量问题,"
"请查询订单后为用户全额退款。"
),
}
]
},
config=config,
version="v2",
)
print(result.interrupts)
这里的:
version="v2"
表示使用当前 invoke 的 V2 输出形式。返回结果是 GraphOutput,可以直接通过:
result.interrupts
读取暂停信息。
模型生成的 Tool Call 可能略有不同。下面是根据该流程整理的典型示例输出:
Interrupt(
value={
"action_requests": [
{
"name": "issue_refund",
"arguments": {
"order_id": "A1024",
"amount": 899,
"reason": "商品质量问题"
}
}
],
"review_configs": [
{
"action_name": "issue_refund",
"allowed_decisions": [
"approve",
"edit",
"reject"
]
}
]
}
)
从结果可以看到,Agent 已经完成了两件事:读取订单,并生成退款方案。
但 issue_refund 此时仍然处在等待审核状态。
action_requests 告诉前端“Agent 准备执行什么”,review_configs 则告诉前端“这次允许审核者做什么”。
因此应用层完全可以据此生成一个审批卡片:
退款申请
订单:A1024
退款金额:899 元
原因:商品质量问题
[批准] [修改] [拒绝]
这比简单弹出“是否继续执行?”更适合真正的业务系统。
五、人工决策处理
人工做出决定之后,需要把结果送回原来的 Agent 执行。
这一步使用:
from langgraph.types import Command
其中最重要的是:
Command(resume=...)
resume 表示:把外部提供的结果送回当前处于暂停状态的执行流程。
假设客服主管确认 899 元全额退款,可以这样恢复:
result = agent.invoke(
Command(
resume={
"decisions": [
{
"type": "approve"
}
]
}
),
config=config,
version="v2",
)
print(ORDERS["A1024"])
恢复时必须继续使用前面的:
thread_id="refund-A1024"
因为 Checkpointer 需要通过它找到之前保存的状态。
运行结果
{
'status': 'paid',
'amount': 899,
'refunded': True
}
从结果可以确认,第一次执行 Agent 时退款并没有发生。
只有收到 approve 之后,issue_refund 才真正运行。
如果客服发现应该只退款 699 元,可以使用 edit:
Command(
resume={
"decisions": [
{
"type": "edit",
"edited_action": {
"name": "issue_refund",
"args": {
"order_id": "A1024",
"amount": 699,
"reason": "部分退款",
},
},
}
]
}
)
edited_action 包含两个字段:
name
args
name 是最终要执行的 Tool 名称,args 是修改后的参数。
实际使用时应尽量只做必要修改。例如审核者修改退款金额很合理,但如果把退款 Tool 改成完全不同的业务操作,模型后续可能需要重新判断整个任务。
还有一种结果是拒绝:
Command(
resume={
"decisions": [
{
"type": "reject",
"message": (
"该订单已经超过退款期限,"
"不要继续执行退款,请向用户说明原因。"
),
}
]
}
)
此时 issue_refund 不会运行。
message 会作为反馈提供给 Agent,所以 Agent 可以继续生成类似:
该订单已经超过退款期限,目前无法直接退款……
这也是 HITL 比简单的“确认弹窗”多出的一层能力:人工决定可以继续参与 Agent 后续推理。
六、条件审核策略
真实项目里,经常会遇到一种情况:同一个 Tool,有些调用可以自动执行,有些调用需要人工确认。
例如:
issue_refund(...)
如果所有退款都人工审核,客服 Agent 的自动化价值会明显下降。
业务规则可能是:
退款金额 <= 100 元
自动执行
退款金额 > 100 元
人工审批
这时可以使用 interrupt_on 配置中的:
when
when 接收一个 ToolCallRequest,根据本次 Tool Call 返回布尔值:
True → 暂停审核
False → 自动继续
先定义判断函数:
from langchain.agents.middleware import ToolCallRequest
def large_refund(request: ToolCallRequest) -> bool:
amount = request.tool_call["args"].get("amount", 0)
return amount > 100
然后配置:
HumanInTheLoopMiddleware(
interrupt_on={
"issue_refund": {
"allowed_decisions": [
"approve",
"edit",
"reject",
],
"when": large_refund,
}
}
)
此时两次 Tool Call 的行为会不同:
issue_refund(amount=59)
↓
自动执行
issue_refund(amount=899)
↓
触发 Interrupt
↓
人工审核
when 很适合根据 Tool 参数实现风险分级,例如:
退款金额 > 100 元
转账金额 > 1000 元
删除生产环境文件
修改正式数据库
向外部客户发送邮件
这样设计以后,审核策略关注的是一次操作产生的风险,而不只是 Tool 名称。
需要注意的是,条件式 when 属于较新的 HITL 配置能力,当前要求 langchain>=1.3.3。
七、生产使用边界
当我们要在生产系统中使用 HITL 机制时,还需要关注几个问题。
第一个是 Checkpointer。
示例中的:
InMemorySaver()
数据只存在当前进程内。进程退出以后,等待审核的状态也会丢失。
生产环境应使用持久化 Checkpointer,例如 PostgreSQL 或 MongoDB 对应的 Saver,使下面这种流程成为可能:
09:00
Agent 提交 899 元退款申请
↓
Checkpoint 持久化
09:01
Agent 请求结束
14:30
主管打开审批中心
↓
点击批准
14:30
根据 thread_id 加载原状态
↓
继续执行退款
第二个问题是幂等性。
如果直接使用 LangGraph 的 interrupt() 编写自定义审核流程,需要知道一个重要执行规则:恢复 Interrupt 时,包含 Interrupt 的 Node 会从 Node 开头重新执行。
因此这种代码需要特别小心:
def node(state):
create_payment_record()
approved = interrupt("是否批准?")
...
恢复执行时,create_payment_record() 可能再次运行。
Interrupt 之前存在副作用的操作,应设计成可重复执行而不会产生额外结果,也就是具备幂等性。
对于普通的 Tool 审批场景,使用 HumanInTheLoopMiddleware 可以减少自己处理这些执行细节的工作。
第三个问题是权限边界。
HITL 适合控制:
是否退款
是否删除
是否发送
退款多少
发送给谁
但数据库账号本身仍然应该遵守最小权限,文件 Tool 仍然应该限制可访问目录,付款接口也应该存在金额限制、鉴权和业务校验。
因此在一个完整 Agent 系统中,可以把几层控制理解为:
Tool 权限
↓
限定 Agent 能够执行什么
Guardrails
↓
检查输入、输出和行为约束
Middleware
↓
干预 Agent 执行过程
Human-in-the-loop
↓
把高风险决定交给人
不同层解决的是不同问题,没有必要全部压到 HITL 上。
总结
Human-in-the-loop 解决的是 Agent 获得真实执行能力之后的控制问题。模型可以负责理解需求、查询信息、形成方案并生成 Tool Call,但涉及退款、删除、付款、发送等高风险动作时,可以先暂停执行,再由人决定是否继续。
在 LangChain 中,这条链路由 HumanInTheLoopMiddleware、Interrupt、Checkpointer、thread_id 和 Command(resume=...) 共同完成:Middleware 判断哪些 Tool Call 需要审核,Interrupt 暂停执行,Checkpoint 保存现场,thread_id 定位原来的运行状态,最终通过 resume 把人工决定送回 Agent。
实际项目中,不必让每个 Tool 都经过人工确认。更合适的方式是按照副作用、金额、环境和业务风险设置审核条件。这样既能保留 Agent 的自动执行能力,也能让真正需要承担责任的操作停在合适的位置。
社区讨论
参与讨论
有问题或想法?欢迎继续讨论。