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 悄悄替换收件人、金额或附件。若任何受保护字段变化,旧审批作废并创建新请求。
还要记录权限快照,但不能只依赖快照执行。快照帮助解释“当时为什么允许提交”,真正执行前仍要重新检查审批者与请求者的当前权限,防止离职、角色撤销或资源归属变化后继续执行。
三、用状态机代替两个布尔字段
approved 与 executed 两个布尔值无法表达过期、拒绝、取消和执行失败,也容易出现非法组合。审批资源应使用单一状态字段与受控迁移。
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_by、claim_expires_at。租约到期后请求自动回到可领取状态。前端轮询或实时推送只负责体验,数据库状态才是唯一事实来源。
批量审批只能用于参数同质、影响可独立回滚的请求。界面必须展示批量范围、总金额或总资源数,并让后端逐项执行与记录结果。不要把 100 个请求压成一个不可分割的“全部通过”,否则其中一个失败时无法准确重试。
五、批准后的执行必须幂等
操作员双击按钮、浏览器重发请求、队列消费者宕机,都可能让执行入口被调用多次。审批 ID 可以作为幂等键的一部分,但若同一审批包含多个步骤,应使用 approvalId + stepName。执行器先写入唯一执行记录,再调用外部服务,并保存外部请求 ID。
最棘手的是外部调用成功、数据库回写失败。此时重试前必须查询外部系统,或使用对方支持的幂等键。若第三方接口既不能查询也不支持幂等,高风险动作不应自动重试,应进入人工核对状态。
批准不代表无限期有效。执行前检查:审批未过期、payload hash 一致、策略版本仍可接受、相关权限仍有效、依赖条件未变化。价格或资源版本变化时,转为 STALE 或取消并重新申请,不要“尽量执行”。
六、超时、拒绝与升级路径要可预测
创建审批时就计算过期时间与升级规则。低时效任务过期后可以取消;生产事故处置可能在十分钟未响应后升级到值班负责人;金融或隐私相关操作则可能禁止自动升级权限,只能换人复核。
拒绝原因最好使用结构化分类加可选备注,例如 INSUFFICIENT_CONTEXT、WRONG_TARGET、POLICY_VIOLATION 和 NOT_NEEDED。Agent 可以据此补齐信息或终止计划,但不应把拒绝当成“换一种说法再次请求”的信号。相同 payload 被拒绝后,除非事实发生变化,不应自动重新排队。
通知也要去重和分级。首次创建、临近 SLA、升级和最终结果分别对应不同事件;不要每次 worker 扫描都发送提醒。通知失败不应改变审批状态,但应进入独立重试队列并保留送达记录。
七、审计日志采用追加写事件
审批表保存当前状态,审计表保存发生过什么。每次创建、查看敏感详情、领取、释放、批准、拒绝、过期、执行和重试都追加事件,记录操作者、来源、时间、旧状态、新状态、理由、策略版本与 trace ID。
审计事件不能包含明文密钥或完整敏感 payload。需要取证时,通过受控权限读取加密原文,并记录这次读取本身。对 UI 展示,可以从事件流生成时间线;对合规导出,则按资源、操作者和时间范围检索。
如果 Agent 会跨多个服务推进长事务,还应配套补偿动作。审批只证明某一步被授权,不保证后续每一步都成功。相关设计可参考Agent Saga 补偿事务设计。
八、测试与监控决定队列是否真的可靠
单元测试要覆盖所有合法与非法状态迁移;并发测试模拟两个审批者同时点击;故障测试在外部调用前后分别中断 worker;时间测试使用可控时钟验证过期与租约;安全测试验证越权审批、payload 篡改和重放请求均被拒绝。
核心指标包括待审批数量、各风险级别等待时长、SLA 超时率、批准率、拒绝原因分布、领取后放弃率、批准到执行的延迟、幂等去重次数、执行失败率和人工升级次数。队列长度上涨不一定代表审批者不足,也可能是 Agent 生成了过多低质量请求,应同时看拒绝原因与重复率。
告警应围绕用户影响:高风险请求超过 SLA、已批准请求长时间未执行、同一资源出现互斥审批、幂等冲突异常升高。通过这些信号,团队才能区分流程拥堵、执行器故障与模型行为漂移。
九、相关阅读
如果审批后的 Agent 任务需要统一调用不同模型,YoTradeApi 可提供兼容常见 SDK 的 API 接入方式,方便集中管理鉴权与用量。