切换主题

Human-in-the-loop Agent 设计:哪些步骤必须人工审批

Easton editorial illustration: agent rollout and rollback rail

"OpenAI Agents SDK Human-in-the-loop 文档说明,工具可以声明需要 approval,运行结果会通过 interruptions 暂停,并可用 RunState approve/reject 后恢复。"

飞书消息草稿已经生成好了:标题、正文、附件链接都填完,只差一步——点发送。但你的 Agent 在 send_message 前停了下来,等你确认。

邮件内容可以自动生成,发给客户前必须展示收件人、主题、正文摘要。CMS 表单填完了,提交时被策略拦下,等待 owner approve。这些场景看起来是 UI 层面的”点确认按钮”,但背后是运行状态暂停。Agent 的 RunState 被保存下来,等你做决策后恢复执行。审批要在哪一步停下来、谁来批、拒绝或超时怎么处理,这些不是前端弹窗的事,是工具系统的安全边界。

这篇文章给出一套可执行的风险分级矩阵、审批点清单、暂停恢复机制和审计字段模板,帮你把审批从”加个确认按钮”改写为”可序列化、可恢复、可审计的系统状态”。

风险分级矩阵:决定哪些动作要审批

不是所有动作都要审批。只读操作可以自动执行,删除和付款必须停下来等人。怎么判断?用这五个维度:

维度分级标准示例动作
外部影响涉及外部系统/用户发邮件、提交表单、调用外部 API、写协作系统(飞书/Slack)
可逆性动作是否可撤销删除记录(不可逆)、写草稿(可逆)、付款(部分可逆需补偿)、发送消息(不可逆)
数据敏感度涉及的数据级别查询公开数据(公开)、修改内部记录(内部)、导出用户隐私(敏感)、读取生产配置(敏感)
金额/权限阈值涉及资金或权限变更付款、转账、退款、改权限、批量操作、删除用户数据
自动程度允许的自动级别只读查询(全自动)、写草稿(全自动)、外发消息(需确认)、删除/付款(需审批)

这套矩阵来自 OWASP LLM06 的”过度功能、过度权限、过度自主”风险分类(发布前需核验 OWASP 编号)。你可以直接套用,或者根据业务场景调整阈值。

具体来说:

外部影响:只要涉及外部系统或用户,就要警惕。邮件发出去就收不回,表单提交可能触发订单,外部 API 调用可能改别人家的数据。

可逆性:删除记录不可逆,写草稿可随时改,付款部分可逆但需要退款流程补偿。

数据敏感度:公开数据可以自由读,内部数据要限制写,敏感数据(用户隐私、生产配置)必须审批。

金额/权限阈值:任何涉及资金或权限变更的动作,都该停下来。付款、转账、退款、改权限、批量操作都是高风险点。

自动程度:读操作可以全自动,写草稿也可以自动(毕竟只是草稿),外发需要确认,删除和付款必须审批。

矩阵不是一次性配置,要根据业务场景调整。比如你的业务场景中”发送消息”风险低(内部通知),可以降到”需确认”;但”付款”风险高(涉及真实资金),必须”需审批+双人复核”。

三个真实场景的分级案例

案例1:飞书消息草稿

Agent 写飞书消息草稿:自动执行(L0)。草稿只是保存在飞书草稿箱,没有发送出去,可逆、无外部影响、不涉及敏感数据。但当 Agent 调用 send_message 发送给客户:需审批(L2)。消息一旦发送就不可逆,外部用户会收到内容,可能包含敏感信息或误导性内容。MCP tools/call 前确认就是这个场景。

案例2:邮件发送

Agent 生成邮件内容:自动执行(L0)。内容只是字符串,没有发送出去,可随时修改。但当 Agent 调用邮件 API 发送给客户:需审批展示收件人/主题/附件(L2)。审批 UI 必须展示摘要和证据,不能只显示”确认发送”按钮。

案例3:CMS 表单提交

