Agent 工程体系 · 第 21/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agent 人工介入:确认、澄清、升级、接管、恢复和责任边界

Agent 并不是“模型自动调用工具”的同义词。只要 Agent 能够修改数据、发送消息、执行命令、提交审批、转移资金或影响第三方,就必须回答一个更基础的问题:

在什么条件下,Agent 可以继续;在什么条件下,必须询问;在什么条件下,必须暂停并交给人;如果人没有及时响应,系统如何恢复;最终谁对决策、执行和结果负责?

人工介入不是一个按钮,而是一组不同语义的控制机制:确认、澄清、升级、接管、恢复。它们分别解决不同类型的不确定性和风险。把它们都实现成“弹窗让用户点一下”,会导致授权范围不清、旧请求重放、状态丢失、人工审批失效,以及事故后无法判断责任归属。

本文使用以下抽象:

  • Agent:能够感知上下文、进行规划、调用工具并维护任务状态的软件系统。
  • 工具调用:Agent 请求外部系统执行动作,例如 send_emailrefund_orderrun_sql
  • 副作用:改变外部世界的结果,例如写数据库、发消息、扣款或删除文件。
  • 人工介入:人或代表组织政策的人工审批系统,对 Agent 的下一步行为施加控制。
  • 人工介入点:流程中必须等待人工决策、补充信息、接管操作或确认恢复的位置。

OpenAI 的 Agents SDK 将 Agent 描述为能够规划、调用工具、协作并维护足够状态以完成多步工作的应用;其文档也将自动防护与人工审查区分为两种机制:前者验证输入、输出或工具行为,后者在敏感动作前暂停运行,等待批准或拒绝。(developers.openai.com)


一、先区分六种人工介入语义

1. 确认:用户已经给出意图,只差授权执行

确认是对一个已经明确的动作进行最后授权。

例如:

“已准备向供应商付款 38,500 元,收款账户尾号 7621,预计到账时间为今天 18:00。是否提交?”

这里用户的目标、对象、参数和动作都已经确定。系统需要的不是更多信息,而是确认用户是否允许执行。

确认的逻辑条件可以写成:

Confirmable(a)=WellDefined(a)Authorized(u,a)Fresh(a)Reviewable(a)\operatorname{Confirmable}(a) = \operatorname{WellDefined}(a) \land \operatorname{Authorized}(u,a) \land \operatorname{Fresh}(a) \land \operatorname{Reviewable}(a)

其中:

  • aa 是待执行动作;
  • WellDefined 表示动作参数完整、类型正确;
  • Authorized 表示当前用户有权限执行该动作;
  • Fresh 表示动作基于当前状态,没有过期或被新状态替代;
  • Reviewable 表示系统能向用户展示足以理解后果的信息。

如果参数还不完整,就不能进入确认,而应进入澄清。如果动作本身超出用户权限,则不能通过确认绕过权限检查。

2. 澄清:目标可能明确,但执行所需信息不足

澄清解决的是信息缺失或语义歧义,不是授权问题。

例如用户说:

“把上个月的订单都退款。”

Agent 至少需要澄清:

  • “上个月”按哪个时区计算;
  • “订单”是全部订单还是某个店铺的订单;
  • 退款是全额还是按商品;
  • 是否包含已经发货或已开票订单;
  • 退款是否需要人工财务审核。

如果有多个合理解释,Agent 不应擅自选择一个。可以把候选解释表示成集合:

I={i1,i2,,in}I = \{i_1, i_2, \dots, i_n\}

当:

I合理>1|I_{\text{合理}}| > 1

且不同解释会产生不同副作用时,必须澄清。这里的关键不是模型“有没有信心”,而是错误解释的代价是否可接受

反例是:

用户说“删除测试数据”,Agent 根据最近一次操作猜测要删除 test 数据库。

即使模型对猜测有较高置信度,也不应直接执行,因为“删除哪个环境、哪些表、哪些记录”仍然没有明确,且动作不可逆。

