Human-in-the-Loop审批超时后任务为什么丢失?等待状态、恢复令牌与过期策略完整排查
文章摘要Human-in-the-Loop可以在Agent执行高风险工具前暂停流程,等待人工批准、修改或拒绝。但很多系统把“等待审批”实现成内存中的Future、HTTP长连接或短期Redis Key:审批人几小时后点击同意时,原Pod已经重启,回调找不到等待对象;或者任务虽然还在数据库中,恢复令牌已无法对应当前Checkpoint;再或者Plan已经修订,旧审批仍然放行了不同参数的副作用。审批超时也不等于拒绝。它可能意味着审批人未看到通知、时区不同、值班人变更或业务已经失去时效。生产系统必须明确等待状态、审批对象、恢复令牌、截止时间、升级路径、过期动作和状态版本。本文从Interrupt持久化、审批请求、Resume Token、乐观锁、一次性消费、Plan与Input绑定、过期扫描、通知重试、代理审批、多级审批、取消、图版本迁移和审计等方面给出完整排查与实现方案。一、最常见的丢失场景Agent运行:生成付款申请 ↓ 等待财务负责人审批代码:CompletableFutureDecisionfuture=newCompletableFuture();waitingMap.put(runId,future);Decisiondecision=future.get(24,TimeUnit.HOURS);几个小时后:Pod滚动发布 →waitingMap清空 →审批回调到达 →找不到Future →任务永远WAITING根因是:等待关系存在内存中 而不是持久状态中二、Human-in-the-Loop需要哪些持久对象至少:Agent Run Checkpoint Interrupt Approval Request Approval Decision Resume Token Notification Attempt Audit Event只存run.status=WAITING不够。三、审批请求模型publicrecordApprovalRequest(StringapprovalId,StringtenantId,StringrunId,StringthreadId,StringinterruptId,StringcheckpointId,StringstepId,intplanVersion,StringplanHash,StringinputHash,ApprovalTypetype,RiskLevelrisk,SetStringallowedActions,StringrequestedBy,InstantrequestedAt,InstantexpiresAt,ApprovalStatusstatus,longrowVersion){}四、审批状态publicenumApprovalStatus{PENDING,APPROVED,EDITED,REJECTED,EXPIRED,CANCELLED,SUPERSEDED,CONSUMED}为什么需要SUPERSEDED:Plan已经重新生成 旧审批请求失效五、审批不是Run级布尔值错误:run.approved=true这无法回答:批准了哪个Step;-批准了哪些参数;-基于哪个Plan;-是否允许修改;-何时过期;-是否已消费。审批必须绑定具体操作。六、Interrupt模型publicrecordAgentInterrupt(StringinterruptId,StringrunId,StringthreadId,StringcheckpointId,StringnodeName,InterruptTypetype,JsonNodepayload,StringpayloadHash,InterruptStatusstatus,InstantcreatedAt,InstantresumedAt){}七、Interrupt与Approval的关系一个Interrupt可能:触发一个审批;-触发多级审批;-请求用户补充信息;-请求人工修改参数;-请求选择分支。Approval是Human Input的一种,不应把所有Interrupt都叫审批。八、恢复令牌为什么不能只含runId错误URL:/approve?runId=R100攻击者或旧页面可能对当前不同状态生效。Resume Token至少绑定:token_id run_id interrupt_id checkpoint_id state_version expected_node allowed_action expires_at nonce九、Resume Token模型publicrecordResumeTokenCla