Agent 填写表单:自动执行(L0)。表单只是填写,没有提交,数据还在本地。但当 Agent 调用 CMS API 提交表单:策略拦截需 owner approve(L2)。策略可以是 guardrail 自动拦截(比如”金额超阈值”),也可以是 policy 静态规则(比如”所有 CMS 提交都要审批”)。

案例4:生产数据库删除

Agent 查询生产数据库:自动执行(L0)。查询只是读操作,无外部影响、可逆、不修改数据。但当 Agent 调用删除 API 删除生产库:强制人工审批+backup 审计(L3)。删除生产数据库不可逆、涉及敏感数据、影响外部用户,必须 Policy 强制规则限制,不能依赖 Guardrail 自动拦截。

这四个案例说明:同一个任务的不同步骤,风险级别不同。写草稿可以自动,发送必须审批,生产库删除必须双人复核。风险分级不是一刀切,要拆到具体动作。

审批点定义清单:具体到动作类型

有了风险矩阵,现在定义审批等级。这套清单覆盖常见动作类型:

L0 自动执行:读操作(查询数据库、检索向量库、读取配置)、写草稿(保存草稿、生成预览)。这些动作没有外部影响、可逆、不涉及敏感数据,可以自动执行。

L1 需确认:外发消息/数据(发送邮件、提交表单、调用外部 API)、批量读操作(导出数据、批量查询)。这些动作有外部影响,但风险相对可控,需要确认但不需要严格审批。

L2 需审批:删除/改权限(删除记录、修改权限、批量删除)、写协作系统(飞书消息、Slack 消息、CRM 记录)。这些动作不可逆或有较高外部影响,必须停下来审批。

L3 强制审批+双人复核:付款/转账(支付订单、退款)、敏感数据操作(导出用户隐私、修改生产配置、删除生产库)。这些动作涉及资金或敏感数据,必须双人复核。

这套清单来自 OpenAI Agents SDK 的 needs_approval 和 MCP Tools 的安全建议(发布前需复核 API 字段名)。MCP spec 要求”工具调用应让用户能拒绝,敏感操作确认”,对应的就是 L2 和 L3 级别。

你可以按业务场景调整。比如:

如果你的业务场景中”发送飞书消息”风险低(内部通知),可以降到 L1 需确认。
如果”删除记录”风险高(用户数据),必须保持 L2 需审批。
如果”付款”风险极高(大额资金),可以升级到 L3 双人复核+审批理由必填。

清单不是静态的,要根据业务变化调整。比如某天业务规则变了,“发送消息”不再需要审批,可以直接从清单中移除。

审批流程状态机:如何暂停、保存状态、恢复执行

审批不是 UI 弹窗,是运行状态暂停。当工具调用需要审批时,Agent 的 RunState 被保存,等你做决策后恢复。

状态流转图

审批状态机包含以下流转:

request -> pending -> approved/rejected/timeout -> resume/abort/compensate

具体步骤:

request:工具调用触发审批请求,RunState 包含工具名、参数、上下文。
pending:等待人工决策,状态保存在 checkpoint 中,关联 thread_id。
approved:审批通过,从 checkpoint 恢复执行,调用工具。
rejected:审批拒绝,进入 abort 或 convert to draft。
timeout:审批超时,进入 escalate 或 auto-reject。
resume/abort/compensate:恢复执行、中断任务、补偿已执行步骤。

恢复方式

三种恢复方式:

approve:继续执行,调用工具,完成后继续下一步。
reject:中断或改草稿,不调用工具。
edit:修改参数后继续,比如修改邮件收件人或内容后重新审批。

checkpoint 和 thread state 是状态保存的技术背景。具体实现见站内已发布的 LangGraph checkpoint/thread state 文章。checkpoint 用于暂停时保存状态,thread state 用于恢复时回到执行点。

OpenAI Agents SDK HITL 代码示例

以下代码演示 OpenAI Agents SDK 的审批机制(API 易变,发布前需核对官方文档):

from agents import Agent, Runner, function_tool

@function_tool(needs_approval=True)
def send_email(to: str, subject: str, body: str) -> str:
    return send_email_handler(to=to, subject=subject, body=body)