3. 升级:系统无法在当前权限、证据或策略下安全决策

升级是将任务交给更高权限、更专业或更有责任资格的处理者。

常见升级原因包括:

  • 证据不足,无法满足决策阈值;
  • 涉及财务、医疗、法律、人事等高风险领域;
  • 当前操作员没有权限;
  • 政策要求双人审批或特定角色审批;
  • 发生异常,自动回退策略也不适用;
  • 多个系统状态冲突,Agent 无法判断哪个是真实状态。

升级和澄清的区别是:

  • 澄清:向当前用户询问缺失信息;
  • 升级:当前 Agent 或当前用户不具备继续决策的资格。

例如:

“订单金额超过普通客服授权上限,需要升级至财务主管审批。”

这不是“请问是否继续”,而是权限边界已经决定了当前角色不能继续。

4. 接管:人直接成为操作主体

接管不是“人批准 Agent 继续”,而是人接替 Agent 负责后续操作。

适合接管的场景:

  • 需要在复杂界面中观察多个动态信号;
  • 失败后没有可靠的自动恢复路径;
  • Agent 已经执行了部分步骤,但下一步需要专家判断;
  • 需要与第三方人工沟通;
  • 自动化操作可能造成更大损害。

例如,Agent 在云平台创建实例时发现配额异常:

  1. Agent 已提交创建请求;
  2. 平台返回部分资源已分配,但网络配置失败;
  3. 自动补偿可能重复创建资源;
  4. 此时应将控制权交给运维人员,而不是再次调用创建工具。

接管意味着系统必须明确:

  • 当前谁拥有控制权;
  • Agent 是否仍可运行后台步骤;
  • 人的操作是否会被 Agent 并发覆盖;
  • 人交还控制权时,Agent 应从哪个状态继续。

5. 恢复:暂停、失败或接管后重新建立可继续状态

恢复是从中断状态重新进入可验证的执行状态。

恢复不是简单地“重新调用上一次工具”。因为上一次调用可能已经成功,只是响应丢失;也可能只完成了一部分;还可能被人工修改过。

恢复前必须先判断:

Recoverable(s)=Durable(s)Consistent(s)ReplaySafe(s)\operatorname{Recoverable}(s) = \operatorname{Durable}(s) \land \operatorname{Consistent}(s) \land \operatorname{ReplaySafe}(s)

其中:

  • Durable:状态已经持久化;
  • Consistent:状态与外部系统没有明显冲突;
  • ReplaySafe:重新执行不会造成重复副作用。

如果不能证明 ReplaySafe,应先查询外部系统状态,而不是直接重试。

6. 责任边界:谁对哪一层负责

责任边界不是一句“用户确认后由用户负责”。Agent 系统通常至少包含四层责任:

层次 负责内容 典型主体
意图层 用户是否提出了这个目标 用户或业务发起人
决策层 是否选择了正确动作和参数 Agent、业务规则、审批人
执行层 是否按批准参数正确调用工具 Agent 运行时、工具服务
结果层 外部系统最终产生了什么结果 工具提供方、业务系统、运营主体

用户点击确认,只能证明“某个身份在某个时间对某个版本的动作表示同意”。它不能自动证明:

  • 参数一定正确;
  • Agent 没有篡改参数;
  • 执行时外部状态仍然有效;
  • 工具服务没有重复执行;
  • 业务政策允许该动作。

二、人工介入点应由风险和不确定性共同决定

不能只按照工具名称决定是否需要人工介入。update_customer 可能只是修改备注,也可能修改收款账户;send_message 可能发送给自己,也可能发送给数万名客户。

可以将一个动作的介入风险表示为:

R(a)=I(a)×P(a)×U(a)R(a) = I(a) \times P(a) \times U(a)

其中:

  • I(a)I(a):影响范围,例如金额、用户数量、数据敏感度;
  • P(a)P(a):错误发生的可能性;
  • U(a)U(a):不可逆程度。

例如:

动作 影响 II 可能性 PP 不可逆程度 UU 处理
查询订单 通常自动
修改草稿 可自动,保留撤销
发送单封邮件 参数预览后确认
批量退款 人工审批
删除生产数据 极高 极高 禁止 Agent 直接执行或必须接管

但是风险模型不能替代硬规则。以下条件应直接阻断自动执行:

Block(a)=PolicyDenied(a)PermissionMissing(a)EvidenceInsufficient(a)\operatorname{Block}(a) = \operatorname{PolicyDenied}(a) \lor \operatorname{PermissionMissing}(a) \lor \operatorname{EvidenceInsufficient}(a)

例如,即使预计金额很小,只要工具是“永久删除个人数据”,也可能被组织策略禁止自动执行。


三、确认必须确认“动作快照”,而不是确认一句自然语言

1. 参数预览包含什么

一个可审计的确认对象应至少包含:

{
  "action": "refund_order",
  "resource": {
    "order_id": "ORD-20260901-1042",
    "customer": "张某",
    "amount": "128.00",
    "currency": "CNY"
  },
  "effect": "原路退回 128.00 元,提交后不可撤销",
  "reason": "商品缺货",
  "preconditions": {
    "order_status": "PAID",
    "refund_remaining": "128.00"
  },
  "expires_at": "2026-09-01T10:15:00+08:00"
}

预览的目的不是让用户阅读内部 JSON,而是让用户看到足以改变决策的事实:

  1. 对谁操作;
  2. 操作什么;
  3. 数量、金额、范围是什么;
  4. 会产生什么后果;
  5. 是否可撤销;
  6. 哪些条件必须仍然成立;
  7. 确认何时失效。

“确认退款吗?”不是充分预览,因为用户无法判断退款对象、金额和状态。

2. 确认令牌绑定动作内容

确认令牌不能只绑定一个任务 ID。否则 Agent 可以先生成一个安全动作,用户确认后再替换成危险参数。

设规范化后的动作为:

D=Canonicalize(tenant,actor,tool,arguments,preconditions,policy_version)D = \operatorname{Canonicalize}( \text{tenant}, \text{actor}, \text{tool}, \text{arguments}, \text{preconditions}, \text{policy\_version} )

生成动作摘要:

h=SHA256(D)h = \operatorname{SHA256}(D)

确认令牌至少应绑定:

{
  "approval_id": "APR-7f1b",
  "action_hash": "sha256:...",
  "actor_id": "user-123",
  "approver_id": "user-123",
  "scope": "refund_order",
  "issued_at": "2026-09-01T10:00:00+08:00",
  "expires_at": "2026-09-01T10:15:00+08:00",
  "nonce": "random-128-bit-value",
  "state": "PENDING"
}

执行时重新计算摘要:

def verify_approval(approval, action, now):
    expected_hash = sha256(canonicalize(action))

    if approval["state"] != "APPROVED":
        raise PermissionError("approval is not approved")

    if approval["action_hash"] != expected_hash:
        raise PermissionError("action changed after approval")

    if now >= parse_time(approval["expires_at"]):
        raise PermissionError("approval expired")

    if approval["consumed_at"] is not None:
        raise PermissionError("approval already consumed")

    return True

每一步都不可省略:

  • 状态检查防止直接伪造“已批准”;
  • 摘要检查防止参数替换;
  • 过期检查防止旧状态下的授权继续有效;
  • 消费检查防止同一令牌重复使用。

3. 防重放不能只依赖前端按钮禁用

前端禁用按钮只能减少误点击,不能防止:

  • 请求包被复制后再次发送;
  • Worker 超时后重试;
  • 用户打开两个浏览器标签页;
  • 消息队列重复投递;
  • Agent 崩溃后恢复时再次提交。

服务端应使用原子状态变更:

UPDATE approvals
SET state = 'CONSUMED',
    consumed_at = CURRENT_TIMESTAMP
WHERE approval_id = ?
  AND state = 'APPROVED'
  AND expires_at > CURRENT_TIMESTAMP
  AND action_hash = ?;

