多智能体协作的状态机陷阱:一次真实踩坑与三条修正原则
一、背景:Skill 文档其实是程序
用 WorkSwarm、JiuwenSwarm 这类多智能体框架编排业务时,真正决定系统行为的往往不是代码,而是每个 Agent 的 Skill 文档(SKILL.md)。它规定了角色、状态、信号、交接步骤——本质上就是一份用自然语言写的状态机。
问题在于:自然语言写的状态机没有编译器。它看起来自洽,跑起来却可能互相矛盾。
二、现场:一条规则和一条步骤对不上
在一套「AI 面试天团」的多 Agent 项目里,总控 Agent(orchestrator)负责串起破冰 → 技术面 → HR 面 → 生成报告四个阶段。文档里有两处关于状态的描述:
第一处,状态迁移规则:
You can only transition to TECH_ACTIVE from CHAT_ACTIVE
after receiving CONTROL_RETURN
第二处,破冰阶段(Phase 2)的执行步骤:
Step 7: After receiving CONTROL_RETURN, confirm:
"CONTROL_RECEIVED|GENERAL_CHAT_OFFICER"
Step 8: Set state = IDLE
把这两段放在一起读就会发现:破冰正常结束后,状态被置成了 IDLE;而进入技术面试的前提是当前状态为 CHAT_ACTIVE。按字面执行,Phase 3 的迁移前提永远不成立。
三、影响:看起来只是笔误,实际是分叉的失败模式
这类矛盾不会报错,它会让系统走向两条都不好的路:
- 严格派:Agent 忠实执行迁移校验,于是卡在 Phase 2 和 Phase 3 之间,表现为反复确认状态、或者干脆停住等一个永远不会来的合法状态。
- 宽松派:Agent 觉得「反正该往下走了」,直接进入 Phase 3。这一次是通的,但意味着「严格单播 + 严格交接」的约束在第一次真实交接时就被绕过了——后面的约束也就没有约束力了。
更麻烦的是不可复现:同一个 Skill,不同模型、不同温度、不同上下文长度下表现不同。你很难判断是模型能力问题,还是文档本身有洞。
四、两种修法
方案 A(推荐):状态只在交接完成时改变。
Phase 2 收到 CONTROL_RETURN 后不要置 IDLE,保持 CHAT_ACTIVE;由 Phase 3 完成 CHAT_ACTIVE → TECH_ACTIVE 的迁移后再释放。这样「当前状态」始终代表「谁在说话」,语义不会漂移。
方案 B:放宽迁移表,但要求携带凭证。
把规则改成「IDLE 或 CHAT_ACTIVE 都可进入 TECH_ACTIVE,但必须持有对应的 CONTROL_RETURN 凭证」,并在状态表里补一行 IDLE → TECH_ACTIVE。适合已经有多处赋值的存量文档,改动最小。
五、附带发现:信号协议单行与多行混用
同一份协议里,破冰的返回信号是单行管道格式:
CONTROL_RETURN|GENERAL_CHAT_OFFICER|STATUS_READY
而技术面试、HR 面试的返回信号写成了两行:
CONTROL_RETURN
TECHNICAL_SCORING_DATA|...
逐行匹配的实现很容易把第二行当成一条独立消息,从而丢掉评分数据——而且丢得很安静,日志里看不出异常。建议统一成单行,或者显式规定「多行消息以 CONTROL_RETURN 行开始、以空行结束」。
六、三条修正原则
1)状态机只留一个事实来源。
状态定义、迁移表、各阶段里的赋值动作,三者必须能互相对上。最省事的做法是在文档里放一张迁移表,每个 Phase 只写「执行迁移表第 N 行」——不要在正文里再散落 Set state = ...,散落一处就多一处漂移机会。
2)交接协议要机器可判定。
固定单行、固定字段顺序、带序号或 nonce;不要用「等待对方说完」这种自然语言描述等待条件。能被正则匹配的信号,比能被理解的话更可靠。
3)Skill 也要走查,至少要跑一遍。
文档上线前,把状态机从头到尾人工推演一遍:每个 Phase 的入口状态、出口状态、失败兜底,逐个对。十分钟的走查,能挡掉绝大多数「看起来合理」的矛盾。
七、小结
多智能体系统的稳定性,很大一部分不取决于模型多强,而取决于喂给它的流程文档有多严谨。把 Skill 当代码写——有状态、有迁移、有协议、有走查——比反复调 prompt 更有效。
这个问题已提交到开源仓库(issue #772),文中方案欢迎补充更好的做法。
- 点赞
- 收藏
- 关注作者
评论(0)