agent = Agent(
    name="EmailAgent",
    tools=[send_email],
    instructions="写邮件草稿,发送前等待审批",
)

result = Runner.run_sync(agent, "给客户写封退款通知邮件")

if result.interruptions:
    state = result.to_state()

    for interruption in result.interruptions:
        print(f"待审批工具: {interruption.tool_name}")
        print(f"参数: {interruption.arguments}")

        decision = show_approval_ui(interruption)

        if decision == "approve":
            state.approve(interruption)
        elif decision == "reject":
            state.reject(interruption)

    result = Runner.run_sync(agent, state)

核心点:

needs_approval=True 标记工具需要审批。
interruptions 包含待审批的工具调用列表。
result.to_state() 把暂停结果转换为可序列化的 RunState。
state.approve()state.reject() 做决策。
Runner.run_sync(agent, state) 从暂停点继续。

注意:API 字段名可能在 2026-07 后变化,需核对官方文档。

LangGraph interrupt/resume 代码示例

以下代码演示 LangGraph 的 interrupt 和 Command resume:

from langgraph.graph import StateGraph, MessagesState
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command, interrupt

def send_email_node(state: MessagesState):
    approved = interrupt({
        "action": "send_email",
        "summary": state["email_summary"],
    })

    if approved != "approved":
        return {"messages": ["邮件发送被拒绝,已保存为草稿"]}

    email_result = send_email(state["email_params"])
    return {"messages": [email_result]}

graph = StateGraph(MessagesState)
graph.add_node("send_email", send_email_node)
graph.add_edge("draft_email", "send_email")

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

thread_id = "thread_123"
config = {"configurable": {"thread_id": thread_id}}

result = app.invoke(
    {"messages": ["给客户写退款通知"]},
    config=config,
)

# graph 暂停后,interrupt payload 会返回给调用方;展示审批 UI,等待人工决策。
decision = show_approval_ui(result["__interrupt__"])

if decision == "approve":
    app.invoke(Command(resume="approved"), config=config)
elif decision == "reject":
    app.invoke(Command(resume="rejected"), config=config)
elif decision == "edit":
    app.update_state(config, {"email_params": {"to": "[email protected]"}})
    app.invoke(Command(resume="approved"), config=config)

核心点:

interrupt() 暂停 graph。
Command(resume=...) 恢复执行。
checkpoint + thread_id 保证状态一致性。
支持 approve/reject/edit 三种恢复方式。

注意:API 可能变化,需核对 LangGraph 官方文档。

审批证据字段清单:保存什么信息,如何追溯

审批不是只做决策,还要记录决策。这套字段是审计日志的最小集:

字段说明示例
tool_name工具名 + 操作类型send_email / delete_record
tool_arguments完整参数 JSON{“to”: “[email protected]”, “subject”: “退款通知”}
invoker_id调用者身份(用户或系统)[email protected] / agent_run_abc123
request_time审批请求时间2026-06-23T09:26:10Z
approver_id审批人身份[email protected]
decision_time审批决策时间2026-06-23T09:35:12Z
decision决策结果approved / rejected / timeout_auto_reject
evidence审批证据(截图/摘要)“收件人正确,内容无敏感信息”
audit_trail_id可关联到运行日志run_abc123_step_5_tool_3

这套字段来自 MCP Tools 的审计建议和 OpenAI API 的 mcp_approval_request item(发布前需核验字段名)。审批日志是可观测性的一部分,具体实践见站内已发布的 Agent monitoring/recovery 文章。

RunState 如何序列化?OpenAI Agents SDK 可通过 result.to_state() 把暂停结果转换为 RunState,LangGraph 使用 checkpoint + thread_id。序列化后保存到数据库或日志系统,关联 audit_trail_id。

审计日志有几个关键用途:

事故追溯:某天发现数据泄露,能查到是谁在什么时候审批了哪个操作。
合规审计:企业环境需要证明”高风险操作有人审批”,日志是证据。
策略改进:统计哪些动作审批频率高、哪些总是被拒绝,调整审批策略。

