深入学 LangChain 官方文档(十五)Human-in-the-loop 与 Guardrails
本篇对应的官方文档
- Human-in-the-loop:支撑
HumanInTheLoopMiddleware、interrupt_on、四类审批决定和暂停恢复生命周期。- Guardrails:支撑确定性与模型型 Guardrail、运行期检查位置和安全治理边界。
- LangGraph Interrupts:支撑 checkpoint、
thread_id、Command(resume=...)和节点重放规则。本篇讲解范围
本篇用客服 Agent 的退款与改地址场景,讲清高风险工具如何按策略暂停、怎样把待审动作交给审批界面、怎样提交决定并恢复同一条运行。重试、降级、PII 处理和 Custom Middleware 留给下一篇;业务身份、数据库事务和完整审计仍由应用系统负责。
客服 Agent 查到订单后,模型生成了一个参数完全合法的 refund_order(order_id="A-2048", amount=699)。schema 校验能证明订单号是字符串、金额是数字,却证明不了当前用户就是订单本人,也证明不了 699 元符合退款政策。
如果执行层看到合法 tool call 就直接退款,Structured Output 越准确,副作用反而可能越稳定地被执行。真正缺少的不是参数格式,而是模型决定与业务执行之间的一道运行期闸门。

Tool schema 负责约束名称和参数,业务授权负责确认主体、额度与政策,人工审批负责处理需要判断的高风险动作。三者处在不同层,任何一层通过都不能替代另外两层。
Human-in-the-loop(HITL,人在回路)要解决的,就是让 Agent 在真正产生副作用前停下来,把"准备做什么、允许怎样处理"交给外部审批者。
一、Guardrail 不是一句"请注意安全"
Guardrail 可以理解为 Agent 运行期的约束策略。它可以检查用户输入、模型输出、工具参数或工具结果,也可以在违反策略时修改、阻断、降级或要求人工介入。
确定性 Guardrail 使用正则、名单、额度、角色和明确业务规则,速度快、结果可预测。模型型 Guardrail 使用分类模型或 LLM 判断语义风险,能识别更隐蔽的问题,但会增加延迟、成本和不确定性。两者不是替代关系:信用卡号脱敏适合确定性规则,复杂内容风险可以再交给模型判断。
HITL 是 Guardrail 的一种实现。它不负责自动判断所有风险,而是在策略命中后把最终决定交给人。

外层 Guardrail 决定在哪些运行位置检查,确定性规则和模型判断负责识别风险;HITL 只接住其中需要人工确认的动作。把所有 Guardrail 都等同于审批,会漏掉脱敏、限额和内容过滤等自动策略。
因此,退款审批不是把"退款要谨慎"写进 system prompt。Prompt 可以影响模型选择,却不能阻止执行层运行已经生成的 tool call。真正的闸门必须位于工具执行路径上。
二、暂停点位于模型返回之后、工具执行之前
HumanInTheLoopMiddleware 会检查模型响应中的 tool calls。它通过 after_model hook 运行:模型已经提出动作,但 ToolNode 还没有执行工具。
若调用命中 interrupt_on 策略,middleware 会构造 HITL 请求并调用 interrupt()。LangGraph 把当前 State 写入 checkpointer,运行返回给调用方,等待外部决定。收到决定后,原图从保存的线程继续推进,批准的动作才会进入工具执行。

模型先产生 tool call,HITL middleware 再匹配策略;命中后保存 State 并输出 interrupt。审批决定通过同一线程返回,随后才可能执行工具并生成 ToolMessage。暂停发生在副作用之前。
这条顺序解释了 HITL 的能力边界。它能阻止一个尚未执行的退款工具,却不能撤销 middleware 运行前已经提交到外部系统的副作用。外部 API 的幂等键、事务与补偿机制仍然必须存在。
三、审批策略应按风险分级
interrupt_on 以工具名为 key。值为 False 时调用直接通过;值为 True 时使用默认审批配置;也可以提供 allowed_decisions、description 与 when,只在特定参数命中时暂停。
客服场景可以分成三层:get_order 是只读查询,正常直通;update_shipping_address 总是需要人工检查;refund_order 只有金额超过自动退款上限时才暂停。策略由真实风险决定,不由工具名称听起来是否危险决定。

