多 Agent 等待图:死锁检测、受害者选择与取消传播
Agent 互相等待结果、锁、审批或容量时,仅靠逐任务超时会把死锁拖成长故障。本文建立带代数的等待图,检测强连通环并用可验证取消收敛释放资源。
本文要点
- 主题聚焦:AI、AI Agent、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
Agent A 等 B 的审阅,B 为补证据又等 A 释放浏览器;或者 supervisor 等所有子任务,某个子任务又发起一项必须由 supervisor 确认的同步委派。它们都还活着,没有抛异常,CPU 甚至很低,但系统永远不会前进。给每个步骤设十分钟超时只能让用户多等十分钟,并可能同时取消整个 run,浪费已经完成的安全工作。
数据库死锁发生在事务与锁之间,多 Agent 等待还包括结果、人工审批、预算、队列槽位和外部工具。不能只把数据库术语换成 Agent 名字:这里必须识别哪些等待可取消、哪些副作用已提交、哪些关系会被事件自然解除。多 Agent 通信 讨论消息范式,本篇只处理跨范式的持久等待关系与取消收敛。
wait-edge.v1 必须由运行时登记
等待方进入阻塞前写 WAIT(waiterExecution, waiterGeneration, targetId, targetGeneration, reason, deadline, cancelMode, edgeVersion)。目标可以是 execution、artifact、approval、reservation 或 capacity permit。运行时还登记目标的满足者关系 PRODUCED_BY/PROVIDED_BY/HELD_BY(target -> execution@generation);自然语言“我在等安全 Agent”不能进入图,必须解析为稳定 ID。
WAIT(exec:A@g7 -> artifact:review-42@g3)
PRODUCED_BY(artifact:review-42@g3 -> exec:B@g3)
WAIT(exec:B@g3 -> resource:browser-9@lease12)
HELD_BY(resource:browser-9@lease12 -> exec:A@g7)
收到目标事件、释放资源、超时、取消或 generation 变化后,在对应 canonical 状态转移中关闭 WAIT 或满足者边。generation 阻止旧 attempt 的迟到消息删除新边。短 RPC 可先在内存采样,但超过探测阈值或持有稀缺资源时必须持久化,否则检测器看不到危险环。
Agent 委派协议 要求父任务能回收责任,因此同步委派必须登记“父等子”关系;如果子任务还需要父的审批,审批应交给独立 actor 或改为异步事件,不能隐藏一条反向边。多 Agent DAG 调度恢复 中合法的依赖边是有向无环的,运行时 wait graph 还会叠加资源与人工关系,因此需要独立检测。
检测器要区分环与慢等待
周期检测器读取同一快照的活跃边,过滤终态和失效 generation,再沿 execution -> target -> holder/producer 求强连通分量,也可先投影成 waiter execution -> satisfying execution 的 wait-for graph。多实例容量只有在无空闲 grant 且所有可能释放者已知时才投影 holder 边。包含环且每项等待只能由分量内事件解除,才是 candidate;A 等慢 API 是依赖延迟,holder 正在推进则是拥塞。
分量要经过短暂确认窗并二次读取,避免与刚到达的完成事件竞争。若环包含不可取消的未知副作用,先冻结相关新调度并进入人工对账,不能直接杀 worker。PostgreSQL 会检测死锁并中止一个事务;Agent 运行时同样需要受害者,但选择成本更复杂。
受害者评分应由确定性策略计算:优先取消未产生外部副作用、可从检查点恢复、低优先级、已消耗成本较少且不持有唯一证据的 execution。禁止模型自行挑选“最不重要的人”。决定记录 deadlockSetDigest、策略版本、候选分数、winner generation 和理由码,随后用 CAS 把受害者从 WAITING 改为 CANCELLING。
取消是协议,不是一个布尔值
run 的单调 cancelGeneration 是事实源,事件只是唤醒。child create、DAG 激活、claim/renew、每次工具接受和 canonical commit 都必须比较继承的 observed generation;不相等即拒绝,因此取消时并发新建的后代也不能启动。控制面向未终态后代发布 CancelRequested.v1,并持续扫描旧 generation 直到收敛;节点停止新调用、唤醒 wait、关闭流,并把副作用登记为 NONE/COMMITTED/UNKNOWN。DOM AbortSignal 和 Go context 只提供传播接口,不能把“已发信号”写成“已停止”。
对已提交结果,取消只阻止继续传播,不能删除事实;对可补偿写入,先记录 canonical commit 再排入版本化补偿;对未知结果,进入 Agent 故障恢复顺序 的对账分支。task claim 到期和取消也不同:到期允许更高 generation 接手,取消则禁止本 run 的新 attempt。两者应结合 多 Agent 任务 Claim,让被取消旧 worker 即使恢复也无法提交。
只有后代都达到 CANCELLED/COMPLETED/NEEDS_RECONCILIATION,所有 capacity permit 和 lease 已释放,父级才进入 CANCELLED_STABLE。超过取消 SLO 的节点进入隔离池并报警,不能提前复用其浏览器、工作区或租户上下文。
负向测试、指标与回滚
测试构造二/三节点环、artifact producer 环、任务等待自己持有的容量、多实例池仍有空闲、完成与判定竞争、旧 generation 关闭新边。再让取消枚举后并发创建 child、工具忽略 cancel、传播中 worker 崩溃;断言新 child 在 claim 前被 epoch 拒绝,每个真实环一个幂等受害者决定,非环不误杀,CANCELLED_STABLE 后无活跃 lease 或 owner。
指标包括活跃等待边、最长等待、候选环数、确认死锁数、误判撤回、受害者成本、取消传播 p95、拒绝新调用到实际静默的间隔、泄漏 permit 和未知副作用数。按 reason 与 generation 统计,避免高基数 Prompt 进入标签。AI 遥测基数预算 可约束这部分观测。
上线先 shadow 构图和报告环,不自动取消;人工核验受害者选择后只对无副作用测试 run 启用。异常时关闭自动 victim action,但保留边登记、探测与告警;若需回滚 schema,先冻结新委派,等待新 cancel generation 收敛并导出未决边。不能直接清空 wait graph,那会把仍持有资源的任务伪装成健康。恢复旧调度器前确认活跃环有人工 owner、取消中的资源已隔离,再解除准入。
Related