字段不是死的,可以扩展。比如你的业务需要记录”审批时长”、“审批渠道”(邮件/Slack/飞书)、“是否双人复核”,都可以加进去。但最小集必须包含上述九个字段。

拒绝和超时:审批失败后怎么办

审批不是总是通过。拒绝和超时需要处理,不能让任务卡住。

拒绝后的三种路径

路径1:continue with fallback。用降级动作继续。比如发送邮件被拒绝,改成保存草稿,继续后续步骤。适合可降级的动作。

路径2:convert to draft。把动作改成草稿状态。比如 CMS 表单提交被拒绝,改成草稿保存,等待人工修改后重新提交。适合需要人工介入的场景。

路径3:abort task。中断整个任务。比如删除生产数据库被拒绝,任务必须停止,不能继续。适合不可逆高风险动作。

选择路径要看动作类型:

可逆动作:fallback 或 convert to draft。
不可逆高风险动作:abort task。
需要人工介入的场景:escalate。

超时后的两种路径

路径1:escalate to backup approver。超时后转给备用审批人。比如主审批人 30 分钟未响应,转给 on-call engineer。适合需要人工决策的场景。

路径2:auto-reject。超时后自动拒绝。比如审批超时 1 小时,自动拒绝并中断任务。适合风险较低但时间敏感的场景。

选择路径要看业务场景:

高风险动作:escalate,不能 auto-reject。
时间敏感动作:auto-reject,避免任务卡住。
一般场景:escalate,给审批人更多时间。

如何回滚已执行步骤

如果任务在审批拒绝后中断,可能需要回滚已执行步骤。比如 Agent 已经创建了订单,但付款审批被拒绝,订单需要取消。

回滚策略:

checkpoint 回滚:从 checkpoint 恢复到审批前的状态,丢弃已执行步骤。
补偿事务:调用补偿 API,比如取消订单、撤销邮件发送。
人工介入:通知人工处理,比如手动取消订单。

回滚不是总能成功。比如邮件已经发送出去,无法撤销。这种情况下只能记录审计日志,事后处理。

四层安全边界:Policy、Guardrail、Approval、Audit 如何组合

审批不是孤立的安全机制。Policy、Guardrail、Approval、Audit 四层需要组合使用,互不替代。

四层职责对照表

层级职责示例
Policy静态规则,限制工具范围”禁止删除生产库”、“付款工具只能调用沙箱环境”
Guardrail自动检查,拦截异常输入/输出输入验证(参数格式校验)、输出清理(敏感信息过滤)、金额阈值检查
Approval人工决策,高风险动作确认发送邮件前展示收件人/内容、删除记录前确认、付款审批
Audit事后追溯,记录决策和执行审批日志、工具调用日志、状态变更日志

四层互不替代:

Policy 不能替代 Guardrail:Policy 是静态规则,无法动态检查输入输出。
Guardrail 不能替代 Approval:Guardrail 是自动检查,无法处理需要人工判断的场景。
Approval 不能替代 Audit:Approval 是决策,Audit 是追溯,两者必须都存在。
Audit 不能替代前三层:Audit 是事后追溯,无法阻止风险发生。

组合使用示例:

付款场景:Policy 限制金额上限、Guardrail 检查参数格式、Approval 双人复核、Audit 记录审批日志。
发邮件场景:Policy 限制收件人范围、Guardrail 检查内容敏感度、Approval 展示摘要确认、Audit 记录发送日志。

MCP 工具安全边界

MCP(Model Context Protocol)工具调用有自己的安全边界(spec 版本 2025-06-18,发布前需核验):

tools/list 展示所有可用工具:用户可以看到 Agent 能调用哪些工具,避免隐藏风险。
tools/call 前确认:敏感操作需要用户确认,对应 Approval 层。
inputSchema 校验:参数格式校验,对应 Guardrail 层。
timeout:工具调用超时限制,避免卡住。
audit logging:工具调用日志,对应 Audit 层。