只有返回影响行数为 1 时,才允许继续执行工具调用。返回 0 表示令牌已过期、已消费、摘要不匹配或不存在。

但这还不够。数据库更新成功后,进程可能在调用外部工具前崩溃,导致审批被消费但动作未执行;也可能工具执行成功后进程崩溃,导致系统误以为未执行。

因此生产实现需要额外的执行记录和幂等键:

idempotency_key = approval_id + ":" + action_hash

工具服务必须保证同一个幂等键最多产生一个业务副作用,或者至少返回同一个执行结果。


四、确认、执行和外部状态之间存在竞态

确认时看到的状态不一定等于执行时的状态。

例如:

  1. 10:00,订单状态为 PAID
  2. Agent 生成退款预览;
  3. 用户在 10:05 点击确认;
  4. 10:04,另一个系统已经完成退款;
  5. Agent 在 10:05 再次调用退款接口。

如果只校验确认令牌,仍然可能造成重复退款。确认令牌必须绑定业务前置条件,执行时重新校验:

预览阶段:
  order.status = PAID
  order.refund_remaining = 128.00
  order.version = 42

执行阶段:
  只有 order.version = 42 且 refund_remaining >= 128.00 才允许退款

对应的条件是:

Execute(a)ApprovalValid(a)PreconditionStillTrue(a)\operatorname{Execute}(a) \Rightarrow \operatorname{ApprovalValid}(a) \land \operatorname{PreconditionStillTrue}(a)

如果版本从 42 变成 43,系统应返回:

{
  "status": "STALE_APPROVAL",
  "message": "订单状态已变化,请重新获取当前状态并确认"
}

这里不能自动沿用旧确认。重新计算金额或对象后,动作已经不是原来被批准的动作。


五、Agent 不确定时,不要用“置信度”替代证据

模型输出的概率或自评置信度不能直接等同于业务可信度。工程上更有用的是把不确定性拆成三类:

1. 事实不确定

系统不知道事实是什么。

例:

“客户是否已经签收?”

如果物流系统返回超时,Agent 不能根据用户语气推断“应该已经签收”。正确处理是查询、等待、升级或拒答。

2. 意图不确定

用户表达可以对应多个动作。

例:

“把这个账户关掉。”

可能是关闭登录、注销服务、解绑银行卡或删除数据。此时应澄清,而不是根据上下文猜测。

3. 后果不确定

动作本身明确,但影响无法可靠估计。

例:

“批量调整所有客户的信用额度。”

即使对象和参数清楚,如果系统不能计算影响范围、上限和合规条件,就应升级。

可以定义一个最小证据条件:

Proceed(a)    E(a)τrisk\operatorname{Proceed}(a) \iff E(a) \geq \tau_{\text{risk}}

其中:

  • E(a)E(a) 是动作所需证据的完整度;
  • τrisk\tau_{\text{risk}} 是由风险等级决定的阈值。

低风险查询可能只需一个数据源;高风险退款可能需要订单状态、支付状态、退款余额和权限记录同时满足。

拒答、回退与升级的区别

  • 拒答:系统明确不能做,例如政策禁止、权限不足、缺少必要证据;
  • 回退:改用风险更低的替代动作,例如只生成草稿、不直接发送;
  • 升级:让有资格的人工角色继续处理。

例如:

“我无法确认该账号是否属于当前租户,因此不能删除。可以先生成待审核的删除清单,或升级给租户管理员核验。”

这句话同时完成了拒答和回退,但没有假装任务已经完成。


六、一个完整的人工介入状态机

将人工介入建模为状态机,比在代码中到处写 if need_human 更可靠。

