更新于

Agent 人工审批队列的实现


让 Agent 在“高风险操作前询问用户”听起来只是一条 prompt,工程上却是一套分布式工作流。模型生成审批请求后,用户可能几小时后才处理;期间权限、价格、库存和目标对象都可能改变。审批者还可能重复点击,多个操作员可能同时领取,同一个 Agent 运行也可能因超时重试。

因此人工审批不能实现成一个阻塞的 await user.confirm(),更不能只把“已同意”写进聊天记录。可靠方案应把审批变成持久化资源:拥有明确状态、不可变的操作摘要、可验证的权限、幂等执行键和完整审计轨迹。

一、先定义哪些动作必须进入队列

不是所有工具调用都需要人工审批。过度审批会让操作员形成“无脑点通过”的习惯,反而降低安全性。可以从四个维度评分:影响是否可逆、涉及金额或敏感数据、目标范围是否广、执行是否会通知外部人员。

风险级别示例默认处理
读取公开文档、运行只读测试自动执行并记录
修改草稿、创建可撤销任务按策略抽样或批量审批
发邮件、改生产配置、产生费用单次明确审批
极高删除数据、批量转账、发布到生产双人复核或专用流程

策略判断应在确定性代码中完成,而不是让模型自己决定“这次风险不高”。模型可以提供动作意图和参数,策略引擎根据工具、资源、环境、金额、用户角色与组织规则生成审批要求。关于更完整的分级思路,可参考AI Agent 权限设计实战;本文只展开队列实现。

二、审批记录必须冻结意图与范围

审批对象不能只保存一段自然语言“允许 Agent 操作吗”。操作员需要知道谁请求、对什么资源、做什么、影响多大、为什么需要,以及批准后多久执行。推荐把模型可读摘要与机器可执行 payload 分开保存。

{
  "approval_id": "apr_01...",
  "requested_by": "agent_run_789",
  "tool": "send_invoice_email",
  "payload_hash": "sha256:...",
  "resource_scope": ["invoice:inv_123", "customer:cus_456"],
  "risk_level": "high",
  "reason": "发送已生成的 8 月账单",
  "expires_at": "2026-08-22T19:00:00Z",
  "policy_version": "billing-v7"
}

原始 payload 应加密保存,展示时做脱敏;payload_hash 用于确保执行的内容与审批时完全一致。审批后不允许 Agent 悄悄替换收件人、金额或附件。若任何受保护字段变化,旧审批作废并创建新请求。

还要记录权限快照,但不能只依赖快照执行。快照帮助解释“当时为什么允许提交”,真正执行前仍要重新检查审批者与请求者的当前权限,防止离职、角色撤销或资源归属变化后继续执行。

三、用状态机代替两个布尔字段

approvedexecuted 两个布尔值无法表达过期、拒绝、取消和执行失败,也容易出现非法组合。审批资源应使用单一状态字段与受控迁移。

PENDING -> CLAIMED -> APPROVED -> EXECUTING -> EXECUTED
   |          |          |            |
   v          v          v            v
EXPIRED    PENDING    CANCELLED     EXECUTION_FAILED
   |
   v
NEW_REQUEST(需要重新评估时)

CLAIMED 表示操作员暂时领取,不代表其他人永远不能处理;它需要租约到期时间。APPROVED 只代表授权成立,实际副作用尚未发生。EXECUTION_FAILED 还要区分可重试与不可重试,不能自动退回 APPROVED 后无限执行。

所有迁移通过类似 transition(approvalId, expectedState, targetState) 的 compare-and-set 完成。数据库更新条件包含旧状态和版本号,受影响行数为零就说明另一个工作者已经处理,当前请求应读取最新状态而不是覆盖。

四、并发领取与排序要服务业务 SLA

审批列表通常按风险和剩余时间排序,而不是简单先进先出。可用优先级分数综合风险、等待时长、业务截止时间与客户等级,但公式必须透明,避免普通请求永久饥饿。对同一资源的一组动作还要显示关联关系,例如“先更新报价,再发送报价单”。