关键提醒:MCP approval 不能替代 OAuth scope 和 server-side authorization。MCP approval 是工具调用前的确认,OAuth scope 是 API 访问权限,server-side authorization 是业务逻辑权限检查。三者必须都存在。

比如飞书 MCP server OAuth 通过了,scope 包含 send_message,但这不代表每条消息都安全。MCP approval 需要在发送前确认内容,server-side authorization 需要检查收件人是否在允许范围内。

外发消息/写协作系统/批量改表的审批场景参考见计划中的飞书 MCP 调研文章。

审批 UI 设计要点

审批 UI 不是简单的”确认/拒绝”按钮,要展示足够信息让人做决策。

设计原则:

展示工具名和参数:让审批人知道 Agent 要调用什么工具、用什么参数。
展示预期影响:比如”发送邮件给 [email protected],主题是退款通知”。
展示可逆性:比如”发送后不可撤销”或”删除后可恢复”。
展示数据敏感度:比如”涉及用户隐私”或”公开数据”。
区分”取消”和”拒绝”:取消是放弃本次审批,拒绝是拒绝工具调用并记录审计日志。

核心元素:

工具名 + 操作类型
完整参数(可折叠)
预期影响摘要
可逆性提示
数据敏感度标签
审批理由输入框(可选)
确认/拒绝/取消按钮

重要提醒:UI 不能替代 server-side authorization。审批 UI 是前端展示,server-side authorization 是后端权限检查。审批人确认了,后端仍然要检查权限。

比如审批 UI 展示”删除记录 ID=123”,审批人点击确认,但后端仍然要检查记录是否属于当前用户、是否有删除权限。

HITL 不是孤立弹窗,需放进工具网关、日志和权限系统。具体架构见计划中的 MCP 生产架构文章。

OWASP LLM01/LLM06 风险映射

OWASP LLM Top 10 定义了 LLM 和 Agent 的安全风险(编号随版本变化,发布前需核验)。其中两个风险与审批直接相关:

风险编号风险描述审批对策
LLM01 Prompt Injection外部输入诱导未授权函数调用、数据泄露、外部命令执行高风险动作需人工审批,不能只靠 prompt 规则;审批 UI 展示参数和预期影响
LLM06 Excessive Agency过度功能、过度权限、过度自主是 Agent 工具系统风险限制工具范围(Policy)、限制自动级别(Approval)、审批按钮”本次会话总是允许”需设置范围限制

LLM01 说明 prompt injection 可以诱导模型绕过 prompt 规则,调用未授权工具。审批需要在高风险动作前停下来,展示参数和预期影响,让人工判断。

LLM06 说明过度自主是 Agent 工具系统风险。审批不是万能药,需要配合 Policy(限制工具范围)和 Approval(限制自动级别)。审批按钮”本次会话总是允许”必须设置范围限制,否则 prompt injection 可以诱导滥用。

NIST AI RMF Core 映射

NIST AI RMF Core 定义了 AI 系统风险管理的四阶段框架(个人/小团队可以用轻量版,不必企业级合规):

阶段审批职责示例
Govern定义角色和风险规则定义审批人角色(owner/on-call engineer)、定义审批等级(L0-L3)、定义拒绝和超时处理策略
Map识别高风险场景用风险分级矩阵识别高风险动作(删除、付款、改权限)、识别 prompt injection 场景(外部输入诱导)
Measure测量审批覆盖率、拒绝率统计高风险动作审批覆盖率、统计拒绝率和超时率、优化审批策略
Manage事故响应和恢复审批日志用于事故追溯、回滚已执行步骤、补偿事务处理

Govern 阶段定义审批规则,Map 阶段识别高风险场景,Measure 阶段测量审批效果:统计高风险动作审批覆盖率、统计拒绝率和超时率、调整审批策略。

个人/小团队可以用轻量版:定义审批等级(L0-L3)、识别高风险动作、统计拒绝率、记录审计日志。不必企业级合规流程,但最小集必须有。

结论