stateDiagram-v2
    [*] --> PLANNING
    PLANNING --> NEED_CLARIFICATION: 信息不足或存在歧义
    PLANNING --> NEED_ESCALATION: 超权限/高风险/证据不足
    PLANNING --> READY: 动作明确且无需人工授权
    PLANNING --> PENDING_APPROVAL: 需要确认

    NEED_CLARIFICATION --> PLANNING: 收到补充信息
    NEED_CLARIFICATION --> CANCELLED: 用户放弃或超时

    PENDING_APPROVAL --> APPROVED: 获得有效批准
    PENDING_APPROVAL --> REJECTED: 用户拒绝
    PENDING_APPROVAL --> EXPIRED: 超过有效期
    PENDING_APPROVAL --> CANCELLED: 任务取消

    APPROVED --> EXECUTING: 原子消费令牌
    EXECUTING --> SUCCEEDED: 工具确认成功
    EXECUTING --> UNKNOWN_RESULT: 请求超时/连接中断
    EXECUTING --> FAILED: 明确失败

    NEED_ESCALATION --> HUMAN_TAKEOVER: 人工接管
    HUMAN_TAKEOVER --> HUMAN_RESOLVED: 人工完成或给出决策
    HUMAN_TAKEOVER --> RECOVERY_REQUIRED: 人工操作中断

    UNKNOWN_RESULT --> RECONCILING: 查询外部执行状态
    RECONCILING --> SUCCEEDED: 确认已执行
    RECONCILING --> RECOVERY_REQUIRED: 确认未执行
    RECONCILING --> HUMAN_TAKEOVER: 状态无法确定

    FAILED --> RECOVERY_REQUIRED: 存在安全补偿路径
    FAILED --> HUMAN_TAKEOVER: 无可靠补偿路径

    RECOVERY_REQUIRED --> READY: 重新规划为安全动作
    RECOVERY_REQUIRED --> PENDING_APPROVAL: 新动作需要重新确认
    RECOVERY_REQUIRED --> HUMAN_TAKEOVER: 需要人工判断

    REJECTED --> [*]
    CANCELLED --> [*]
    EXPIRED --> [*]
    SUCCEEDED --> [*]
    HUMAN_RESOLVED --> [*]

关键约束如下:

  1. APPROVED 不是 EXECUTING,批准和执行必须分开;
  2. UNKNOWN_RESULT 不能直接进入重试;
  3. RECOVERY_REQUIRED 不能复用旧动作摘要;
  4. 人工接管后,Agent 必须停止可能冲突的自动执行;
  5. 任何状态转换都应写入持久化事件,而不是只存在进程内存。

七、人工接管时,必须解决控制权并发

接管最容易出现“人和 Agent 同时操作”。

例如:

  1. Agent 正在编辑客户资料;
  2. 客服打开页面准备手动修改;
  3. Agent 后台根据旧上下文再次保存;
  4. 客服的修改被覆盖。

因此任务需要一个控制权租约:

{
  "task_id": "TASK-1001",
  "controller": "human:user-88",
  "lease_id": "LEASE-a91c",
  "lease_expires_at": "2026-09-01T10:30:00+08:00",
  "agent_paused": true
}

工具写入时检查:

写操作允许,当且仅当:
  lease_id 有效
  当前控制者仍为调用方
  数据版本未发生冲突

如果人工接管,Agent 的后台循环应收到取消信号:

async def agent_loop(task):
    while True:
        if await task.is_human_controlled():
            await task.wait_for_human_event()
            continue

        decision = await plan_next_step(task)

        if decision.requires_approval:
            await task.pause_for_approval(decision)
            continue

        await execute_with_idempotency(decision)

wait_for_human_event() 不是忙等。任务应被持久化并释放计算资源,由事件、队列或 webhook 唤醒。

人交还控制权时,不能简单地把 agent_paused 改为 false。Agent 应重新读取:

  • 人工实际修改了什么;
  • 外部资源当前版本;
  • 哪些原计划步骤已经完成;
  • 原计划是否仍然满足策略;
  • 是否需要重新确认。

八、恢复的核心是对账,而不是重试

1. 明确失败

工具返回业务失败,例如:

{
  "status": 409,
  "code": "INSUFFICIENT_BALANCE",
  "message": "可退款余额不足"
}

这类结果通常可以直接进入重新规划:

原动作:退款 128 元
当前事实:可退款余额为 0 元
安全回退:生成退款失败说明,升级财务处理

2. 明确成功

工具返回业务成功,并有外部唯一 ID:

{
  "status": "SUCCEEDED",
  "refund_id": "RF-8821"
}

系统可以将任务标记为成功,并把外部 ID 写入审计记录。

3. 未知结果

请求超时不等于执行失败。

客户端 -> 退款服务:请求
退款服务:已扣款并提交退款
网络:响应丢失
客户端:超时

此时系统状态是:

Slocal=UNKNOWNS_{\text{local}} = \text{UNKNOWN}

不能把它改成 FAILED 后自动重试。正确流程是:

  1. 使用原幂等键查询执行记录;
  2. 查询业务对象当前状态;
  3. 检查是否存在外部交易 ID;
  4. 如果确认成功,补写本地状态;
  5. 如果确认未执行,才允许重新提交;
  6. 如果外部状态仍无法确定,升级人工。
async def reconcile(operation):
    result = await provider.query_by_idempotency_key(
        operation.idempotency_key
    )

    if result.status == "SUCCEEDED":
        await mark_succeeded(operation, result.external_id)
    elif result.status == "NOT_FOUND":
        await mark_retryable(operation)
    else:
        await escalate(operation, reason="external state unknown")

“重试三次”只能解决明确的瞬时失败,不能解决未知结果。对有副作用的操作,重试策略必须由幂等性和对账能力共同决定。


九、人工审批不是把模型的责任转嫁给用户

1. 人工看到什么,决定了审批是否有效

如果审批界面只显示:

Agent 请求调用 refund_order

那么人工并不知道:

  • 退款哪个订单;
  • 金额是多少;
  • 原因是什么;
  • 数据是否已经变化;
  • 是否会触发其他流程。

这类审批形式上有人点击,实质上没有完成有效复核。

有效审批应绑定:

批准人身份
+ 批准时间
+ 动作摘要
+ 参数快照
+ 前置条件
+ 策略版本
+ 过期时间
+ 执行结果

2. 审批人的责任是判断,不是验证所有底层事实

审批人通常负责:

  • 判断动作是否符合业务目的;
  • 判断风险是否在授权范围内;
  • 判断展示出的对象和参数是否正确;
  • 在需要时要求补充证据。

审批人不应被默认为负责:

  • 验证 Agent 内部每一步推理;
  • 证明底层数据库没有 bug;
  • 证明第三方接口一定不会重复执行;
  • 替系统承担权限控制和幂等控制。

如果系统把未经验证的模型生成内容直接显示为事实,审批流程本身就有缺陷。

3. 工具服务仍然必须独立校验

即使 Agent 已获得批准,工具服务也应再次检查:

租户边界
用户权限
参数约束
资源当前状态
金额上限
幂等键
数据版本
审计要求

人工审批是业务授权的一部分,不应成为绕过服务端安全校验的特殊通道。


十、一个最小的端到端实现模型

下面给出与具体 Agent SDK 无关的 Python 示例,展示“规划—预览—确认—消费—执行”的核心生命周期。它使用内存字典,仅用于理解;生产环境应替换为事务数据库和具备幂等能力的工具服务。

from dataclasses import dataclass
from hashlib import sha256
from datetime import datetime, timedelta, timezone
import json
import secrets

UTC = timezone.utc


def canonicalize(action: dict) -> str:
    return json.dumps(action, sort_keys=True, separators=(",", ":"))


def action_hash(action: dict) -> str:
    return sha256(canonicalize(action).encode()).hexdigest()


@dataclass
class Approval:
    approval_id: str
    action_hash: str
    actor_id: str
    expires_at: datetime
    state: str = "PENDING"
    consumed_at: datetime | None = None


approvals = {}


def create_approval(actor_id: str, action: dict) -> Approval:
    approval = Approval(
        approval_id="APR-" + secrets.token_hex(8),
        action_hash=action_hash(action),
        actor_id=actor_id,
        expires_at=datetime.now(UTC) + timedelta(minutes=15),
    )
    approvals[approval.approval_id] = approval
    return approval


