跳至主要内容
多 Agent 协作:拓扑、通信与终止

多 Agent 协作:拓扑、通信与终止

我做过一次很典型的过度设计:把「读需求 → 查代码 → 写实现 → 跑测试」拆成四个 Agent,各带各的 prompt,用一个群聊式的消息总线互相喊话。跑起来之后,测试 Agent 拿着实现 Agent 第三版的草稿去测,实现 Agent 又根据另一条消息改了回去,两个 Agent 来回改了七轮,谁也没说清楚「哪一版是当前版本」。单 Agent 加四个工具的版本,反而半小时就跑通了。

那次之后我对多 Agent 的默认态度是:先证明单 Agent 不够,再拆。

本系列第 14/15 篇 · 上一篇:Agent 记忆系统的读写与遗忘 · 下一篇:上线之后:可观测性、成本与护栏

拆分的真实理由只有四个

上下文隔离。一个子任务要读几万 token 的源码或日志,如果和主任务的决策上下文混在一起,会把关键约束挤出窗口。让子 Agent 单独读、只回传结构化结论,主 Agent 的上下文就干净了。这是我认为最站得住脚的理由。

权限隔离。检索公开库的 Agent 和执行写操作的 Agent 不该共享同一套工具权限。把高权限动作收进一个受严格约束的 Agent,能显著缩小注入攻击的爆炸半径(第 15 篇展开)。

角色专精与并行。互不依赖的子任务可以并发跑,各自用不同的模型档位和 prompt。规划用强模型、抽取用小模型、格式转换直接用代码,成本结构会比「一个强模型包办」合理。

独立评测与迭代。某个子任务的失败率明显高,拆出来之后可以单独建评测集、单独换模型、单独灰度。

反过来,下面这些不是拆分的理由:「架构看起来更清晰」(拆完消息流往往更乱)、「可以让模型自己讨论出更好答案」(多数时候只是互相强化同一个错误)、「方便以后扩展」(未来收益换现在成本,不划算)。

一个很实用的判断:如果把多个 Agent 之间的通信契约写清楚,你发现它其实就是「主 Agent 调几个工具」,那就别拆。多 Agent 的成本主要不在模型调用,在状态一致性。

四种拓扑

Supervisor(中心编排):一个主 Agent 负责分解、派发、汇总,子 Agent 无状态、只接受任务返回结果。和第 2 篇的规划层几乎同构,只是执行单元从工具换成了 Agent。最稳、最好调试,是我的默认选择。缺点是主 Agent 成为瓶颈,它的上下文会随着子结果回传变长。

层级式:supervisor 下面再挂 supervisor,适合任务本身有天然层级(按模块、按地域、按批次)。但层数一多,信息每上一层就压缩一次,原始证据容易丢。我限制在两层以内,并要求每层的汇总都带可追溯的 artifact_id。

流水线 / 串行:A 的产出是 B 的输入,固定顺序,无回环。可控性最好,延迟可预测,适合「抽取 → 校验 → 生成 → 审校」这类明确阶段。缺点是不能回退重做,前面错了后面全歪,所以每阶段之间要有显式的质量门禁。

群聊 / 黑板:多个 Agent 往共享空间写消息,谁都能读谁都能响应。最灵活,也最容易失控——消息顺序不确定、版本不清、容易出现互相覆盖和无限讨论。我只在探索性、允许人工介入的离线场景用过,不放在面向用户的链路上。如果一定要用,就把黑板做成只追加的事件日志,而不是可覆盖的共享变量。

拓扑 一致性 并行 可调试性 适合
Supervisor 高 中 高 大多数生产场景
层级 中 高 中 大规模分治
流水线 高 低 高 阶段固定的批处理
群聊 / 黑板 低 高 低 离线探索、需人工裁决

通信契约:消息不是自然语言

开头那个失败案例的根因,就是消息里没有版本概念。现在我把 Agent 间通信定义成结构化记录,且只传产物句柄,不传大段文本:

JSON13 行
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"msg_id": "m_0187",
"task_id": "t_8f31",
"from": "impl",
"to": "supervisor",
"in_reply_to": "m_0162",
"artifact": {"id": "art_44", "uri": "store://t_8f31/impl/v3", "schema": "patch_v1"},
"based_on": {"artifact_id": "art_41", "version": 2},
"status": "done",
"checks": [{"name": "tests_pass", "value": true}],
"confidence": 0.86,
"budget_used": {"tokens": 9200, "seconds": 34}
}

几个字段是专门为了防坑加的:

based_on 声明这条产物基于哪个版本。测试 Agent 拿着 art_41/v2 去测、实现 Agent 已经产出 art_44/v3 时,supervisor 能立刻判定结果过期,不需要靠模型在对话里「说清楚」。

schema 强制产物是结构化且可校验的。第 2 篇的 done_when 在这里变成跨 Agent 的验收条件;产物不符合 schema 就直接打回,别让下游 Agent 去猜一段自由文本什么意思。

checks 把判断变成布尔值。让一个 Agent 用自然语言评价另一个 Agent 的产出「质量不错」,是多数互相强化错误的起点。

in_reply_to 和 msg_id 用来构建因果链,是跨 Agent trace 的基础(第 15 篇)。