只读查询走 False 直通,低额退款由 when 判定后自动通过,高额退款与改地址进入审批。分级能把人工注意力留给真实副作用,避免安全工具也被无差别阻塞。
when 接收 ToolCallRequest,可以读取本次 tool call 的参数。它适合实现稳定、可审计的条件,例如金额阈值或工作区路径;如果风险依赖用户角色,还要从可信 runtime context 读取身份,不能让模型自己在参数里声明"我是管理员"。
四、用一条代码链走通暂停与恢复
下面的示例只模拟退款执行,重点是 HumanInTheLoopMiddleware、checkpointer、thread_id 与 Command 的配合。生产环境应把 InMemorySaver 换成持久化 checkpointer,并把退款工具连接到具备鉴权、幂等和审计能力的业务服务。
python
import os
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware, ToolCallRequest
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
# 作用:模拟读取订单,只返回演示用的非敏感状态。
@tool
def get_order(order_id: str) -> str:
return f"订单 {order_id} 已支付,可申请退款。"
# 作用:模拟提交退款;真实系统必须在服务端再次鉴权并使用幂等键。
@tool
def refund_order(order_id: str, amount: float) -> str:
return f"订单 {order_id} 已提交退款 {amount:.2f} 元。"
# 作用:只让超过自动退款上限的调用进入人工审批。
def needs_refund_review(request: ToolCallRequest) -> bool:
amount = float(request.tool_call["args"].get("amount", 0))
return amount > 200
model = ChatOpenAI(
model="qwen3.7-plus",
api_key=os.environ["MODEL_API_KEY"],
base_url=os.environ["MODEL_BASE_URL"],
)
agent = create_agent(
model=model,
tools=[get_order, refund_order],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"get_order": False,
"refund_order": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": needs_refund_review,
"description": "高额退款需要客服主管审批",
},
}
)
],
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "refund-A-2048"}}
first_result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "查询订单 A-2048,并退款 699 元",
}
]
},
config=config,
version="v2",
)
for pending in first_result.interrupts:
print(pending.value["action_requests"])
print(pending.value["review_configs"])
final_result = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config,
version="v2",
)
print(final_result.value["messages"][-1].content)
第一次 invoke() 运行到高额退款时返回 interrupts,而不是继续调用 refund_order。每个 interrupt 的 value 中,action_requests 描述待执行的工具与参数,review_configs 描述审批者能够选择的决定。

action_requests 回答"Agent 想做什么",review_configs 回答"审批者能怎样处理"。审批界面应把两者按位置配对展示,而不是自行猜测某个工具允许 edit 还是只能 reject。
第二次 invoke() 传入 Command(resume=...),并复用原来的 config。这不是启动一个新的 Agent 请求,而是向已经暂停的运行补交决定。
五、四类决定有不同的执行语义
approve 保留原工具名和参数,随后执行工具。它适合审批者确认动作完全正确的情况。
edit 提供新的 edited_action,可以修改工具参数后再执行。它适合把退款金额从 699 元调整为政策允许的 499 元。修改应保持保守;如果把工具名和参数大幅改写,模型可能重新评估并产生额外调用。
reject 跳过工具,并把拒绝说明作为反馈交给 Agent。对退款、删库、发邮件等副作用动作,拒绝必须走这条分支,并明确说明是终止、询问用户还是改走安全方案。
respond 不执行工具,而是把人的文字作为成功的工具结果返回。它适合 ask_user 这类"工具本身就是向人提问"的占位能力,不适合表达拒绝。

approve 与 edit 最终进入真实工具,reject 生成拒绝反馈但不执行,respond 生成成功语义的人工回复。把 respond 用于拒绝,会让模型误以为工具已经成功完成。
决定类型不是界面按钮文案,而是会改变 Agent State 和 ToolMessage 语义的运行协议。审批系统必须保存原动作、决定人、决定内容与恢复结果,不能只记录"点了确认"。
六、恢复靠 checkpoint 和同一个 thread_id
HITL 能跨时间等待,是因为暂停时 State 已被 checkpointer 保存。thread_id 是找到这份状态的稳定指针:再次使用同一个值,运行时才能加载原 checkpoint;换成新值,只会得到一条空白线程。