def approve(actor_id: str, approval_id: str):
    approval = approvals[approval_id]

    if approval.actor_id != actor_id:
        raise PermissionError("wrong approver")

    if datetime.now(UTC) >= approval.expires_at:
        approval.state = "EXPIRED"
        raise PermissionError("approval expired")

    if approval.state != "PENDING":
        raise PermissionError("approval is not pending")

    approval.state = "APPROVED"


def consume_approval(approval_id: str, action: dict):
    approval = approvals[approval_id]

    if approval.state != "APPROVED":
        raise PermissionError("approval is not approved")

    if datetime.now(UTC) >= approval.expires_at:
        approval.state = "EXPIRED"
        raise PermissionError("approval expired")

    if approval.action_hash != action_hash(action):
        raise PermissionError("action changed")

    if approval.consumed_at is not None:
        raise PermissionError("replay detected")

    # 生产环境必须是数据库中的原子 UPDATE,而不是普通赋值
    approval.consumed_at = datetime.now(UTC)
    approval.state = "CONSUMED"


def execute_refund(action: dict, idempotency_key: str):
    # 生产环境由支付服务保证同一 key 不产生重复退款
    print({
        "tool": "refund_order",
        "order_id": action["order_id"],
        "amount": action["amount"],
        "idempotency_key": idempotency_key,
        "status": "SUCCEEDED",
    })


action = {
    "tool": "refund_order",
    "tenant_id": "tenant-a",
    "order_id": "ORD-100",
    "amount": "128.00",
    "currency": "CNY",
    "precondition": {
        "order_status": "PAID",
        "order_version": 42,
    },
}

approval = create_approval("user-123", action)

# 用户界面应展示 action 的可读摘要,而不是只展示 approval_id
print("请确认:", action)
approve("user-123", approval.approval_id)

consume_approval(approval.approval_id, action)

idempotency_key = approval.approval_id + ":" + action_hash(action)
execute_refund(action, idempotency_key)

这个示例有几个重要边界:

  • approval_id 不是动作本身,必须同时校验 action_hash
  • approve() 只表示授权,不执行退款;
  • consume_approval() 只负责一次性消费授权,不应承担业务执行;
  • execute_refund() 仍需由下游服务实现幂等;
  • 示例中的普通字典操作没有并发安全,不能直接用于生产;
  • 实际系统还必须在执行前重新查询 order_version 和退款余额。

如果用户在审批期间修改了退款金额,应创建新的动作摘要和新的审批记录,而不是更新原记录。审批记录一旦产生,通常应视为不可变事件。


十一、常见错误及其失败表现

错误一:所有不确定性都问用户

表现是 Agent 不断询问:

“要不要继续?”
“这样可以吗?”
“是否确认?”

问题在于用户被迫替系统做类型校验、权限检查和策略判断。低风险、可验证的决策应由系统自动处理;只有涉及用户意图、业务偏好或责任授权的问题才应询问用户。

错误二:只在 UI 层做确认

表现是用户点击“确认”后,服务端直接执行请求参数,而不是执行审批时展示的参数。

后果是攻击者或错误代码可以替换请求体。确认必须在服务端与动作摘要绑定。

错误三:把超时当成失败

表现是支付、退款、发信等操作在超时后自动重试,最终出现重复副作用。

修复方式是引入 UNKNOWN_RESULT 状态和对账流程。

错误四:人工接管后 Agent 仍在运行

表现是人工刚修正数据,Agent 又根据旧上下文覆盖修改;或者人工已经取消订单,Agent 仍继续发货。

接管必须是控制权转移,同时暂停冲突的自动步骤。

错误五:审批通过后不重新检查前置条件

表现是用户批准时订单有效,执行时订单已经退款、过期或转移到另一个租户。

批准确认的是某个动作在某组前置条件下的授权,不是永久授权。执行时必须重新校验状态。