风险分级是审批设计的第一步。不是所有动作都要审批,只读和写草稿可以自动执行,删除和付款必须停下来等人。用五个维度判断:外部影响、可逆性、数据敏感度、金额/权限阈值、自动程度。

审批不是弹窗,是可序列化、可恢复、可审计的系统状态。RunState 被保存到 checkpoint,从暂停点恢复执行,审计日志记录决策和执行链。

四层安全边界互不替代。Policy 限制工具范围,Guardrail 自动检查输入输出,Approval 人工决策高风险动作,Audit 事后追溯。组合使用才能覆盖完整风险。

OWASP LLM01 和 LLM06 说明 prompt injection 和过度自主是 Agent 工具系统的核心风险。审批需要配合 Policy 和 Guardrail,不能孤立使用。

NIST AI RMF Core 提供风险管理框架。个人/小团队可以用轻量版:定义审批等级、识别高风险动作、统计拒绝率、记录审计日志。

下一步阅读:

LangGraph checkpoint/thread state 文章(站内已发布):审批状态保存的技术背景。
Agent monitoring/recovery 文章(站内已发布):审批日志是可观测性的一部分。
MCP 生产架构文章(计划中):HITL 不是孤立弹窗,需放进工具网关和权限系统。
飞书 MCP 调研文章(计划中):外发消息/写协作系统的审批场景参考。

设计 Agent 人工审批流程

用风险分级、运行暂停、审批证据和审计日志设计可恢复的 Agent 人工审批流程。

  1. 1

    步骤 1: 列出工具和动作

    列出 Agent 能调用的工具、外部系统和具体动作,不要只按工具名分类。
  2. 2

    步骤 2: 标注风险维度

    按外部影响、可逆性、数据敏感度、金额或权限阈值、自主程度标注每个动作的风险。
  3. 3

    步骤 3: 设置审批等级

    为每类动作设置 auto、draft、approval、strong approval 或 deny 策略。
  4. 4

    步骤 4: 保存运行状态

    在运行时保存 approval request、RunState 或 checkpoint,并关联 taskId、runId 与 traceId。
  5. 5

    步骤 5: 展示审批证据

    给审批人展示工具名、参数摘要、影响对象、可逆性、敏感度和预期后果。
  6. 6

    步骤 6: 处理 approve、reject 和 timeout

    根据决策恢复执行、降级为草稿、补偿已执行步骤、升级审批人或停止任务。
  7. 7

    步骤 7: 记录审计并回归测试

    保存审批日志和恢复结果,把拒绝、超时和补偿路径加入回归测试。

常见问题

AI Agent 哪些操作必须人工审批?
外发消息、删除或覆盖数据、付款、权限变更、敏感数据读写、批量写入、不可逆提交,以及向远程工具共享上下文的操作,都应人工审批或强审批。
只要有 guardrail,还需要人工审批吗?
需要。Guardrail 是自动检查,人工审批是高风险动作前的人工决策点;前者适合拦截格式、阈值和敏感信息,后者适合判断业务动作是否应该发生。
OAuth scope 通过后,为什么还要审批?
OAuth scope 只说明调用身份具备技术权限,不代表某个业务动作在当前上下文中应该执行。发送哪封邮件、删除哪条记录、付款给谁,仍然需要业务审批和服务端授权。
审批按钮能不能本次会话总是允许?
可以,但必须限制有效期、工具范围和对象范围,并记录审计日志。不能把一次确认扩展成全工具、永久、无条件自动放行。
Agent 被拒绝后应该怎么办?
拒绝后应进入明确分支:保存草稿、请求补充信息、换低风险方案、升级人工处理、执行补偿动作或停止任务,不能静默重试同一高风险操作。
审批记录应该保存哪些字段?
至少保存 taskId 或 runId、tool、参数摘要、影响对象、风险级别、审批人、决定、理由、时间、traceId、恢复动作和错误码。

19 分钟阅读 · 发布于: 2026年9月11日 · 修改于: 2026年9月11日

评论

使用 GitHub 账号登录后即可评论

Easton BlogEaston Blog