首次运行把 State 写入 thread_id=refund-A-2048 对应的 checkpoint。审批决定只有携带同一 thread_id 才能回到原暂停点;新 ID 会创建新线程,无法继承待审动作。
演示中的 InMemorySaver 会随进程结束而丢失数据,不能支撑跨服务重启的审批。生产环境需要持久化 checkpointer,还要定义 thread 的所有权、过期时间、重复恢复保护和审批并发控制。
同一个 thread_id 也不等于同一个用户。服务端仍要验证当前审批者是否有权恢复这条线程,不能因为客户端知道 ID 就允许其提交决定。
七、多个动作必须逐一、按序决定
模型可能一次提出"修改地址"和"发确认短信"两个 tool calls。若两者都命中策略,interrupt 会同时包含两个 action_requests;恢复时也必须提交两个 decisions,并保持相同顺序。

待审动作 A、B 与决定 1、2 采用位置配对,而不是按按钮点击时间或工具名重新排序。缺少决定或顺序错位,都可能把审批结论应用到错误动作。
审批界面最好为每个动作展示稳定序号、工具名、关键参数、允许决定和风险说明,提交时再按原序列组装 decisions。不要把多个动作折叠成一个"全部同意",除非业务明确允许批量授权且后端仍能逐项审计。
八、底层 interrupt 会重跑节点
LangGraph 的 interrupt() 不是在 Python 栈帧中原地休眠。它通过特殊异常让运行时暂停;恢复后,所在节点会从头重新执行,再把 resume value 作为 interrupt() 的返回值。
这带来三条必须遵守的工程规则。第一,不要用宽泛 try/except Exception 包住 interrupt(),否则暂停异常可能被吞掉。第二,同一节点内多个 interrupt 的顺序必须稳定,因为 resume value 按位置匹配。第三,interrupt 之前的副作用可能再次发生,必须幂等或移动到暂停之后。

恢复时节点从头重跑:纯读取可以再次执行,外部写操作必须使用幂等键或放到审批之后;interrupt 不能被宽泛异常捕获,多个 interrupt 也不能在不同运行中改变顺序。
HumanInTheLoopMiddleware 已经封装了常见 tool-call 审批。只有需要在自定义图节点中收集额外输入或实现特殊工作流时,才应直接使用 interrupt();此时必须把节点重放当作设计前提,而不是异常情况。
九、审批不能替代业务系统
一套可用的高风险动作链至少有四层。模型与 tool schema 负责形成结构化请求;Guardrail 与 HITL 负责决定是否暂停;业务服务负责身份、额度、幂等和事务;审计系统负责记录谁在何时批准了什么。
人工点了 approve,只表示允许 Agent 继续尝试执行。外部服务仍可能因为订单状态变化、余额不足或权限过期而拒绝操作;这类结果要作为真实工具错误返回,不能把审批成功写成业务成功。
反过来,业务服务已经具备严格授权,也不代表 HITL 没有价值。授权回答"这个角色能不能做",审批回答"这一次是否应该做"。在高金额退款、生产数据修改和对外发送场景里,两者通常同时需要。
总结:让副作用在可恢复的位置等人
Human-in-the-loop 把 Agent 的一次"确认"扩展成可验证的运行协议:模型提出动作,middleware 按风险策略暂停,checkpointer 保存状态,审批者对每个动作给出合法决定,运行时再用同一 thread_id 恢复。
这条协议最重要的边界同样清楚:schema 不能替代授权,checkpoint 不能替代幂等,respond 不能替代 reject,知道 thread ID 也不能替代身份认证。
当审批链路稳定后,新的问题会出现:PII 脱敏、失败重试、模型降级、调用限额和自定义策略应该怎样组合,多个 Middleware 的先后顺序又会怎样改变行为。下一篇将进入 Built-in 与 Custom Middleware,把这些治理能力放回完整 Agent 生命周期中。