深入学 LangChain 官方文档(十五)Human-in-the-loop 与 Guardrails

深入学 LangChain 官方文档(十五)Human-in-the-loop 与 Guardrails

本篇对应的官方文档

  • Human-in-the-loop:支撑 HumanInTheLoopMiddlewareinterrupt_on、四类审批决定和暂停恢复生命周期。
  • Guardrails:支撑确定性与模型型 Guardrail、运行期检查位置和安全治理边界。
  • LangGraph Interrupts:支撑 checkpoint、thread_idCommand(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_decisionsdescriptionwhen,只在特定参数命中时暂停。

客服场景可以分成三层:get_order 是只读查询,正常直通;update_shipping_address 总是需要人工检查;refund_order 只有金额超过自动退款上限时才暂停。策略由真实风险决定,不由工具名称听起来是否危险决定。

只读查询走 False 直通,低额退款由 when 判定后自动通过,高额退款与改地址进入审批。分级能把人工注意力留给真实副作用,避免安全工具也被无差别阻塞。

when 接收 ToolCallRequest,可以读取本次 tool call 的参数。它适合实现稳定、可审计的条件,例如金额阈值或工作区路径;如果风险依赖用户角色,还要从可信 runtime context 读取身份,不能让模型自己在参数里声明"我是管理员"。

四、用一条代码链走通暂停与恢复

下面的示例只模拟退款执行,重点是 HumanInTheLoopMiddleware、checkpointer、thread_idCommand 的配合。生产环境应把 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 这类"工具本身就是向人提问"的占位能力,不适合表达拒绝。

approveedit 最终进入真实工具,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 生命周期中。

相关推荐
中微极客2 小时前
2026年生产级RAG技术栈选型:LangChain+Cohere Rerank实战
人工智能·langchain
只一3 小时前
拿捏大模型输出随机性:Temperature、Top-K 原理 + LangChain 工程落地实战
javascript·langchain
GuWen_yue4 小时前
Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%
javascript·react.js·langchain
玉宇夕落4 小时前
LangChain 工作流中的温度、Top-K、Top-P
langchain
Esaka_Forever4 小时前
LLM 大语言模型 vs Agent 智能体:核心区别 + LangChain 学习必要性
langchain
bonechips4 小时前
LLM 的"严谨"与"胡说":temperature、Top K + LangChain 工作流
langchain·llm
元直数字电路验证4 小时前
深入理解 AI Agent:从模型能力到生产级系统的完整路线图
人工智能·langchain·aigc·agent·智能体
白执落5 小时前
使用 Python + LangChain + Vue3 构建 LLM 聊天应用
人工智能·python·langchain
陳陈陳14 小时前
从“胡说八道”到“妙笔生花”:我用LangChain手搓了一个可控AI写作流(Temperature+TopK调参指南)
langchain·llm