多个操作员领取时,可使用数据库的行锁与 SKIP LOCKED,或通过原子更新写入 claimed_byclaim_expires_at。租约到期后请求自动回到可领取状态。前端轮询或实时推送只负责体验,数据库状态才是唯一事实来源。

批量审批只能用于参数同质、影响可独立回滚的请求。界面必须展示批量范围、总金额或总资源数,并让后端逐项执行与记录结果。不要把 100 个请求压成一个不可分割的“全部通过”,否则其中一个失败时无法准确重试。

五、批准后的执行必须幂等

操作员双击按钮、浏览器重发请求、队列消费者宕机,都可能让执行入口被调用多次。审批 ID 可以作为幂等键的一部分,但若同一审批包含多个步骤,应使用 approvalId + stepName。执行器先写入唯一执行记录,再调用外部服务,并保存外部请求 ID。

最棘手的是外部调用成功、数据库回写失败。此时重试前必须查询外部系统,或使用对方支持的幂等键。若第三方接口既不能查询也不支持幂等,高风险动作不应自动重试,应进入人工核对状态。

批准不代表无限期有效。执行前检查:审批未过期、payload hash 一致、策略版本仍可接受、相关权限仍有效、依赖条件未变化。价格或资源版本变化时,转为 STALE 或取消并重新申请,不要“尽量执行”。

六、超时、拒绝与升级路径要可预测

创建审批时就计算过期时间与升级规则。低时效任务过期后可以取消;生产事故处置可能在十分钟未响应后升级到值班负责人;金融或隐私相关操作则可能禁止自动升级权限,只能换人复核。

拒绝原因最好使用结构化分类加可选备注,例如 INSUFFICIENT_CONTEXTWRONG_TARGETPOLICY_VIOLATIONNOT_NEEDED。Agent 可以据此补齐信息或终止计划,但不应把拒绝当成“换一种说法再次请求”的信号。相同 payload 被拒绝后,除非事实发生变化,不应自动重新排队。

通知也要去重和分级。首次创建、临近 SLA、升级和最终结果分别对应不同事件;不要每次 worker 扫描都发送提醒。通知失败不应改变审批状态,但应进入独立重试队列并保留送达记录。

七、审计日志采用追加写事件

审批表保存当前状态,审计表保存发生过什么。每次创建、查看敏感详情、领取、释放、批准、拒绝、过期、执行和重试都追加事件,记录操作者、来源、时间、旧状态、新状态、理由、策略版本与 trace ID。

审计事件不能包含明文密钥或完整敏感 payload。需要取证时,通过受控权限读取加密原文,并记录这次读取本身。对 UI 展示,可以从事件流生成时间线;对合规导出,则按资源、操作者和时间范围检索。

如果 Agent 会跨多个服务推进长事务,还应配套补偿动作。审批只证明某一步被授权,不保证后续每一步都成功。相关设计可参考Agent Saga 补偿事务设计

八、测试与监控决定队列是否真的可靠

单元测试要覆盖所有合法与非法状态迁移;并发测试模拟两个审批者同时点击;故障测试在外部调用前后分别中断 worker;时间测试使用可控时钟验证过期与租约;安全测试验证越权审批、payload 篡改和重放请求均被拒绝。

核心指标包括待审批数量、各风险级别等待时长、SLA 超时率、批准率、拒绝原因分布、领取后放弃率、批准到执行的延迟、幂等去重次数、执行失败率和人工升级次数。队列长度上涨不一定代表审批者不足,也可能是 Agent 生成了过多低质量请求,应同时看拒绝原因与重复率。

告警应围绕用户影响:高风险请求超过 SLA、已批准请求长时间未执行、同一资源出现互斥审批、幂等冲突异常升高。通过这些信号,团队才能区分流程拥堵、执行器故障与模型行为漂移。

九、相关阅读

如果审批后的 Agent 任务需要统一调用不同模型,YoTradeApi 可提供兼容常见 SDK 的 API 接入方式,方便集中管理鉴权与用量。