消息传递还是共享状态? 我的选择是:短指令和结果走消息,长期事实走共享状态,但共享状态必须带版本和写入者。第 13 篇讲的 expected_version 乐观锁在这里同样适用。绝不允许两个 Agent 直接覆盖同一个字段——所有写入经过一个带校验的 state.apply(patch, expected_version),冲突就显式合并。

还有一条纪律:子 Agent 不直接看主 Agent 的完整对话历史。给它的上下文只包含任务描述、必要约束、以及它需要的产物。上下文隔离本来就是拆分的目的,把历史全传进去等于白拆。

终止条件:多 Agent 最容易失控的地方

单 Agent 有步数上限就够了,多 Agent 多了三类不收敛:互相等待(死锁)、来回否定(振荡)、以及不断生成新任务(任务膨胀)。所以终止条件要在系统层定义,不能交给任何一个 Agent 自己判断。

PYTHON18 行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def supervise(task, budget):
state = SharedState(task, version=0)
for round_no in range(budget.max_rounds):
pending = plan_next(state) # 返回可并发的子任务
if not pending:
return finish(state)
results = run_parallel(pending, timeout=budget.round_timeout)
for r in results:
if not r.valid or r.based_on.version != state.version:
state.mark_stale(r) # 过期结果不写回
continue
state.apply(r.patch, expected_version=state.version)
state.version += 1
if state.unchanged(lookback=2): # 连续两轮无有效状态变化
return halt(state, reason="plateau")
if budget.exhausted() or state.conflict_unresolved():
return halt(state, reason="budget_or_conflict", need_human=True)
return halt(state, reason="round_limit", need_human=True)

四个判据缺一不可:

  • 轮次上限(我一般 3–6 轮)与全局预算(总 token、总墙钟、总金额);
  • 平台检测:连续 N 轮共享状态没有实质变化就停,这是治振荡最有效的一条;
  • 收敛判据:把「做完了」写成对 required_fields 的布尔检查,而不是让某个 Agent 宣布完成;
  • 冲突升级:两个 Agent 给出互斥结论且仲裁无法判定时,停下来交人,不要让它们继续讨论。模型之间的辩论在缺乏外部证据时,往往只是在互相强化初始偏差。

need_human=True 的出口必须真实存在。我见过太多系统「理论上会交人工」,实际上 halt 之后直接返回了一段最像结论的文本,等于把兜底也自动化掉了。

成本模型与观测

多 Agent 的成本很容易失控,因为它不是线性叠加:

TEXT1 行
1
总成本 ≈ Σ(每个 Agent 的轮次 × 单轮上下文 token) + 协调开销 + 重复劳动

一个常被低估的项是每个子 Agent 都要带一份系统指令、工具定义和约束说明。四个 Agent 就是四份固定开销。再加上 supervisor 每轮要把子结果重新读一遍,同一份材料可能被编码三四次。

控制手段和第 3 篇一脉相承:按角色动态裁剪工具集;给不同 Agent 配不同档位的模型(规划用强模型,抽取和格式校验用小模型甚至纯代码);产物只传句柄不传全文;对高频子任务做结果缓存。

观测上,跨 Agent 的 trace 必须能用 task_id + msg_id 串成一张因果图。至少要能回答:哪个 Agent 在第几轮基于哪个版本做了什么、产出了什么、被谁消费了。没有这张图,多 Agent 的调试基本靠读聊天记录,而聊天记录恰恰是最不可信的——它不记录「哪个版本生效」。

优劣与边界

收益:长材料隔离后主决策上下文更干净;权限可以按角色收缩;独立子任务可并行、可用不同模型档位;子任务能单独评测和灰度;失败可以被限制在一个分支里,不必整条重跑。

代价:状态一致性成为一等难题(版本、过期、并发写);协调开销和重复编码让成本可能翻倍;失败模式变多,尤其是振荡和任务膨胀;调试难度陡增;端到端延迟由最慢的分支决定,长尾明显。

什么时候别拆:任务步骤少、上下文装得下、子任务之间强依赖且需要频繁来回调整、延迟预算严格、以及团队还没有稳定的 trace 和评测体系时。我的经验是,没有跨 Agent trace 就不要上多 Agent,因为出了问题你连复现都做不到。

还有一种更省事的中间形态值得提:不是多个「Agent」,而是同一个 Agent 切换多套「角色配置 + 工具集 + 独立上下文窗口」,由代码显式调度。它拿到了上下文隔离和权限隔离的主要收益,却没有共享状态和消息协议的一致性负担。多数我一度想拆成多 Agent 的需求,最后停在了这个形态。

系列写到这里,单个 Agent、RAG 链路、融合架构、记忆与协作都齐了。剩下的是把它们放进生产环境之后的事:怎么知道它现在是不是正在悄悄变差,怎么把成本和延迟按在预算里,以及检索到的外部内容本身可能带着恶意指令时该怎么办。你现在的协作流程里,能不能一眼看出某个结论是基于哪一版产物得出的?

关于本文

由 Lu's Blog 撰写,采用 CC BY-NC 4.0 许可协议。

#Agent #多智能体 #架构

评论