如果 Agent 中的工具只是负责搜索资料、查询数据等类的操作,即使模型判断有偏差,通常也只是返回结果不够准确。

但当 Tool 开始具备发送邮件、修改数据库、退款、删除文件等能力,问题就不同了。模型生成的 Tool Call 会真正改变外部系统,一次判断错误可能直接产生业务后果。

因此,一个可以自主调用工具的 Agent,还需要具备另一种能力:在执行某些操作之前暂停,把决定权交给人。

Human-in-the-loop(HITL,人在回路,或人机协同)提供的就是这层控制。本文重点看清它在 LangChain Agent 中如何工作,以及 HumanInTheLoopMiddleware、Interrupt、Checkpoint、Thread 和 Command(resume=...) 是怎样连接起来的。

一、人工介入边界

假设我们正在开发一个电商客服 Agent。

它可以使用三个 Tool:

代码片段Text
get_order
get_refund_policy
issue_refund

用户说:

代码片段Text
订单 A1024 的商品有质量问题,帮我退款。

Agent 可以先查询订单:

代码片段Text
get_order(order_id="A1024")

然后读取退款规则:

代码片段Text
get_refund_policy(order_id="A1024")

这两步主要读取数据,通常也不会修改真实业务状态。

而接下来则不一样:

代码片段Text
issue_refund(
    order_id="A1024",
    amount=899
)

这个 Tool 一旦执行,可能真的向支付系统发起退款。

这里需要区分两个阶段:

代码片段Text
模型生成 Tool Call

Tool 真正开始执行

Human-in-the-loop 介入的位置就在两者之间。

完整过程变成:

代码片段Text
用户提出退款

Agent 查询订单

Agent 判断可以退款

生成 issue_refund Tool Call

人工审核

批准 / 修改 / 拒绝

根据决定继续运行

因此,HITL 很适合放在高风险、有副作用或者需要业务责任确认的操作之前。

而像查询天气、搜索知识库之类的低风险 Tool 则不需要 HITL 的介入;如果这些也逐次要求确认,会让 Agent 的使用体验变得非常繁琐。

二、审核运行机制

在当前 LangChain Agent 中,这类 Tool 审核可以通过 HumanInTheLoopMiddleware 完成。

它属于 Agent Middleware。

如果前面已经了解过 Middleware,可以把 HITL 看成一个运行在模型输出之后的检查器:

代码片段Text
Model

生成 AIMessage

HumanInTheLoopMiddleware

检查其中的 Tool Call

是否需要人工审核
  ├─ 否 → 继续执行 Tool
  └─ 是 → 触发 Interrupt

这个执行位置很重要。

模型此时已经完成推理,并明确生成了准备调用的 Tool 和参数,例如:

代码片段Text
issue_refund
order_id = A1024
amount = 899

issue_refund 尚未真正运行。

Middleware 会根据事先配置的审核策略检查这次 Tool Call。如果匹配到需要人工处理的操作,就构造一份审核请求并触发 Interrupt。

整个生命周期概括为:

代码片段Text
Model

Tool Call

after_model Middleware

HITL 策略检查

Interrupt

Checkpoint 保存状态

等待人工决定

Command(resume=...)

恢复执行

这里的“暂停”也不同于:

代码片段Python
input("是否批准?")

input() 会占着当前 Python 进程等待用户输入。

LangGraph 的 Interrupt 会把 Graph State 交给 Checkpointer 保存。当前请求可以结束,过一段时间再根据原来的执行状态继续。

因此,一个退款申请完全可能上午产生,下午才由客服主管批准。

三、中断配置语法

真正使用 HITL 时,我们需要理解 HumanInTheLoopMiddleware 这个类:

代码片段Python
from langchain.agents.middleware import HumanInTheLoopMiddleware

它最主要的参数是:

代码片段Python
interrupt_on

它是一个字典,Key 是 Tool 名称,Value 决定这个 Tool 是否需要人工审核,以及人工可以采取哪些操作。

最简单的配置如下:

代码片段Python
HumanInTheLoopMiddleware(
    interrupt_on={
        "get_order": False,
        "get_refund_policy": False,
        "issue_refund": True,
    }
)

这里有两种基础写法。

代码片段Python
"get_order": False

表示匹配到 get_order 时不触发人工审核,可以正常继续执行。

而:

代码片段Python
"issue_refund": True

表示每次调用 issue_refund 都进入审核流程,并使用默认审核配置。

实际项目通常还需要更细的控制,此时可以给 Tool 传入一个配置对象:

代码片段Python
"issue_refund": {
    "allowed_decisions": ["approve", "edit", "reject"]
}

allowed_decisions 用来限制审核者能够采取哪些决定。

当前内置的四种决定是:

类型含义典型场景
approve按原 Tool Call 执行退款金额正确
edit修改参数后执行将退款 899 元改为 699 元
reject不执行,并把原因反馈给 Agent当前订单不允许退款
respond人工直接提供 Tool ResultAgent 主动向用户询问信息

对于退款、删除数据、付款等具有一定操作风险的 Tool,通常使用前三种。

respond 更适合这样的 Tool:

代码片段Text
ask_user("请选择退款原因")

人工输入:

代码片段Text
商品质量问题

这段内容会直接作为成功的 Tool Result 返回给 Agent。

因此,如果审核者想阻止退款,应使用 reject。使用 respond 会让 Agent 认为这个 Tool 已经成功获得了一个结果。

除了 interrupt_on,还可以配置:

代码片段Python
description_prefix

它负责设置审核提示的统一前缀,例如:

代码片段Python
HumanInTheLoopMiddleware(
    interrupt_on={...},
    description_prefix="以下操作需要人工确认",
)

对于具体 Tool,还可以通过 description 自定义审核描述。

这些描述最终可以展示在 Web、桌面客户端或企业审批页面中,告诉审核者 Agent 准备执行什么。

四、退款审核示例

下面把前面的电商客服场景实现出来。

为了让示例集中在 HITL,这里模拟一个简单的内存订单业务系统。

先定义两个 Tool:

代码片段Python
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 时,需要引入三个关键对象:

代码片段Python
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:

代码片段Python
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 仍然只是:

代码片段Python
@tool
def issue_refund(...):
    ...

审核规则放在 Middleware 中管理。这样业务 Tool 不需要同时承担“谁可以批准我”“什么时候需要确认”这类流程控制职责。

接下来调用 Agent。

HITL 需要能够识别一条暂停中的执行记录,因此调用时还要提供 thread_id

代码片段Python
config = {
    "configurable": {
        "thread_id": "refund-A1024"
    }
}

可以把 thread_id 理解为这次 Agent 会话在持久化系统中的定位标识。

然后发送用户请求:

代码片段Python
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": (
                    "订单 A1024 的商品存在质量问题,"
                    "请查询订单后为用户全额退款。"
                ),
            }
        ]
    },
    config=config,
    version="v2",
)
 
print(result.interrupts)

这里的:

代码片段Python
version="v2"

表示使用当前 invoke 的 V2 输出形式。返回结果是 GraphOutput,可以直接通过:

代码片段Python
result.interrupts

读取暂停信息。

模型生成的 Tool Call 可能略有不同。下面是根据该流程整理的典型示例输出:

代码片段Text
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 则告诉前端“这次允许审核者做什么”。

因此应用层完全可以据此生成一个审批卡片:

代码片段Text
退款申请

订单:A1024
退款金额:899 元
原因:商品质量问题

[批准] [修改] [拒绝]

这比简单弹出“是否继续执行?”更适合真正的业务系统。

五、人工决策处理

人工做出决定之后,需要把结果送回原来的 Agent 执行。

这一步使用:

代码片段Python
from langgraph.types import Command

其中最重要的是:

代码片段Python
Command(resume=...)

resume 表示:把外部提供的结果送回当前处于暂停状态的执行流程。

假设客服主管确认 899 元全额退款,可以这样恢复:

代码片段Python
result = agent.invoke(
    Command(
        resume={
            "decisions": [
                {
                    "type": "approve"
                }
            ]
        }
    ),
    config=config,
    version="v2",
)
 
print(ORDERS["A1024"])

恢复时必须继续使用前面的:

代码片段Python
thread_id="refund-A1024"

因为 Checkpointer 需要通过它找到之前保存的状态。

运行结果

代码片段Text
{
    'status': 'paid',
    'amount': 899,
    'refunded': True
}

从结果可以确认,第一次执行 Agent 时退款并没有发生。

只有收到 approve 之后,issue_refund 才真正运行。

如果客服发现应该只退款 699 元,可以使用 edit

代码片段Python
Command(
    resume={
        "decisions": [
            {
                "type": "edit",
                "edited_action": {
                    "name": "issue_refund",
                    "args": {
                        "order_id": "A1024",
                        "amount": 699,
                        "reason": "部分退款",
                    },
                },
            }
        ]
    }
)

edited_action 包含两个字段:

代码片段Text
name
args

name 是最终要执行的 Tool 名称,args 是修改后的参数。

实际使用时应尽量只做必要修改。例如审核者修改退款金额很合理,但如果把退款 Tool 改成完全不同的业务操作,模型后续可能需要重新判断整个任务。

还有一种结果是拒绝:

代码片段Python
Command(
    resume={
        "decisions": [
            {
                "type": "reject",
                "message": (
                    "该订单已经超过退款期限,"
                    "不要继续执行退款,请向用户说明原因。"
                ),
            }
        ]
    }
)

此时 issue_refund 不会运行。

message 会作为反馈提供给 Agent,所以 Agent 可以继续生成类似:

代码片段Text
该订单已经超过退款期限,目前无法直接退款……

这也是 HITL 比简单的“确认弹窗”多出的一层能力:人工决定可以继续参与 Agent 后续推理。

六、条件审核策略

真实项目里,经常会遇到一种情况:同一个 Tool,有些调用可以自动执行,有些调用需要人工确认。

例如:

代码片段Python
issue_refund(...)

如果所有退款都人工审核,客服 Agent 的自动化价值会明显下降。

业务规则可能是:

代码片段Text
退款金额 <= 100 元
    自动执行

退款金额 > 100 元
    人工审批

这时可以使用 interrupt_on 配置中的:

代码片段Python
when

when 接收一个 ToolCallRequest,根据本次 Tool Call 返回布尔值:

代码片段Text
True  → 暂停审核
False → 自动继续

先定义判断函数:

代码片段Python
from langchain.agents.middleware import ToolCallRequest
 
 
def large_refund(request: ToolCallRequest) -> bool:
    amount = request.tool_call["args"].get("amount", 0)
    return amount > 100

然后配置:

代码片段Python
HumanInTheLoopMiddleware(
    interrupt_on={
        "issue_refund": {
            "allowed_decisions": [
                "approve",
                "edit",
                "reject",
            ],
            "when": large_refund,
        }
    }
)

此时两次 Tool Call 的行为会不同:

代码片段Text
issue_refund(amount=59)

自动执行

issue_refund(amount=899)

触发 Interrupt

人工审核

when 很适合根据 Tool 参数实现风险分级,例如:

代码片段Text
退款金额 > 100 元
转账金额 > 1000 元
删除生产环境文件
修改正式数据库
向外部客户发送邮件

这样设计以后,审核策略关注的是一次操作产生的风险,而不只是 Tool 名称。

需要注意的是,条件式 when 属于较新的 HITL 配置能力,当前要求 langchain>=1.3.3

七、生产使用边界

当我们要在生产系统中使用 HITL 机制时,还需要关注几个问题。

第一个是 Checkpointer。

示例中的:

代码片段Python
InMemorySaver()

数据只存在当前进程内。进程退出以后,等待审核的状态也会丢失。

生产环境应使用持久化 Checkpointer,例如 PostgreSQL 或 MongoDB 对应的 Saver,使下面这种流程成为可能:

代码片段Text
09:00
Agent 提交 899 元退款申请

Checkpoint 持久化

09:01
Agent 请求结束

14:30
主管打开审批中心

点击批准

14:30
根据 thread_id 加载原状态

继续执行退款

第二个问题是幂等性。

如果直接使用 LangGraph 的 interrupt() 编写自定义审核流程,需要知道一个重要执行规则:恢复 Interrupt 时,包含 Interrupt 的 Node 会从 Node 开头重新执行。

因此这种代码需要特别小心:

代码片段Python
def node(state):
    create_payment_record()
 
    approved = interrupt("是否批准?")
 
    ...

恢复执行时,create_payment_record() 可能再次运行。

Interrupt 之前存在副作用的操作,应设计成可重复执行而不会产生额外结果,也就是具备幂等性。

对于普通的 Tool 审批场景,使用 HumanInTheLoopMiddleware 可以减少自己处理这些执行细节的工作。

第三个问题是权限边界。

HITL 适合控制:

代码片段Text
是否退款
是否删除
是否发送
退款多少
发送给谁

但数据库账号本身仍然应该遵守最小权限,文件 Tool 仍然应该限制可访问目录,付款接口也应该存在金额限制、鉴权和业务校验。

因此在一个完整 Agent 系统中,可以把几层控制理解为:

代码片段Text
Tool 权限

限定 Agent 能够执行什么

Guardrails

检查输入、输出和行为约束

Middleware

干预 Agent 执行过程

Human-in-the-loop

把高风险决定交给人

不同层解决的是不同问题,没有必要全部压到 HITL 上。

总结

Human-in-the-loop 解决的是 Agent 获得真实执行能力之后的控制问题。模型可以负责理解需求、查询信息、形成方案并生成 Tool Call,但涉及退款、删除、付款、发送等高风险动作时,可以先暂停执行,再由人决定是否继续。

在 LangChain 中,这条链路由 HumanInTheLoopMiddleware、Interrupt、Checkpointer、thread_idCommand(resume=...) 共同完成:Middleware 判断哪些 Tool Call 需要审核,Interrupt 暂停执行,Checkpoint 保存现场,thread_id 定位原来的运行状态,最终通过 resume 把人工决定送回 Agent。

实际项目中,不必让每个 Tool 都经过人工确认。更合适的方式是按照副作用、金额、环境和业务风险设置审核条件。这样既能保留 Agent 的自动执行能力,也能让真正需要承担责任的操作停在合适的位置。