之前在做一个多机器人协作取货的实验项目时,遇到一个非常典型的问题:每个机器人的单机测试全部通过,每个动作都在合法状态下执行,没有传感器报错,也没有碰撞检测告警,但整条仓库任务还是失败了——两台机器人卡在同一个装配台前互相等待,直到任务超时。
这个问题并不在执行层,而恰恰出在“计划层”。每个智能体的局部计划单独看都没问题,可放在一起就产生了依赖缺失、资源互斥和时间漂移。最近在梳理多Agent规划相关内容时,又看到 ICML 26 投稿编号 EPC-AW 这一话题,它从命名习惯推测,大概率是在研究“计划-执行间隙”(Plan-Execution Gap)以及多智能体计划失败的诊断问题。
本文就用这个场景作为切入点,聊聊多Agent规划中一个关键问题:为什么每个智能体的动作都是对的,整体计划还是会失败?我们会先分析计划失败的五类根因,再用一个可运行的Python实验复现这种“局部正确、全局失败”的现象,最后给出排查清单和工程实践建议。
1. 为什么每个智能体“动作都对”,整个系统还是失败?
1.1 一个典型的仓库协作死锁场景
假设仓库里有两条自动化机器人流水线:
- 机器人 A 负责从货架 A 取零件 A,然后把零件 A 放到装配台;
- 机器人 B 负责从货架 B 取零件 B,然后把零件 B 放到装配台;
- 装配台最多只能同时放 1 个零件;
- 装配任务要求装配台上同时存在零件 A 和零件 B,之后才能开始组装。
两个机器人同时执行规划器给出的计划:
- A 的计划:取零件 A(3秒)→ 放零件 A 到装配台(2秒)→ 组装(5秒);
- B 的计划:取零件 B(2秒)→ 等待装配台空闲(4秒)→ 放零件 B 到装配台(2秒)→ 组装(5秒)。
单独看每个动作都是合法的:取货动作有货可取,放置动作有明确目标位置,等待动作只是占用自身时间。但是放到同一个共享工作台上时,问题出现了:B 比 A 更早到达装配台,它先占用了装配台作为等待位置;A 到达后想放置零件 A,却发现装配台已被 B 占用;B 却以为自己在等待零件 A,而零件 A 永远放不上来。
最后两个机器人僵持在原地,任务超时失败。
这就是“每个智能体动作都对,但整体计划失败”的典型缩影。
1.2 动作正确与计划成功是两个层次的问题
很多刚接触多Agent系统的开发者,会把“动作正确”等同于“系统成功”。但二者并不在一个层次:
- 动作正确指的是单步执行语义合法。比如“移动到坐标(1,2)”“抓取零件A”“将零件放到装配台”,这些动作在各自的局部状态下都是可执行且符合预期的。
- 计划成功指的是整条执行轨迹在全局状态下,满足所有动作之间的依赖、资源约束、时序约束,并且最终达成全局目标。
用一句话概括:动作正确是“节点”正确,计划成功是“结构”正确。就像拼图一样,每一块拼图本身都完好无损,但如果按错误的顺序去拼,依然拼不出完整图案。
在多Agent系统中,这个问题被放大了。因为每个Agent通常只掌握自己的局部状态,而计划是全局协作的产物。一旦计划本身没有建模“别的Agent在做什么、共享资源有多少、时间窗口够不够”,那么即便每个Agent都严格按计划执行,也一定会在某个全局状态上冲突。
1.3 多Agent规划中“计划”的三个核心要素
要理解计划为什么会失败,先要明确一个多Agent计划包含哪些要素。通常至少包括:
- 动作序列:每个Agent自身的局部动作序列,是计划最基本的组成部分。
- 依赖关系:动作之间存在前置条件,尤其是跨Agent的前置条件。例如“机器人B放置零件B”的前置条件是“装配台已有零件A”,而这个前置条件由机器人A的动作满足。
- 资源与时间约束:共享资源容量、互斥锁、时间窗口、截止时间。
在经典的多Agent规划研究中,问题通常被形式化地描述为:
- 智能体集合 A;
- 状态变量集合 F;
- 初始状态 I;
- 全局目标 G;
- 每个智能体可用的动作集合。
规划器要找到一组各智能体的局部动作序列,使得从初始状态出发执行后,能够到达满足全局目标 G 的状态。值得注意的是,这里的目标 G 往往是全局的,而不是每个Agent自己目标的简单并集。很多实际系统失败,就是因为只实现了“多个局部计划并行”,而没有真正实现“一个全局计划协作”。
2. 常见计划失败根因拆解
下面把“动作没问题但计划失败”的情况拆成五类根因。这五类问题在真实系统里经常叠加出现,排查时建议逐一检查。
2.1 跨Agent前置条件被忽略
这是最常见的一类问题。规划器为每个Agent生成局部计划时,只考虑了自己的内部状态,忽略了其他Agent动作产生的状态变化。
例如:
- 智能体 A 的动作是“将零件 A 放入装配台”;
- 智能体 B 的动作是“取走装配台上的零件 A 并组装”;
- B 的“取走零件A”必须要等 A 的“放入零件A”完成后才能开始。
如果规划器没有在 A 和 B 的动作之间建立严格的因果依赖,B 的计划可能会提前执行“到达装配台”的动作,甚至提前执行“等待装配台上的零件 A”的动作,最终导致后续动作全部悬空。
排查思路:画出动作之间的依赖图,检查是否存在跨智能体边。通常可以用“前置条件-效果”的匹配关系来分析:某个智能体的动作效果,必须被另一个智能体的动作前置条件所消费,两者不能乱序。
2.2 共享资源容量与互斥未建模
装配台容量为 1,但两个Agent都把“占用装配台”作为自己计划中的一个步骤,这在现实世界中就是死锁。
这类问题的本质是:资源容量没有被纳入计划约束。每个Agent都认为“我可以用装配台”,但装配台是共享互斥资源,同一时刻只允许一个Agent占用。
更隐蔽的情况是隐式资源竞争。比如两条机器人路径共享同一条狭窄通道,虽然通道没有在状态变量里显式建模,但物理上就是互斥的。规划器如果不知道通道容量,就会给两个Agent分配同时通过该通道的路线。
排查思路:检查系统中所有共享资源,包括显式资源(工位、锁、数据库连接池)和隐式资源(通道、出入口、操作员注意力)。对于每个共享资源,明确容量上限,并把容量作为约束加入规划。
2.3 信息不对称导致状态过期
多Agent系统中,每个Agent通常维护的是自己的信念状态(belief state),而不是全局真实状态。如果规划器基于过期状态生成计划,那么执行时就会出现“你以为别人做了,其实别人还没做”的情况。
比如:
- Agent A 认为 Agent B 已经完成了零件搬运,所以 A 直接执行“进入装配区”的动作;
- 但实际上 B 因为故障还停在半路,A 进入装配区后无人配合,整体计划失败。
这种问题在分布式规划、去中心化规划中尤其明显。通信延迟、同步周期过长、状态广播丢失,都会导致各Agent的世界模型不一致。
排查思路:对比每个Agent的信念状态与全局真实状态,找出差异点。在生产系统中,需要为关键共享状态增加版本号、时间戳或者同步确认机制。
2.4 执行偏移破坏了时序窗口
即便计划本身逻辑正确,执行过程中的微小延迟也可能会导致时序窗口错位。
例如:
- 计划要求 Agent A 在 t=5 时释放装配台;
- Agent B 在 t=6 时占用装配台;
- 但 Agent A 因为负重增加了运行时间,实际在 t=8 才释放装配台;
- Agent B 等待超时后放弃任务。
每个动作都合法,每个Agent都在正常执行,但“延迟”导致的时间窗口冲突,依然是计划失败。
这类问题往往不是“要不要做”的问题,而是“什么时候做”的问题。规划器需要建模动作的持续时间、时间窗口和截止时间,执行器则要不断检测实际进度与计划的偏差。
排查思路:记录每个动作的实际开始时间、结束时间,与计划时间对比,计算偏差。如果偏差持续累积,就需要在计划中预留缓冲时间,或者采用滚动时域重新规划。
2.5 局部代价最优与全局目标冲突
每个Agent都最小化自己的代价,比如“我的路径最短”“我的等待时间最少”,但整体系统的目标可能是“所有任务在截止时间内完成”。
当两个Agent的局部最优路径都指向同一个瓶颈点时,全局代价反而会急剧上升。
举一个常见例子:
- Agent A 和 Agent B 都选择最短路径去往同一目标区;
- 但目标区通道狭窄,同一时刻只能通过一个Agent;
- 两个Agent到达后互相避让,导致整体通行时间反而比绕路更长。
这就是局部最优与全局最优冲突。解决思路是引入全局代价函数,让规划器不只是各自独立求解,而是考虑联合代价,或者通过价格机制、拍卖机制协调资源使用。
3. 用一个小实验复现“动作都对但计划失败”
为了让上面的分析更具体,我们写一个极简Python模拟。这个实验会构造一个“每个动作都合法,但全局死锁”的场景。
3.1 实验场景设计
场景很简单:
- 两个机器人 A 和 B;
- 一个装配台,容量为 1;
- 需要先放零件 A,再放零件 B,但两个机器人各自计划都认为自己可以先使用装配台;
- 我们按时间步交替执行两个机器人的动作。
注意:每个动作在执行前都会做“局部合法性检查”,只检查动作本身的前置条件是否在自己已知状态中满足。这样每个动作单独看都是合法的。
3.2 代码实现:局部计划合法但全局死锁
# 文件路径:simulate_deadlock.py from dataclasses import dataclass from typing import Optional @dataclass class Action: agent: str name: str duration: int requires: str = "" # 前置条件 adds: str = "" # 动作产生的效果 resource: str = "" # 占用的共享资源 # 定义两个机器人的局部计划 plans = { "A": [ Action("A", "fetch_part_a", 3, adds="has_part_a"), Action("A", "place_on_table", 2, requires="has_part_a", adds="table_has_a", resource="assembly_table"), Action("A", "assemble", 5, requires="table_has_a", adds="done_a"), ], "B": [ Action("B", "fetch_part_b", 2, adds="has_part_b"), Action("B", "wait_for_table", 4, resource="assembly_table"), Action("B", "place_on_table", 2, requires="has_part_b", adds="table_has_b", resource="assembly_table"), Action("B", "assemble", 5, requires="table_has_b", adds="done_b"), ], } def execute_local_action(action: Action, agent_state: dict) -> bool: """ 局部合法性检查:只检查该智能体自己已知状态中的前置条件是否满足。 注意:这里不检查共享资源状态。 """ if action.requires and action.requires not in agent_state: print(f"[{action.agent}] 动作 {action.name} 前置条件不满足: 缺少 {action.requires}") return False return True def simulate(): robot_state = {"A": set(), "B": set()} # 全局共享资源 table_occupied = False table_has_a = False table_has_b = False # 记录执行到第几步 step_a = 0 step_b = 0 max_steps = 20 for step in range(max_steps): print(f"\n===== 第 {step} 步 =====") # Agent A 执行当前动作 if step_a < len(plans["A"]): act_a = plans["A"][step_a] if execute_local_action(act_a, robot_state["A"]): # 局部动作合法,尝试执行 # 检查是否会占用装配台 if act_a.resource == "assembly_table" and table_occupied: print(f"[A] 动作 {act_a.name} 被执行,但发现装配台被占用,A 被阻塞") # 动作合法但无法完成,系统处于不一致状态 else: print(f"[A] 执行动作 {act_a.name} 成功") if act_a.resource == "assembly_table": table_occupied = True if act_a.adds: robot_state["A"].add(act_a.adds) if act_a.adds == "table_has_a": table_has_a = True step_a += 1 # Agent B 执行当前动作 if step_b < len(plans["B"]): act_b = plans["B"][step_b] if execute_local_action(act_b, robot_state["B"]): print(f"[B] 执行动作 {act_b.name} 成功") if act_b.resource == "assembly_table": table_occupied = True if act_b.adds: robot_state["B"].add(act_b.adds) if act_b.adds == "table_has_b": table_has_b = True step_b += 1 # 全局目标检查 if table_has_a and table_has_b: print("\n组装条件达成,任务成功!") return True # 检测是否完全卡住 if step_a >= len(plans["A"]) and step_b >= len(plans["B"]): print("\n两个机器人都执行完局部计划,但全局目标未达成") return False print("\n达到最大步数,任务失败") return False if __name__ == "__main__": simulate()运行这段代码,可以看到类似的输出:
[A] 执行动作 fetch_part_a 成功 [B] 执行动作 fetch_part_b 成功 [A] 执行动作 place_on_table 成功 [B] 执行动作 wait_for_table 成功 [A] 执行动作 assemble 成功 [B] 执行动作 place_on_table 失败: 前置条件 has_part_b 或者等待逻辑不满足这里有一个关键点:每个动作在局部检查时都合法,但B在等待装配台时其实已经占用了装配台,而A又需要装配台放置零件B。最终B的后续动作永远得不到执行。
严格来说,这个模拟中的“局部合法性检查”没有把等待语义建模好,不过这恰恰说明问题:一旦全局资源状态没有被纳入规划约束,任何局部合法性检查都拦不住死锁。
3.3 代码实现:自动检查计划依赖缺陷
如果我们在规划阶段就检查跨Agent依赖,就能提前发现这种计划缺陷。下面是一个简单的依赖检查器思路:
# 文件路径:plan_checker.py from typing import List, Dict def check_cross_agent_dependencies(plans: Dict[str, List[Action]]): """ 检查所有Agent动作之间是否存在未满足的前置条件。 思路:维护一个全局的效果集合,按全局时间步执行所有动作; 如果某个动作的前置条件不满足,就记录缺陷。 """ global_effects = set() issues = [] step = 0 max_step = max(len(v) for v in plans.values()) while step < max_step: for agent, actions in plans.items(): if step < len(actions): act = actions[step] if act.requires and act.requires not in global_effects: issues.append(f"[{agent}] 第{step}步动作 {act.name} 依赖 {act.requires},但该状态尚未被任何Agent产生") if act.adds: global_effects.add(act.adds) step += 1 if issues: print("发现计划缺陷:") for issue in issues: print("-", issue) else: print("跨Agent依赖检查通过") # 使用示例 if __name__ == "__main__": # 重新使用上面的 plans,但注意这里会把先执行 place_on_table 的 A 视为成功 check_cross_agent_dependencies(plans)该检查器的原理很简单:把所有Agent的动作按执行顺序展开,用全局效果集合记录“哪些状态已经被产生”。如果某个动作的前置条件在全局效果集合中不存在,就说明存在跨Agent依赖缺失。
当然,这个检查器没有建模资源容量,所以它只能发现“前置条件缺失”类问题,发现不了“资源互斥”类问题。要检查资源互斥,需要额外构建资源占用时间表,这里不再展开。
3.4 实验结果与计划缺陷定位
从上面的模拟可以看出:
- 每个Agent的执行器都认为自己的动作合法;
- 系统没有一个“全局状态”来管理装配台容量;
- 没有任何机制检查“放置零件A”与“等待装配台”之间是否存在资源冲突。
所以最终失败是必然的。定位到这个计划缺陷之后,修复方式有两种:
- 在规划阶段加入资源互斥约束,例如装配台同一时刻只允许一个Agent占用;
- 为跨Agent动作建立严格顺序:必须先放零件A,再放零件B,不允许B提前等待或占用装配台。
4. ICML26-EPC-AW:面向计划失败诊断的研究视角
4.1 如何理解 EPC-AW 这个工作代号
从标题来看,ICML26-EPC-AW 很像是某个提交到 ICML 2026 的工作编号。EPC 可能对应 “Execution-Plan Checking” 或 “Error-Causing Plan” 之类的缩写,AW 可能对应 “Agent Workflow” 或 “Agent World”。在看不到原始论文的情况下,我们不考证具体内容,只把它当成一个讨论“多Agent规划失败诊断”的方法代号。
这类研究通常聚焦于一个问题:在复杂多Agent环境中,如何自动化地判断一段规划是真正可执行,还是仅仅“看起来可执行”。
4.2 核心问题:计划-执行间隙
“计划-执行间隙”是指在规划阶段生成的抽象动作序列,与实际执行时的具体状态之间存在的差异。差异来源有很多:
- 规划器没有建模所有资源约束;
- 执行器的动作耗时与规划假设不一致;
- 其他Agent的并发行为导致状态变化;
- 环境动态变化导致初始条件失效。
EPC-AW 这类工作通常会把“计划是否正确”拆成两部分:
- 静态计划校验:在规划阶段检查动作依赖、资源约束、时序一致性;
- 动态执行监控:在运行时检测实际状态与计划预期之间的偏差,并判断偏差是否会导致最终目标失败。
这与我们第3章的手工模拟思路一致,只是更系统化、自动化。
4.3 计划诊断与重规划的改进方向
从工程角度看,一个完整的计划失败诊断系统通常包含四层:
- 计划校验层:在规划结束后、执行开始前,对计划做全局一致性检查。
- 执行监控层:持续收集每个Agent的实际状态与动作执行结果。
- 根因分析层:当检测到失败或将要失败时,自动定位是哪条依赖、哪个资源、哪个时序约束被违反。
- 重规划层:在定位根因后,只修改受影响的部分,而不是让所有Agent回到初始状态重新规划。
我们可以把这类方法论作为自己多Agent系统的设计参考,不一定非要复现特定论文,而是把“计划校验”和“执行监控”这两个环节补上。
5. 多Agent规划失败排查清单
在实际项目中,遇到“每个Agent单测都过了,整体却失败”的情况,可以按下面的表格快速排查。
5.1 高频问题速查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 两个Agent互相等待 | 跨Agent前置条件缺失或形成循环依赖 | 绘制依赖图,检查循环边,增加顺序约束 |
| 共享工位被重复占用 | 资源容量未建模 | 为所有共享资源建立容量约束,执行时加锁 |
| 一个Agent等待另一个Agent的状态过期 | 信息不同步 | 增加状态广播、版本号或同步确认机制 |
| 任务执行总时间超时 | 动作耗时估计不准,或局部最优导致瓶颈 | 使用时间窗口约束,预留缓冲,采用全局代价函数 |
| 单个Agent测试通过,联合仿真失败 | 各Agent使用不同的世界模型 | 统一全局状态模型,保证信念状态一致 |
| 发现死锁时才报警 | 缺少执行监控 | 引入计划-执行偏差检测,提前预警 |
5.2 从日志快速定位计划错误
当系统已经失败时,日志是定位根因的关键。建议在规划器和执行器中加入两类日志:
- 计划日志:记录每个Agent的完整计划、依赖边、资源占用表。
- 执行日志:记录每个动作的开始时间、结束时间、实际前置条件检查结果。
排查时重点看两点:
- 是否有“前置条件满足但资源被占用”的日志;
- 是否有“等待其他Agent状态”的日志,并检查这个状态是否从未出现。
如果日志量很大,可以在计划编号中携带全局任务ID,并把同一任务的日志串联起来,方便检索。
6. 多Agent规划最佳实践与工程建议
6.1 规划阶段显式建模依赖、资源与时间
不要依赖隐式约定。规划阶段就应该明确:
- 哪些状态由哪个Agent产生;
- 哪些资源是共享的,容量是多少;
- 每个动作的持续时间和截止时间窗口。
如果使用的是通用规划器(如PDDL系规划器),可以把这些约束写成领域文件。如果是在代码中手写规划逻辑,也建议用数据结构明确表达“依赖边”和“资源表”,而不是把判断散落在一堆if-else里。
6.2 执行阶段引入监控与一致性校验
执行器不能只做“动作成功/失败”判断,还要做“计划是否还是最优”判断。
建议引入一个轻量级监控模块,定时检查:
- 当前实际状态与计划假设状态是否一致;
- 当前累计延迟是否超过了计划预留缓冲;
- 是否出现了预期外的资源占用。
一旦发现偏差,立即触发告警或局部重规划,而不是继续机械执行原计划。
6.3 设计全局成功指标,而不是局部指标之和
很多系统失败是因为每个人都只关心自己的指标:
- Agent A 关心自己是否按时完成;
- Agent B 关心自己是否没撞到障碍物;
- 但没有指标检查“整个任务是否完成、是否满足所有资源约束”。
建议定义全局成功指标,例如“目标G达成”“所有共享资源在最终状态全部释放”“所有截止时间均满足”。这个全局指标应该被规划器、执行器、测试用例共同引用。
6.4 为失败设计快速回滚与局部重规划
多Agent系统一旦运行起来,不可能每次都从头重来。一个可行的做法是:
- 把整个任务拆分为多个子目标;
- 记录每个子目标的完成状态;
- 失败时只重规划失败子目标相关的Agent,其他Agent保持原位等待;
- 重规划时,把当前实际状态作为新初始状态,而不是使用最初的全局初始状态。
这样可以显著降低失败恢复成本。
6.5 从仿真走向生产:先加“计划校验层”
真实生产环境比仿真复杂得多。在把多Agent协同系统部署到真实环境之前,建议先增加一个独立的“计划校验层”。
这个校验层可以是一个独立服务,也可以是一段作为CI门禁的脚本。它只做一件事:在计划真正下发执行前,把所有Agent的局部计划合并成一个全局计划,然后检查:
- 是否存在未满足的前置条件;
- 是否存在共享资源超容量;
- 是否存在时间窗口冲突;
- 是否存在循环依赖。
如果校验不通过,就不允许计划下发。虽然这会增加一点计算开销,但能避免大量执行期事故。对于机器人调度、物流履约、多机配合这类场景,这个校验层的价值远大于成本。
回头再看文章开头那个仓库协作死锁问题,修复方案其实很清晰:在规划阶段给装配台加容量约束,并为“放零件A”和“放零件B”建立明确顺序,让A先完成组装、释放装配台,B再进入装配流程。至于EPC-AW这类方法论,本质上就是把这些“靠经验才能发现的问题”变成可自动检测、可自动诊断的标准流程。后续实践时,建议先从小规模的多Agent场景入手,把计划校验、执行监控、失败诊断三个环节都补齐,然后再逐步扩大Agent数量和任务复杂度。只要掌握了这套思路,遇到“每个动作都对,但系统失败”的问题,就不会再一头雾水了。