错误六:把“有人工点击”当成责任闭环

表现是系统没有记录批准人、动作版本、工具响应和最终外部结果,事故后只能看到“某人点过确认”。

责任闭环需要完整事件链:

用户请求
→ Agent 计划
→ 证据与策略检查
→ 参数预览
→ 批准/拒绝
→ 动作摘要
→ 令牌消费
→ 工具请求
→ 外部结果
→ 对账或补偿
→ 最终通知

十二、生产诊断应围绕“状态事实”而不是聊天记录

当人工介入流程出错时,优先查询以下字段:

task_id
approval_id
action_hash
policy_version
actor_id
approver_id
controller
task_state
tool_call_id
idempotency_key
external_operation_id
resource_version
created_at
approved_at
consumed_at
completed_at

诊断顺序应是:

  1. 确认任务状态:系统认为任务处于什么状态;
  2. 确认动作版本:审批时的摘要是否等于执行时摘要;
  3. 确认令牌生命周期:是否过期、重复消费或被撤销;
  4. 确认控制权:Agent 还是人工拥有任务租约;
  5. 确认外部结果:工具是否已经产生副作用;
  6. 确认补偿路径:是否可以安全撤销、对账或升级;
  7. 确认通知结果:用户看到的是计划、批准、执行中还是最终成功。

日志中不要只记录自然语言提示。自然语言适合用户交互,不适合作为一致性判断依据。状态、摘要、版本号和外部 ID 才是恢复和审计所需要的事实。


十三、把人工介入设计成协议,而不是提示语

一个成熟的人工介入协议至少应规定:

确认协议

Agent 生成动作快照
→ 服务端规范化并计算摘要
→ 展示参数、影响和有效期
→ 合格身份批准
→ 执行前重新校验摘要和前置条件
→ 原子消费批准
→ 使用幂等键调用工具

澄清协议

识别缺失字段或多个合理解释
→ 只询问决定性问题
→ 保存原问题和用户回答
→ 重新规划
→ 如果动作变化,重新生成预览

升级协议

记录升级原因
→ 选择具备相应权限或专业能力的角色
→ 附带证据、候选动作和已完成步骤
→ 等待人工决策或接管
→ 记录人工结论

接管协议

暂停冲突的 Agent 步骤
→ 创建人工控制租约
→ 展示当前状态和未完成动作
→ 人工操作
→ 记录人工修改
→ 重新对账
→ 决定恢复 Agent、继续人工或结束任务

恢复协议

读取持久化状态
→ 查询外部系统
→ 区分成功、失败和未知结果
→ 只有在可证明幂等时才重试
→ 生成新的动作或新的审批
→ 继续、补偿或升级

这些协议的实现方式可以不同:工作流引擎、消息队列、数据库状态机、Agent SDK 的暂停与恢复机制都可以承载它们。OpenAI 的相关文档把“人审”定位为在敏感工具调用前暂停运行,并提供结果与状态、可恢复状态等运行时概念;具体应用仍需自行定义业务审批、权限、幂等和责任规则。(developers.openai.com)

Anthropic 关于 Agent 构建的资料也强调,应根据任务选择简单工作流或更具自主性的 Agent,并通过工具、环境和流程约束控制复杂度;这意味着人工介入点应放在确实需要判断、授权或承担责任的位置,而不是用审批掩盖一个不可控的自动流程。(anthropic.com)

最终,人工介入的目标不是让人替 Agent 完成所有工作,而是让系统在关键边界上做到四件事:

  1. Agent 不清楚时,能明确停下来;
  2. 用户要承担授权时,能看到准确动作;
  3. 自动化失去控制时,能安全交给人;
  4. 任务中断或结果不明时,能依据事实恢复。

只有当确认绑定动作、澄清改变规划、升级改变责任层级、接管转移控制权、恢复重新建立状态一致性时,Human-in-the-loop 才不是流程装饰,而是 Agent 工程体系中的真实安全边界。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。