笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • AI / 大模型

    • AI / 大模型
    • 大模型核心原理
    • 推理与微调优化
    • RAG 检索增强生成
    • RAG 上下文剪枝实战(Listwise Pruning 复现)
    • RAG 数据清理
    • RAG 存量数据清理
    • Agent 开发
    • Agent 运行时深水区
    • Multi-Agent 协作
    • Agent 评测与线上运营
    • AI 与游戏研发周期
    • 互动影游与长视频创作 Agent
    • LLM 应用安全
    • LLM 评测方法论
    • LLM 成本与延迟
    • 微调策略
    • AI 研发工程化

Multi-Agent 系统:拓扑、通信与结果不一致的裁决

五种协作拓扑 · 三层通信机制 · 冲突消解四级阶梯 · MAS 特有失效

🧠 一句话记忆锚点

MAS 的难点从来不是"让多个 Agent 跑起来",是"它们说的话不一致时听谁的"。全篇两个正交维度:协作拓扑(谁指挥谁)× 通信机制(信息怎么流),冲突时以一致性要求为先——强一致共享状态必须走黑板+版本号,不能走广播。冲突消解是四级有序阶梯:先分类(事实/口径/时序/幻觉),再走证据优先 → 置信度加权 → 第三方仲裁 → 人工闸门;投票不在阶梯里,因为 2 个 Agent 平局无解、同源模型投票不独立。裁决必须在主 Agent 装配上下文之前完成,否则就是上下文中毒。MAS 的固定代价是 token O(N²) 与调试面积膨胀,所以先问"能不能不上"。

阅读边界

本篇是 多 Agent 协作的单一事实源:拓扑定义、通信机制、冲突消解协议、MAS 特有失效、任务分派与结果聚合。

单 Agent 的调度状态机、分层记忆读写时机、工具沙箱、并发/预算/幂等实现,全部归 Agent 运行时深水区——那篇的结论以「单机单 Agent」为规模前提,本篇接在它之后。Golden Set 构造、成功率分母口径、轨迹级 judge、span 树归因、影子/灰度/回滚判据归 Agent 评测与线上运营。单 Agent 的范式对照(ReAct / Plan-and-Execute / Reflexion / ToT)与框架选型结论归 Agent 开发。

本篇主骨架是决策矩阵轴:「协作拓扑 × 通信机制」两维对照,每格扎穿一层理由。

名词速查

首次阅读可跳过本表,直接看「场景问题」,遇到生词再回查。

本篇是多 Agent 协作这一组名词的归属页。这一组词对后台工程师格外友好——它本质上是分布式系统那套问题在 Agent 上重演一遍:服务编排、消息中间件、共享状态一致性、脑裂裁决、级联故障,你都见过。区别在于每个"节点"都会自信地胡说八道。

篇幅较长,按 spec 拆成两张表:本篇自有名词(完整版)与引用自其他上下文(只给去处)。

本篇自有名词

名词后台类比在系统里干什么类比失效边界
MAS(多 Agent 系统)微服务集群——把单体拆成多个各司其职的服务用多个专职 Agent 分工协作完成单个 Agent 难以胜任的复杂任务微服务拆分后各服务的接口契约是精确的,联调可穷举;MAS 的"接口"是自然语言,上游输出的措辞变化会改变下游行为,没有 schema 能约束住。而且拆分带来的固定代价是 token O(N²) 与调试面积膨胀——所以第一个问题永远是"能不能不拆"
协作拓扑(Topology)服务调用关系图——星型(网关放射)、链式(管道)、网状(互相调用)等描述"谁指挥谁"的组织结构:主从 / 流水线 / 对等 / 分层 / 群体。决定了任务怎么下发、结果怎么回收服务拓扑由部署与配置决定,运行时固定可绘图;Agent 拓扑里主从关系是靠 prompt 约定的——下游 Agent 完全可以"不服从"(忽略指令、越权行动、反过来给主 Agent 派活)。拓扑是纸面约定,没有基础设施层面的强制力
黑板(Blackboard)共享数据库 / 带版本号的共享状态——所有参与方读写同一份权威数据所有 Agent 读写同一份共享状态。强一致要求的场景必须走这条,配版本号做乐观锁共享库有事务、隔离级别、约束检查兜底;黑板上写的是自然语言事实,两个 Agent 可以写入语义矛盾但格式都合法的内容("库存充足" vs "库存不足"),数据库层面查不出冲突。冲突只能靠本篇的裁决阶梯在应用层消解
消息总线(Message Bus)Kafka / MQ 的发布订阅——异步解耦,各方按需消费Agent 间异步传递消息,发送方不阻塞、接收方按需处理,适合松耦合的并行协作MQ 保证消息不丢、可重放、顺序可控;这里消息内容是模型生成的自然语言——重放同一条消息,接收方的处理结果可能不同。而且"消息被正确理解"没有 ack 可以确认,投递成功 ≠ 语义送达
共享状态(Shared State)全局配置中心 / 分布式 KV——多方可见的一份状态三层通信机制里最直接的一层:把状态放在公共位置供所有 Agent 读取配置中心的读写语义明确,最终一致有明确收敛时间;这里的风险是上下文分裂——各 Agent 读到状态的不同版本后各自推理,产出互相矛盾的结论,且没有任何一方察觉自己读到的是旧的
任务分派(Task Dispatch)任务队列 + 负载均衡——按能力和负载把活派下去主 Agent 把大任务拆解成子任务,按各下游 Agent 的专长分配负载均衡按可测量的负载与健康度决策,派错了可以重新调度;这里的拆解由模型完成,拆解本身就可能是错的(漏了必要子任务、拆出重叠子任务、拆出无法完成的子任务),而下游会照着错误的拆解认真干完。错误在最上游就固化了
结果聚合(Aggregation)多路查询结果归并——收齐各分片结果后合并返回收集各下游 Agent 的产出,合并成最终答案分片归并有明确的合并规则(union / sum / 排序),结果确定;这里各方产出可能互相矛盾,"合并"首先是裁决(听谁的),其次才是拼装。跳过裁决直接拼接,就是把矛盾原样塞进主 Agent 的上下文——即上下文中毒
冲突消解四级阶梯故障处置的分级升级流程(自动恢复 → 加权决策 → 仲裁 → 人工介入)结论不一致时的有序处置:先分类(事实/口径/时序/幻觉),再依次走证据优先 → 置信度加权 → 第三方仲裁 → 人工闸门分级升级的每一级都有客观判据决定是否升级;这里"证据是否更强""置信度是否可信"都需要判断,而做判断的往往还是模型。所以阶梯的价值在于强制顺序(先找证据,别急着投票),而不在于每级都能给出正确答案
证据优先(Evidence First)以日志/审计记录为准——有原始凭证的一方胜阶梯第一级:谁能给出可验证的原始出处(检索到的原文、工具返回值),就采信谁日志是不可篡改的客观记录;Agent 给出的"证据"可能是它编的(伪造引用、虚构工具返回)。所以这一级必须校验证据真实可溯源(原文能在库里查到、工具调用有对应 span),而不是看谁"给了证据"
置信度加权(Confidence Weighting)按副本可信度加权投票——像 Raft 里不同角色的权重差异阶梯第二级:按各 Agent 的历史准确率或自报置信度加权Raft 的权重由协议定义、语义明确;模型自报的置信度普遍过高且校准很差——它说 95% 确信时实际准确率可能只有 70%。所以只能用外部统计出的历史准确率做权重,绝不能直接采信自报值
第三方仲裁(Arbitration)引入独立的第三方校验服务打破平局阶梯第三级:让一个未参与争议的 Agent(最好是不同底座模型)来判独立校验服务的逻辑与被校验方无关,判断真正独立;这里若仲裁者与争议方是同一个底座模型,它们会犯相同的错误——看起来独立,实际高度相关。这也是"投票"不在阶梯里的原因:同源模型投票不独立,且 2 个 Agent 平局无解
人工闸门(Human Gate)人工审批节点——自动化流程里的最后一道阶梯第四级:不可逆操作或裁决不下的场景转人工审批流的待办有明确的 SLA 与责任人;Agent 转人工时,人拿到的是一大段自然语言争议,缺少结构化的决策依据,判断成本很高。所以转人工时必须同时给出"争议点 + 双方证据 + 建议",否则这道闸门会成为吞吐瓶颈
上下文中毒(Context Poisoning)脏数据写入主库,后续所有读取都受污染未经裁决的矛盾结论或错误事实进入主 Agent 上下文后,污染其后所有推理脏数据可以定位、修正、回滚;进了上下文的错误事实无法"删掉"——即使后续追加一句"刚才那条是错的",模型仍可能继续沿用。所以裁决必须在装配上下文之前完成,这是时序要求而非质量要求
责任扩散(Diffusion of Responsibility)多个服务都以为对方做了校验,结果谁都没做MAS 特有失效:每个 Agent 都假定别人会处理某个环节,该环节实际无人负责服务间的职责可以靠接口契约与集成测试钉死;Agent 的职责是 prompt 里描述的,边界天生模糊,且它们不会像人一样确认"这块归谁"。必须显式给每个子任务指定唯一负责 Agent,并对产出做外部断言
级联幻觉(Cascading Hallucination)级联故障——上游的错误被下游放大,最终雪崩上游 Agent 编造的事实被下游当作输入前提,逐层加工后变成一份"看起来论证充分"的错误结论级联故障有熔断和降级可以阻断,且故障信号明确(超时、错误码);幻觉级联没有任何错误信号——每一层的输出都格式合法、语气自信,越往下游越像真的。只能靠每层的事实性断言拦,不能靠链路层面的熔断
MAS 死锁分布式死锁——各方互相等待对方先行两个 Agent 互相等待对方给出前提("你先确认 A""你先确认 B"),或互相把任务推给对方数据库死锁有检测器与自动牺牲者回滚;这里没有全局等待图可查——Agent 的"等待"表现为不断产出礼貌的追问,看起来是在正常工作。只能靠全局轮数与预算硬闸兜住,见 Agent 运行时深水区
token O(N²)全连接广播的通信开销随节点数平方增长MAS 的固定成本:N 个 Agent 互相传递上下文,token 消耗随 N 平方膨胀网络广播的开销可以靠拓扑优化(树形、gossip)降到 O(N log N);这里降不下来的部分是——每个 Agent 都需要足够的上下文才能正确工作,砍通信就等于让它们瞎干。这是 MAS 的准入门槛:省不掉,只能评估值不值

本篇引用的其他上下文名词

名词在本篇里的角色完整解释去哪读
调度状态机 / 终止线 / 分层记忆 / 压缩水位 / 工具沙箱 / 预算熔断 / 幂等键单 Agent 运行时实现;本篇只在"压缩窗口归属"一处与它交接Agent 运行时深水区 · 名词速查
ReAct / Plan-and-Execute / Function Calling / MCP每个 Agent 内部用的范式与协议Agent 开发 · 名词速查
span 树归因 / 成功率分母 / 轨迹级 judge多 Agent 轨迹的可观测与判分Agent 评测与线上运营 · 名词速查
context(本轮装配结果)/ memory(分层记忆)上下文分裂与中毒涉及的两个概念,与推理侧同名异义Agent 运行时深水区 · 名词速查
token 经济 / 预算护栏O(N²) 成本的计价口径成本与延迟 · 名词速查
六角色 DAG / 长程一致性MAS 在创作场景的落地实例互动影游与长视频创作 Agent · 名词速查

场景问题

一个内容审核流程:主 Agent A 收到一条待审内容,分别问两个下游——B(规则检索 Agent,查审核规则库)和 C(案例检索 Agent,查历史判例)。

B 回来说:「该内容违反 3.2 条,应下架。」
C 回来说:「历史上 4 起同类内容全部判为『通过,加提示标签』。」

A 现在拿着两个矛盾的结论。它做了最自然的事——把两段话都塞进自己的上下文,然后问模型「综合判断」。模型输出:「建议下架并加提示标签。」

这个输出是荒谬的(下架了还加什么标签),但它没报错、格式合法、看起来还挺全面。这就是 MAS 最典型的坏法:冲突没被消解,只被拼接了。

再往下看一层,问题更细:

  • B 说的「3.2 条」和 C 说的「同类内容」,是不是真的同一类?没人验证过。
  • C 说的「4 起」是什么时间范围?如果规则 3.2 是上个月新增的,那 4 起判例全部作废——这是时序冲突,不是事实冲突,两者的处理方式完全不同。
  • 假如再叫来一个 D 投票?D 和 B 都是同一个模型跑的,会犯同向错误。投票只会让错的那边变成 2:1。

打个比方

MAS 像一个跨部门评审会。Supervisor 拓扑 = 有个主持人点名发言、汇总决议;群聊拓扑 = 大家在一个会议室自由讨论;黑板拓扑 = 不开会,各自往同一份共享文档里写,看别人写了什么再补。

类比在哪失效:真人开会时,B 和 C 发现结论矛盾会当场互相追问("你说的同类是指哪几起?"),冲突在会议室里就消解了。Agent 不会——B 和 C 各自只对 A 说话,它们看不到对方的结论,也没有动机去核对。所以 MAS 里的冲突消解不是"让它们聊聊",而必须是框架层的一道显式裁决工序:由 A 或一个独立仲裁器在收到全部回复后主动执行。指望 Agent 自己发现矛盾,等于指望没有议程的会议自己得出决议。

这篇要回答的就是:拓扑怎么选、信息怎么流、结论冲突时听谁的。

实现方案

维度一:五种协作拓扑

拓扑决定「谁能指挥谁、谁等谁」。五种,按工业实践频率排序:

拓扑一句话适用场景Agent 数实际上限典型失效token 成本量级
Supervisor / 中心调度一个 Planner 分派给多个 Worker,Worker 只对 Planner 说话子任务边界清晰、需要统一验收3~8中心瓶颈:Planner 上下文塞满所有 Worker 的回复O(N),最省
Pipeline / 流水线上一个的输出即下一个的输入,单向阶段固定、每阶段产物明确(剧本→分镜→合成)3~10错误逐级放大,且下游无法要求上游返工O(N),最省
Group Chat / 群聊多 Agent 共享一条消息流,轮转发言需要互相质疑的场景(方案评审、红蓝对抗)2~5发言权争夺、跑题、token O(N²)O(N²),最贵
Blackboard / 黑板不直接对话,各自读写共享状态区长任务、需要共享大量结构化状态5~20写覆盖、读到中间态、状态膨胀O(N),但状态区本身可能很大
Contract Net / 市场竞标任务广播 → Agent 出价 → 择优派单Agent 能力异构且事先不确定谁最合适理论无上限出价不可信(模型自评能力普遍高估)O(N) 广播 + O(N) 出价

规模锚点:上表的「实际上限」是指单任务内同时活跃的 Agent 数,前提是每个 Agent 有独立上下文窗口。超过上限时的信号不是崩溃,而是「Planner 的上下文里 Worker 回复占比 > 60%」——此时 Planner 已经在做摘要工作而不是决策工作。

市场竞标:为什么工业界少用

Contract Net 在多智能体学术文献里是经典拓扑(源自 1980 年代的分布式问题求解),但工业 Agent 系统里罕见,原因很实在:出价机制依赖 Agent 对自身能力的准确自评,而 LLM 的自评是系统性高估的——你问它"你能不能处理这个任务,置信度多少",它几乎总说能。于是竞标退化成随机派单,还多付了一轮广播 + 出价的 token。

它真正成立的前提是出价可被客观校验(比如 Agent 报的是"我有这个工具的访问权限"这类硬事实,而非"我擅长这个")。把出价从主观置信度换成能力声明的静态匹配,它就退化成了下面的「能力路由」——那就直接用能力路由,省掉竞标这一轮。

三种主要拓扑的消息流向对比:

维度二:三层通信机制

打个比方:这三层就是你搭微服务时选的那三种通信方式——RPC 直连(点对点,知道对方是谁)、Kafka/消息队列(发出去就不管,谁订阅谁消费)、共享数据库/Redis(不互相说话,都往同一份状态里读写)。耦合度从高到低,可观测性也从高到低。类比失效边界:微服务之间传的是结构化 payload,接收方按 schema 解析,传多少就是多少;Agent 之间传的消息要进对方的上下文,于是有两件事变了——一是它会占预算且随 Agent 数平方增长,二是接收方是模型,它会对同一段文本做自己的解读,传过去的不一定是收到的。所以这里的选型压力不只是耦合度,更是 token 与语义保真度。

拓扑说的是「谁跟谁」,通信机制说的是「怎么传」。三层,从松到紧:

1. 消息传递

三种寻址方式,耦合代价递减:

寻址语义耦合代价用在哪
直接寻址A 明确指定发给 B最高:A 必须知道 B 存在、知道 B 的能力Supervisor 派单、Pipeline 交接
广播发给所有人,各自决定要不要理中:发送方不知道谁在听任务招标、全局状态变更通知
发布订阅按 topic 投递,订阅方自行注册最低:双方都不知道对方事件驱动的松耦合系统

消息传递的最大陷阱:token O(N²)

只要 Agent 间的消息进入彼此的上下文,token 消耗就随 Agent 数平方增长——N 个 Agent 每人都要读其余 N−1 个人的输出。5 个 Agent 的群聊,第 10 轮时每个 Agent 的上下文里有 45 条别人的发言。

这是 Group Chat 拓扑最贵的地方,也是大多数 MAS demo 在真实任务上跑爆预算的原因。修法只有两条:要么改拓扑(Supervisor 让 Worker 互不可见,降到 O(N)),要么改传递内容(互传摘要与结论而非完整轨迹——但这会引入摘要漂移,见 运行时 · 三种失效)。没有第三条。

2. 共享状态(黑板)

黑板不是"一个 dict"。它是一个带并发语义的存储,最少要有三样东西:

// 黑板:结构化共享状态,非自由文本。
// 关键在 Version —— 没有它,两个 Agent 的并发写必然丢一个。
type Blackboard struct {
    mu      sync.RWMutex
    entries map[string]Entry
}

type Entry struct {
    Key     string
    Value   any
    Version int64  // 乐观锁:写入时必须带上读到的版本
    Author  string // 谁写的 —— 冲突归因时唯一的线索
    WroteAt time.Time
}

var ErrStaleWrite = errors.New("blackboard: stale write, re-read and retry")

// CompareAndSwap:读到 v3 就必须以 v3 提交,否则拒绝。
// 拒绝后 Agent 必须重读 —— 而不是覆盖。
func (b *Blackboard) CAS(key string, expectVer int64, val any, author string) error {
    b.mu.Lock()
    defer b.mu.Unlock()

    cur, ok := b.entries[key]
    if ok && cur.Version != expectVer {
        return fmt.Errorf("%w: key=%s want=%d got=%d (held by %s)",
            ErrStaleWrite, key, expectVer, cur.Version, cur.Author)
    }
    b.entries[key] = Entry{
        Key: key, Value: val, Version: expectVer + 1,
        Author: author, WroteAt: time.Now(),
    }
    return nil
}

三个必须有的性质:

  • 版本号 / 乐观锁:没有它,两个 Agent 并发写同一 key 必然丢一个写,且不会报错——这是 MAS 最难查的 bug 之一。
  • Author 字段:冲突发生后要归因「这个结论是谁写的」,没有 author 就只能靠猜。
  • 结构化而非自由文本:黑板存 {"protagonist": {"name": "林", "age": 28}},不存「主角叫林,28 岁」。自由文本的黑板等于让每个 Agent 重新做一次信息抽取,抽取偏差会累积。

3. 控制权交接(Handoff)

交接是"我做不了,你来",与派单的区别在于控制权真的转移了——交出去之后自己不再等待结果。

交接时的关键决策是带什么、不带什么:

带不带为什么
任务目标与硬约束上游的完整推理轨迹轨迹会把上游的错误假设一起传下去,且爆 token
已确认的结构化事实上游的中间猜测猜测传下去会被下游当作既定事实(级联幻觉的主要入口)
交接令牌(见下)上游的凭据凭据只该由工具持有,见 运行时 · 凭据句柄化

交接必须幂等,因为交接消息可能重发(网络重试、调度器崩溃恢复、上游超时后误判失败而重发)。如果不幂等,同一个子任务会被下游执行两遍——如果它带副作用(发一条消息、扣一次费),就是真实事故。

令牌三段构成 task_id + from + seq,缺一不可:

// HandoffToken:三段缺一不可。
//   task_id 定位是哪个任务(跨任务的同名子任务不能互相去重)
//   from    定位是谁交接的(同一任务里 B→D 和 C→D 是两次不同交接)
//   seq     定位是第几次(同一对 Agent 之间可能多次交接)
type HandoffToken struct {
    TaskID string
    From   string
    Seq    int
}

func (t HandoffToken) Key() string {
    return fmt.Sprintf("%s/%s/%d", t.TaskID, t.From, t.Seq)
}

type HandoffGuard struct {
    mu   sync.Mutex
    seen map[string]Result // 已执行过的交接 → 上次的结果
}

// Accept:重复交接直接返回上次结果,不重新执行。
func (g *HandoffGuard) Accept(t HandoffToken, exec func() Result) Result {
    g.mu.Lock()
    if prev, dup := g.seen[t.Key()]; dup {
        g.mu.Unlock()
        return prev // 幂等:副作用只发生一次
    }
    g.mu.Unlock()

    r := exec()

    g.mu.Lock()
    g.seen[t.Key()] = r
    g.mu.Unlock()
    return r
}

三段里最容易被漏掉的是 from:只用 task_id + seq 时,同一任务里 B→D 的第 1 次交接和 C→D 的第 1 次交接会撞成同一个 key,第二次被当成重复而静默跳过——一个子任务凭空消失,且日志上看不出来。

这一节记住三句话

  1. 只要 Agent 间的消息进了彼此的上下文,token 就是 O(N²)——修法只有改拓扑(让 Worker 互不可见)或改传递内容(只传结论摘要),没有第三条。
  2. 黑板不是一个 dict,它必须有版本号(否则并发写静默丢一个)、author(否则冲突无法归因)、结构化字段(否则每个 Agent 重做一次信息抽取,偏差累积)。
  3. 交接要带结论、不带轨迹,且必须幂等。令牌三段 task_id + from + seq 缺一不可——漏掉 from,两个上游对同一下游的交接会撞 key,一个子任务凭空消失且无日志。

矩阵:拓扑 × 通信机制

这是本篇的主骨架。两维不是自由组合——同一拓扑配不同通信机制,失效模式完全不同。只填工业实践里真会遇到的格:

消息传递共享状态(黑板)交接 Handoff
Supervisor✅ 最常见。Planner 直接寻址派单,Worker 回复。
失效点:Planner 上下文被 Worker 回复挤满,退化成摘要机器。修法:Worker 回复走结构化 schema 而非自由文本,Planner 只读字段不读散文。
✅ 长任务标配。派单走消息,产物落黑板。
失效点:两个 Worker 并发写同一 key 丢写。修法:CAS + 按子任务划分 key 命名空间,从设计上避免共写。
⚠️ 反模式。Supervisor 交出控制权就不再是 Supervisor 了,验收环节凭空消失。除非是显式的"升级到另一个 Supervisor"。
Pipeline✅ 天然匹配。上游直接寻址下游。
失效点:下游发现上游产物有问题,但单向流没有回退边。修法:给每级一个 reject(reason) 反向通道,或在级间插评审 Agent。
⚠️ 少见。Pipeline 的产物本就是逐级传递的,再加黑板等于两套真相,容易不同步。仅当需要跨级共享设定(如创作流水线的 Story Bible)时才加,且黑板只读不写。✅ 就是 Pipeline 的本质——每级交接控制权。令牌的 seq 即级号。
Group Chat✅ 定义上就是消息传递(广播到同一条流)。
失效点:token O(N²) + 跑题 + 发言权争夺。修法:硬限最大轮数、指定主持人决定发言顺序、每轮强制产出结构化结论而非自由讨论。
⚠️ 冗余。既然人人互见消息流,黑板提供不了额外信息,只增加一套状态。❌ 语义冲突。群聊里没有"控制权"这个概念可交,人人都在说话。
Blackboard⚠️ 冲突组合,见下方裁决。✅ 定义上就是共享状态。
失效点:读到中间态(另一个 Agent 写了一半)、状态无限膨胀。修法:写入原子化(要么整条 entry 可见要么不可见)+ 给 entry 加 TTL 或按任务归档。
✅ 可组合。交接时不传数据,只传"去读黑板的哪个 key"——这是最省 token 的交接形态。
Contract Net✅ 竞标本身就是广播 + 定向回价。⚠️ 少见。✅ 中标即交接。

空缺的格子(❌ / ⚠️ 标注)不是没想到,是这些组合在语义上冗余或自相矛盾——上表已逐格给出理由。

维度冲突时的裁决规则

规则:以一致性要求为先。需要强一致的共享状态,就不能用广播消息传递,必须走黑板 + 版本号。

具体冲突就在上表 Blackboard × 消息传递 那一格。看一个例子——三个 Agent 协作写一份剧本,需要共享「主角当前年龄」这个设定:

  • 走广播消息:Agent X 广播「主角改成 32 岁」。问题是广播没有版本语义——如果 Y 在收到广播前已经基于 28 岁写了一段,它的输出就是脏的,而且没有任何机制能发现。更糟的是,如果 Y 和 Z 同时各广播一个新年龄,最终状态取决于消息到达顺序,不可复现。
  • 走黑板 + CAS:X 读到 age@v3,以 v3 提交 32。Y 同时也想改,它读到的是 v3,提交时被 ErrStaleWrite 拒绝,被迫重读拿到 v4=32,再决定要不要改。冲突从"静默丢写"变成"显式报错"——这就是全部的收益。

对照另一个方向的冲突,Supervisor × 黑板 vs Supervisor × 消息传递:

Supervisor + 消息传递Supervisor + 黑板
主要风险中心瓶颈:所有 Worker 回复进 Planner 上下文状态竞争:Worker 并发写同一 key
风险性质会爆窗、会变慢,但看得见静默丢写,看不见
怎么选Worker 产物小、需 Planner 逐个判断 → 选它Worker 产物大、Planner 只需知道"完成了" → 选它,但必须上 CAS

一句话记法:消息传递的失效是吵(吵到爆窗),黑板的失效是静(静悄悄丢数据)。选黑板就必须付版本号这笔钱。

冲突消解:可判定的四级裁决阶梯

打个比方:这套阶梯就是你值班时的故障定级与升级流程——P3 先让自动化重试一把(看能不能自己好)、P2 值班的人查日志判一下、P1 拉群把相关方叫齐、P0 直接叫老板决策。每一级都有明确的进入判据,而且不允许跳级:不是因为流程官僚,而是因为越高级别越贵,能在低级别解决的问题上升一级就是浪费。类比失效边界:故障定级的输入是客观信号(错误率、影响面),谁都能算出同一个级别;这里的输入是两个 Agent 各自的说法,而其中一方可能在编造依据。所以这套阶梯的第 1 级不是"判断谁更可信",而是先去外部找证据把编造的那方直接踢掉——少了这一步,后面所有级别都是在两份可能都不可信的材料之间做选择。

回到开篇的场景。B 和 C 的结论矛盾,A 该怎么办。

这道题最常见的错答法是给一串并列选项:「可以投票、可以让它们再讨论一轮、可以交给人」。并列清单不是答案,因为它没回答"什么时候用哪个"。正确的结构是有序阶梯 + 每级的进入判据。

第 0 级:先分类,再选手段

不分类就选手段,等于用治骨折的方法治感冒。四类冲突,识别特征与处理方向完全不同:

类型识别特征本质处理方向
事实冲突对同一客观命题给出相反结论(「违反 3.2 条」vs「不违反 3.2 条」)至少一方错了找证据,能复算的复算
口径冲突结论方向一致但数字/结论不同,且各自的统计口径、单位、时间窗不同两边可能都对统一口径后重算,不是选一边
时序冲突两方基于不同时刻的状态快照(C 的判例早于规则 3.2 生效日)两边在各自时刻都对按时间戳取新,旧结论作废而非"错"
幻觉冲突一方引用的依据无法溯源(引了一个不存在的条款号)一方凭空编造直接判负,不进入后续裁决

表是四类的并列定义,但真正难的是判断次序——四个问题必须按下面这个顺序问,第一个答「否」的地方就是它的类型:

拿到一对矛盾结论,按①②③的次序问;第一个答「否」的地方就是它的类型① 依据能溯源吗引的条款号 / 文档真的存在吗
<rect x="8" y="90" width="210" height="42" rx="6" fill="#1e293b" stroke="#38bdf8" stroke-width="1.5"/> <text x="113" y="108" text-anchor="middle" fill="#e0f2fe">② 双方基于同一时刻吗</text> <text x="113" y="123" text-anchor="middle" fill="#94a3b8" font-size="10">比对依据上的时间戳</text> <rect x="8" y="152" width="210" height="42" rx="6" fill="#1e293b" stroke="#38bdf8" stroke-width="1.5"/> <text x="113" y="170" text-anchor="middle" fill="#e0f2fe">③ 口径 / 单位 / 时间窗一致吗</text> <text x="113" y="185" text-anchor="middle" fill="#94a3b8" font-size="10">统计范围是不是同一个</text> <rect x="8" y="214" width="210" height="42" rx="6" fill="#1e293b" stroke="#38bdf8" stroke-width="1.5"/> <text x="113" y="232" text-anchor="middle" fill="#e0f2fe">④ 事实冲突</text> <text x="113" y="247" text-anchor="middle" fill="#94a3b8" font-size="10">至少一方错了 → 找证据复算</text> <rect x="300" y="28" width="352" height="42" rx="6" fill="#1e293b" stroke="#f97316" stroke-width="1.5"/> <text x="316" y="46" fill="#fed7aa">幻觉冲突 → 直接判负</text> <text x="316" y="61" fill="#94a3b8" font-size="10">不进入后续裁决,整条阶梯在这里就结束了</text> <rect x="300" y="90" width="352" height="42" rx="6" fill="#1e293b" stroke="#a855f7" stroke-width="1.5"/> <text x="316" y="108" fill="#e9d5ff">时序冲突 → 按时间戳取新</text> <text x="316" y="123" fill="#94a3b8" font-size="10">两边在各自时刻都对,旧结论是作废而不是「错」</text> <rect x="300" y="152" width="352" height="42" rx="6" fill="#1e293b" stroke="#a855f7" stroke-width="1.5"/> <text x="316" y="170" fill="#e9d5ff">口径冲突 → 统一口径后重算</text> <text x="316" y="185" fill="#94a3b8" font-size="10">两边可能都对,此时「选一边」本身就是错的</text> 
否否否是是是次序不能换:①最便宜且能整个踢掉一方;②③会把「两边都对」误判成「有一方错」。「事实冲突」是问完三题后剩下的残差,不是默认答案——多数人直接从它开始,所以错。

开篇场景的真实分类:看起来是事实冲突,实际很可能是时序冲突——如果规则 3.2 是新增的,C 的 4 起判例全部作废,根本不需要"综合判断"。

不分类的后果是具体的:把时序冲突当事实冲突处理,你会去比较 B 和 C 谁更可信,然后得出一个"B 置信度 0.8、C 置信度 0.7,听 B"的结论——碰巧对了,但理由完全是错的,下次口径反过来就会错。把口径冲突当事实冲突处理更糟:两边都对,你却强行判了一边负。

分类这一步本身要落地成一个动作:要求每个下游 Agent 的回复必须带上「依据 + 依据的时间戳 + 可溯源标识」三个字段。没有这三个字段,分类无法自动进行,只能靠人看。

第 1~4 级:阶梯与进入判据

级手段进入判据代价为什么不能跳过前一级
1证据优先:可工具复算或可溯源到权威数据的一方胜至少一方的结论能被工具重新算一遍,或能溯源到 SSOT一次工具调用,最便宜—
2置信度加权:取 self-report 置信度 × 该 Agent 的历史准确率双方都无法客观复算,但能取到置信度或历史准确率需要维护历史准确率表有客观依据时用加权是浪费——而且可能输给一个高置信度的错答
3第三方仲裁 Agent:独立 Agent 看双方论据后判加权后差距在阈值内(无明显优势方,如 |Δ| < 0.15)多一次完整推理,最贵的自动手段无证据时直接仲裁,等于引入第三个幻觉源去评判两个幻觉
4人工闸门① 冲突涉及不可逆动作;或 ② 仲裁后重试 N 次仍不收敛(同分/反复翻转)人工延迟,从秒级变成分钟/小时级人是最贵的资源,必须最后一级

逐级说清楚:

第 1 级 · 证据优先。 「可复算」的判定要具体:B 说"违反 3.2 条",可复算 = 拿条款号去规则库查原文,看内容是否真的落在条款描述内。这一步能筛掉的不只是错答,还有幻觉冲突——如果 3.2 条不存在,B 直接判负,裁决在第 1 级就结束了。证据优先能解决的冲突比想象的多,因为 Agent 的冲突里相当大比例是一方在编造依据。

第 2 级 · 置信度加权。 两个坑:其一,LLM 的 self-report 置信度校准很差(普遍高估,且对措辞敏感),单用它不如不用;必须乘上一个外部观测到的历史准确率——即这个 Agent 在这类任务上过去的实际命中率,这个数字来自 评测与线上运营 的离线回放。其二,加权的结果是一个连续分数,必须设阈值把"没有明显优势"这种情况识别出来,而不是硬取较大值——0.71 vs 0.69 判 B 胜,是在把噪声当信号。

第 3 级 · 第三方仲裁。 仲裁 Agent 的 prompt 有个硬要求:只给它双方的论据,不给双方的身份和置信度。给了身份它会偏向"听起来更权威"的那个,给了置信度它会直接抄分高的那个——这两种偏差都会让仲裁退化成第 2 级。另外仲裁 Agent 最好换一个模型(不同厂商或不同规模),理由见下一节。

第 4 级 · 人工闸门。 两个触发条件写死在代码里,不由模型判断:不可逆动作(下架内容、扣费、对外发布——这层分级复用 运行时 · 危险动作三级分级);不收敛(仲裁重试 N 次,出现同分或结论反复翻转)。「反复翻转」这个信号特别要监控——它说明输入本身是模糊的,再仲裁一百次也不会收敛,继续重试只是在烧钱。

为什么「多数投票」不在这个阶梯里

这是这道题真正的区分度所在。两条独立的原因,任一条都足以否掉它:

原因一:A/B/C 场景里实际只有 2 个投票者,2 票在数学上无法产生多数。 A 是提问者和裁决者,不是投票者;投票者只有 B 和 C。两票相反就是 1:1 平局,投票机制不产生任何结论——它需要奇数个投票者才有定义。这不是"投票效果不好",是投票在这个场景下没有输出。想用投票就必须加一个 D,而这引出第二个问题。

原因二:同源模型的投票不独立,会把错误加权放大成"虚假共识"。 投票的全部数学基础是投票者的错误相互独立——独立时多数投票才能把准确率从 p 提升到超过 p。但 B、C、D 如果是同一个模型(同一权重、同一训练数据)的三个实例,对同一段模糊输入,它们会犯同向错误:不是随机分散地错,而是一起错到同一个答案上。

这时投票的结果是最坏的:三票一致,置信度看起来极高,但答案是错的。你不但没纠错,还把一个错答包装成了"三个 Agent 一致确认",让下游更难质疑它。这就是虚假共识——投票把相关性误当成了独立验证。

所以阶梯的第 1 级是证据而不是投票:证据是外部的、与模型无关的,它天然独立于模型的偏差。投票只在一个前提下有效——投票者来自不同模型且各自有独立信息源,这个前提在大多数 MAS 里都不成立(通常是同一个模型换了 system prompt),所以默认不把它放进阶梯。这也是第 3 级仲裁 Agent 建议换模型的理由。

裁决时机:必须在主 Agent 装配上下文之前

这一条是把 MAS 和单 Agent 运行时接起来的关键。

裁决 MUST 在 A 装配自己的上下文之前完成,只有裁决结果进 A 的窗口。

开篇场景里 A 犯的错正是这个:它把 B 和 C 的矛盾结论都塞进了自己的上下文,然后指望模型"综合判断"。后果有两层:

  1. 立刻的后果:模型面对矛盾输入时倾向于拼接而非取舍("下架并加提示标签"),因为拼接在语言上更像"全面考虑"。
  2. 长期的后果:矛盾结论一旦进了 A 的上下文,就成了 A 后续所有推理的前提——这就是 运行时 · 上下文中毒 记的那个坑。而且它比普通的上下文中毒更难清:中毒的不是一条错事实,是一对互斥事实,压缩摘要时模型会随机保留其中一条,之后你完全不知道它按哪条在推理。

所以正确的流程是:

图里那两条虚线红叉是全篇最重要的一笔:B 和 C 的原始矛盾结论没有一条直连到 A 的上下文装配。

裁决结果进 A 的窗口时,还要带上裁决理由与被否结论——但形态是「已裁决:采纳 X,因为 Y;C 的相反结论因时序作废」这样一条已闭合的事实,而不是两条待判断的开放信息。这个区别很细但很实在:前者模型不会再去权衡,后者会。

仲裁器代码骨架

把上面四级写成可执行的样子。注意 Resolve 的每个 return 都对应阶梯的一级,升级人工是一个显式返回分支,不是注释:

package arbiter

// Claim:下游 Agent 的一条结论。三个非结论字段是分类的前提 ——
// 没有 Evidence / AsOf / Traceable,第 0 级分类无法自动进行。
type Claim struct {
    Agent      string
    Conclusion string
    Evidence   string    // 依据(条款号、文档 ID、查询语句)
    AsOf       time.Time // 依据对应的状态时刻 —— 时序冲突全靠它
    Traceable  bool      // 依据能否溯源到 SSOT
    SelfConf   float64   // 模型自报置信度,校准很差,只作为第 2 级输入之一
}

type Verdict struct {
    Level    int    // 在第几级收敛(1~4),落 metric 用
    Winner   *Claim // nil 表示需要人工
    Rejected []Claim
    Reason   string // 进 A 上下文的那句话
    NeedsHuman bool
}

type ConflictKind int

const (
    KindFactual ConflictKind = iota // 事实冲突
    KindMetric                      // 口径冲突
    KindTemporal                    // 时序冲突
    KindHallucination               // 幻觉冲突
)

type Arbiter struct {
    verify     func(Claim) (ok bool, err error) // 工具复算/溯源
    accuracy   func(agent string) float64       // 历史准确率,来自离线回放
    judge      func(a, b Claim) (winner string, err error) // 换模型的仲裁 Agent
    confMargin float64 // 第 2 级的阈值,经验 0.15
    maxJudge   int     // 第 3 级最多重试次数
    irreversible func(conclusion string) bool   // 是否触发不可逆动作
}

func (r *Arbiter) Resolve(a, b Claim) Verdict {
    // ── 第 0 级:分类。不分类就选手段 = 必错。
    switch r.classify(a, b) {

    case KindHallucination:
        // 一方依据不可溯源 → 直接判负,不进入后续裁决。
        if !a.Traceable && b.Traceable {
            return Verdict{Level: 1, Winner: &b, Rejected: []Claim{a},
                Reason: fmt.Sprintf("采纳 %s:%s 的依据 %q 无法溯源", b.Agent, a.Agent, a.Evidence)}
        }
        if !b.Traceable && a.Traceable {
            return Verdict{Level: 1, Winner: &a, Rejected: []Claim{b},
                Reason: fmt.Sprintf("采纳 %s:%s 的依据 %q 无法溯源", a.Agent, b.Agent, b.Evidence)}
        }
        // 双方都不可溯源 —— 没有任何可信输入,不做自动裁决。
        return Verdict{Level: 4, NeedsHuman: true,
            Reason: "双方依据均无法溯源,无可信输入"}

    case KindTemporal:
        // 时序冲突:按时刻取新,旧结论「作废」而非「错误」——
        // 措辞很重要,写成"错误"会污染该 Agent 的历史准确率统计。
        newer, older := a, b
        if b.AsOf.After(a.AsOf) {
            newer, older = b, a
        }
        return Verdict{Level: 1, Winner: &newer, Rejected: []Claim{older},
            Reason: fmt.Sprintf("采纳 %s(依据时刻 %s);%s 的结论基于更早的 %s,已作废",
                newer.Agent, newer.AsOf.Format("2006-01-02"), older.Agent, older.AsOf.Format("2006-01-02"))}

    case KindMetric:
        // 口径冲突:两边可能都对,不能选一边 —— 必须统一口径重算。
        // 这是唯一一个「不产出 Winner 也不需要人工」的出口:回退给调用方重问。
        return Verdict{Level: 1, Winner: nil,
            Reason: "口径不一致(统计窗口/单位不同),需统一口径后重新查询,不做优劣裁决"}

    case KindFactual:
        // 走完整阶梯。
    }

    // ── 第 1 级:证据优先。能复算的复算。
    aOK, aErr := r.verify(a)
    bOK, bErr := r.verify(b)
    if aErr == nil && bErr == nil && aOK != bOK {
        w, l := a, b
        if bOK {
            w, l = b, a
        }
        return Verdict{Level: 1, Winner: &w, Rejected: []Claim{l},
            Reason: fmt.Sprintf("采纳 %s:其依据 %q 经复算成立,%s 的依据复算不成立",
                w.Agent, w.Evidence, l.Agent)}
    }

    // ── 第 2 级:置信度加权。self-report 必须乘历史准确率。
    sa := a.SelfConf * r.accuracy(a.Agent)
    sb := b.SelfConf * r.accuracy(b.Agent)
    if math.Abs(sa-sb) > r.confMargin {
        w, l, ws, ls := a, b, sa, sb
        if sb > sa {
            w, l, ws, ls = b, a, sb, sa
        }
        return Verdict{Level: 2, Winner: &w, Rejected: []Claim{l},
            Reason: fmt.Sprintf("采纳 %s:加权置信 %.2f vs %.2f(差距超阈值 %.2f)",
                w.Agent, ws, ls, r.confMargin)}
    }

    // ── 第 3 级:第三方仲裁。隐去身份与置信度,只给论据。
    flips := 0
    var last string
    for i := 0; i < r.maxJudge; i++ {
        winner, err := r.judge(strip(a), strip(b)) // strip: 去掉 Agent 名与 SelfConf
        if err != nil {
            break
        }
        if last != "" && winner != last {
            flips++ // 结论翻转 —— 输入本身模糊,再判也不会收敛
        }
        last = winner
        if flips == 0 && i+1 >= 2 { // 连续两次同结论才算收敛
            w, l := a, b
            if winner == b.Agent {
                w, l = b, a
            }
            return Verdict{Level: 3, Winner: &w, Rejected: []Claim{l},
                Reason: fmt.Sprintf("采纳 %s:第三方仲裁连续两次一致", w.Agent)}
        }
    }

    // ── 第 4 级:人工闸门。显式返回分支,不是 TODO 注释。
    // 两个触发条件都由代码判定,不交给模型。
    why := "仲裁未收敛(结论反复翻转,输入本身模糊)"
    if r.irreversible(a.Conclusion) || r.irreversible(b.Conclusion) {
        why = "冲突涉及不可逆动作,按策略强制人工确认"
    }
    return Verdict{Level: 4, NeedsHuman: true, Reason: why}
}

三个容易写错的实现细节,都在上面代码里:

  1. 时序冲突的措辞是「作废」不是「错误」。如果把旧结论记为错误,它会进入该 Agent 的历史准确率统计,把一个本来正确的 Agent 的准确率拉下来——而准确率正是第 2 级的输入。这个反馈回路会慢慢腐蚀整个裁决系统。
  2. 口径冲突的出口既不是 Winner 也不是人工,而是「回退重问」。它是唯一一个可以自动修复但不产生胜负的分支,漏掉它会导致把两个都对的结论判一个负。
  3. 第 3 级的收敛判定要求"连续两次同结论",不是"跑一次就信"。跑一次的仲裁和第 2 级的加权一样脆弱,而翻转次数正是"该升级到人工"的最强信号。

规模锚点

上面这套阶梯以「一次冲突涉及 2~3 个 claim、下游 Agent 数 ≤ 8」为前提。claim 数上到十几个时(比如 10 个 Agent 各给一个结论),阶梯本身还成立,但第 3 级的两两仲裁会变成 O(N²) 次推理——此时应先按结论聚类(相同结论归并),把 10 个 claim 压成 2~3 个阵营再进阶梯。

这一节记住三句话

  1. 先分类再选手段。四类冲突(事实 / 口径 / 时序 / 幻觉)处理方向完全不同:口径冲突两边都对(统一口径重算)、时序冲突两边在各自时刻都对(按时间戳取新、旧的作废不算错)。不分类就选手段,等于用治骨折的方法治感冒。
  2. 阶梯必须从证据起步,且不能跳级。证据 → 置信度加权 → 第三方仲裁 → 人工闸门,逐级变贵。无证据时直接上仲裁,等于引入第三个幻觉源去评判两个幻觉。
  3. 多数投票不在阶梯里,两条独立理由各自足够:2 个投票者产生不了多数(A 是裁决者不是投票者);同源模型会犯同向错误,投出"三票一致但全错"的虚假共识——把相关性误当成了独立验证。

任务调度、分派与结果聚合

打个比方:这一节就是你写过的任务编排器——把大活拆成 DAG、按 worker 能力或负载派单、收回结果做验收、失败了决定重试还是换台机器。Airflow、Celery、你自己那套调度框架,做的都是这件事。类比失效边界:常规编排里 worker 的产出是确定的——同样输入跑两遍结果一样,失败会明确抛错。这里 worker 是模型:它可能"成功返回"一个错答案(不抛错、格式还合法),所以验收不能省,且验收方不能是产出方;它的失败也分两类——偶发(该重试)和能力不匹配(该改派),而区分二者要看失败原因,不能一律重试。

任务分解与 DAG

多 Agent 的调度输入是一张 DAG,关键是哪条边是真依赖、哪条是假依赖:

  • 真依赖(必须串行):下游需要上游的确定性产物才能开始。分镜必须等剧本定稿——不是"最好等",是没有剧本无法分镜。
  • 假依赖(可并行):只是书写顺序上先后,实际无数据依赖。查规则库和查历史判例之间没有依赖,串行只是因为代码是顺着写的。

判定方法很朴素:问「上游产物的内容变化,会不会改变下游的输入」。会 → 真依赖;不会 → 假依赖,拆开并行。这个判定要在编排期做(写 DAG 时),不能交给运行时的模型判断——模型倾向于保守串行,会白白丢掉并行度。

三种分派策略

策略怎么派适用条件陷阱
能力路由按静态声明的能力/工具权限匹配Agent 能力边界清晰且稳定声明与实际能力漂移(工具下线了声明还留着)→ 需定期校验
负载均衡派给当前最空闲的同类 Agent有多个同质 Agent 实例只对无状态Agent 成立;有会话状态的必须黏在同一实例
竞标广播 → 出价 → 择优能力异构且事先不知谁合适出价基于模型自评 = 系统性高估(见前文)→ 出价必须是可校验的硬事实

默认选能力路由。它最简单、最可预测、最好调试。只有当"派给谁"这件事本身需要运行时信息才升级到后两者。

结果聚合与验收

派出去之后,谁负责验收必须显式指定,否则就是下面要讲的「责任扩散」。三条规则:

  1. 验收方不能是产出方。让 Worker 自己判断自己的产物是否合格,等于没有验收——模型对自己的输出评价普遍偏高。
  2. 验收标准要在派单时就给出,写进子任务的定义里("返回必须是含 X/Y/Z 三字段的 JSON,且 X 非空"),不是产出后再临时定。
  3. 验收失败要区分「重派」和「改派」:同一个 Agent 重试(可能是偶发失败)vs 换一个 Agent(可能是能力不匹配)。判据是失败原因——格式错/超时 → 重派;「我做不了这个」→ 改派。重试次数上限与退避策略复用 运行时 · 重试策略。

整体回滚边界:这是 MAS 比单 Agent 难的地方。子任务 1、2 成功且已产生副作用,子任务 3 失败——不能简单地"整体回滚",因为 1、2 的副作用可能不可逆。可行的边界是:把有副作用的子任务尽量后置,编排成「所有只读/可回滚的先跑,不可逆的集中在最后一段」,这样失败时需要补偿的范围最小。这本质上是 Saga 模式在 Agent 编排上的应用。

拓扑无关的 Agent 接口 + 消息总线

这是"模块化可插拔"落到代码上的样子。关键性质:换拓扑不改 Agent 实现——Agent 只认识 Envelope 进、Envelope 出,完全不知道自己身处 Supervisor 还是群聊:

package mas

// Envelope:唯一的传输单元。Agent 只认识它,不认识拓扑。
type Envelope struct {
    TaskID  string
    From    string
    To      string   // 直接寻址;广播时为空
    Topic   string   // 发布订阅时使用
    Payload any
    Trace   string   // 贯穿全链路的 trace id,归因全靠它
}

// Agent:拓扑无关的最小接口。
// 注意它没有任何「向谁发送」的能力 —— 发送由总线按拓扑决定。
// 这是「换拓扑不改 Agent」的全部秘密:剥夺 Agent 的寻址权。
type Agent interface {
    Name() string
    Handle(ctx context.Context, in Envelope) ([]Envelope, error) // 返回想发出的消息
}

// Topology:决定一条消息实际投递给谁。换拓扑 = 换这一个实现。
type Topology interface {
    Route(out Envelope, registry map[string]Agent) []string // 返回收件人名单
}

type SupervisorTopology struct{ Planner string }

func (t SupervisorTopology) Route(out Envelope, reg map[string]Agent) []string {
    // Supervisor:Worker 的输出一律回 Planner,Worker 之间互不可见 —— 这就是 O(N) 的来源
    if out.From != t.Planner {
        return []string{t.Planner}
    }
    if out.To != "" {
        return []string{out.To}
    }
    names := make([]string, 0, len(reg))
    for n := range reg {
        if n != t.Planner {
            names = append(names, n)
        }
    }
    return names
}

type GroupChatTopology struct{}

func (GroupChatTopology) Route(out Envelope, reg map[string]Agent) []string {
    // 群聊:广播给除自己外所有人 —— O(N²) token 就是在这一行产生的
    names := make([]string, 0, len(reg))
    for n := range reg {
        if n != out.From {
            names = append(names, n)
        }
    }
    return names
}

// Bus:调度循环。预算与终止条件在这里,不在 Agent 里。
type Bus struct {
    agents   map[string]Agent
    topo     Topology
    guard    *HandoffGuard
    maxHops  int // 硬闸:消息传递总跳数上限,防活锁
}

func (b *Bus) Run(ctx context.Context, seed Envelope) error {
    queue := []Envelope{seed}
    for hops := 0; len(queue) > 0; hops++ {
        if hops >= b.maxHops {
            return fmt.Errorf("mas: max hops %d exceeded (可能活锁), trace=%s", b.maxHops, seed.Trace)
        }
        cur := queue[0]
        queue = queue[1:]

        for _, name := range b.topo.Route(cur, b.agents) {
            ag := b.agents[name]
            in := cur
            in.To = name

            outs, err := ag.Handle(ctx, in)
            if err != nil {
                return fmt.Errorf("mas: agent %s: %w", name, err)
            }
            queue = append(queue, outs...)
        }
    }
    return nil
}

换拓扑只需要 bus.topo = GroupChatTopology{},六个 Agent 的实现一行不动。代价也要说清:这层抽象让"消息去哪"从 Agent 代码里消失了,调试时你不能再从 Agent 代码读出数据流向,必须去看 Topology + trace。这是可插拔的固定成本。

这一节记住三句话

  1. 拆 DAG 的唯一判据是"上游产物变了会不会改下游输入"。会 → 真依赖必须串行;不会 → 假依赖拆开并行。这个判定要在编排期做完,交给运行时的模型它会保守串行、白丢并行度。
  2. 验收方不能是产出方,且验收标准要在派单时就写进子任务定义里。让 Worker 自评等于没有验收——模型对自己的输出评价普遍偏高。
  3. 有副作用的子任务尽量后置。子任务 1、2 已产生不可逆副作用而 3 失败时,没法"整体回滚";编排成"只读/可回滚的先跑、不可逆的集中在最后一段",失败时的补偿范围最小——本质是 Saga 模式在 Agent 编排上的应用。

为什么这么做

为什么冲突消解必须是阶梯而不是策略池

策略池("有这几种方法可选")的问题不在于方法不对,在于它把选择权留给了运行时——而运行时的选择者是模型。让模型选"这次用投票还是用仲裁",你就得到一个不可复现的系统:同一个冲突两次运行走了不同路径,线上出问题无法回溯。

阶梯把选择权收回到代码里的判据:能不能复算是一次工具调用的返回值,差距是否超阈值是一个减法。这两件事对同一输入永远给同一答案,所以整条裁决路径是确定性的、可回放的。落 metric 时 Verdict.Level 直接告诉你「本周有多少冲突在第 1 级就解决了、多少升到了人工」——这个分布是 MAS 健康度最直接的指标,比成功率灵敏得多。

为什么裁决要放在装配之前,而不是让模型自己权衡

反直觉的点在于:模型确实有能力识别矛盾。你给它两段冲突文本并明确问"这两段矛盾吗",它答得很好。

但在真实的 Agent 循环里,A 的任务不是"识别矛盾",是"完成审核"。矛盾输入只是它上下文里的一部分材料,而模型面对材料冲突的默认行为是找一个能同时容纳两者的表述——因为在语言概率上,"综合考虑 X 和 Y" 比 "Y 是错的" 更常见、更安全。于是你得到"下架并加提示标签"这种荒谬输出。

把裁决前移,本质是把一个模型不擅长的判断(在有任务压力时取舍矛盾)换成一个框架擅长的判断(按判据走阶梯)。这跟 运行时 里"不能让模型自己判断操作危险不危险"是同一个道理:不是模型能力不够,是这个判断不该由带着任务倾向的一方来做。

为什么黑板要付版本号这笔钱

不加版本号的黑板在 demo 里跑得很好——两个 Agent 很少真的同时写同一个 key。问题是这个 bug 的发生率随并发度上升而上升,而可发现性始终为零:丢写不报错、不留痕,只表现为"某个 Agent 的修改好像没生效"。

版本号买到的唯一东西是把静默失败变成显式报错。ErrStaleWrite 一抛,你至少知道发生了并发写;Agent 重读重试,语义上正确。这笔钱必须付的判据很简单:只要有两个以上 Agent 可能写同一个 key,就必须付。如果 key 空间按子任务严格划分(Agent X 只写 shot_1.*),可以不付——但那要靠命名规范保证,而规范会腐化。

为什么别的选择不行

MAS 特有的五种失效

这五种只在多 Agent 下出现,单 Agent 再怎么坏也坏不出来:

失效表现检测手段兜底
死锁 / 活锁(含「礼貌性死锁」)两方都在等对方先出方案("你先提,我配合"↔"还是你先");或消息在两个 Agent 间无限往返但无进展消息总跳数硬闸(上面 maxHops)+ 检测消息内容指纹重复(同一对 Agent 间连续 N 轮语义无新增)指定 tie-breaker:编排期就规定"冲突时 X 先发言";活锁触发即降级为单 Agent 兜底完成
责任扩散子任务无人执行——每个 Agent 都以为别人会做。广播派单最容易出派单后维护未认领子任务表,超时未认领即告警;每个子任务必须有唯一 owner 字段禁止无 owner 的广播派单:广播只用于通知,派单必须直接寻址
上下文分裂同一事实在两个 Agent 窗口里是不同版本(A 以为主角 28 岁,B 以为 32 岁),各自输出都自洽但拼不起来关键共享事实做跨 Agent 一致性断言:定期抽取各 Agent 的 facts,对同名 key 比对共享事实只存黑板不进各自可压缩区(详见 运行时 · 多 Agent 窗口归属);Agent 读黑板而非记在自己上下文里
级联幻觉A 的猜测被 B 当作既定事实引用,B 的输出又被 C 引用——三跳之后没人知道源头是个猜测每条 claim 强制带 Traceable 与来源标识;跨 Agent 传递时保留来源链而非只传结论交接时只传已确认事实,不传中间猜测(前文交接表);裁决器对不可溯源 claim 直接判负
token 组合爆炸成本随 Agent 数平方增长,5 个 Agent 的任务比单 Agent 贵 20 倍按 Agent 维度分别计量 token(不是只看总量),看互传消息占各自上下文的比例改拓扑(群聊 → Supervisor)或改传递内容(全文 → 结构化结论)。这两条之外没有第三条

它们与单 Agent 失效的共同点:多数不报错,只表现为"结果变差"——和 运行时的三种记忆失效 一样,必须靠主动断言发现,不能等异常。

差异在于:单 Agent 的失效是纵向的(一条轨迹上前面污染后面),检测只需看一条 trace;MAS 的失效是横向的(两个 Agent 的状态互不一致),检测必须跨 trace 比对。这是 MAS 可观测性真正难的地方,也是为什么 span 必须挂同一个 Trace id(上面 Envelope.Trace)——归因手段见 评测 · span 树归因。

何时不该上 MAS

MAS 是有固定代价的:token 组合爆炸(互看输出 O(N²))与调试面积膨胀(N 个 Agent 的状态组合 + 跨 trace 比对)。付这笔钱之前先过三条否决判据:

三条都否 → 用单 Agent + 工具,不要上 MAS:

  1. 子任务之间需要并行吗? 如果本来就是顺序执行,多 Agent 只是把一个 for 循环拆成了几个进程。
  2. 需要不同的 system prompt 隔离吗? 如果所有子任务都能在同一个 system prompt 下完成,拆成多个 Agent 只是给同一个模型换了几个名字——这是最常见的伪 MAS。
  3. 需要不同的权限边界吗? 如果所有子任务用同一批工具、同一套凭据,"多 Agent"提供不了任何隔离价值。

看起来该上 MAS、实际不该的典型反例

「代码审查系统:一个 Agent 查安全、一个查性能、一个查风格,最后汇总」。

看起来完美符合 MAS——三个专业角色、职责清晰、可以并行。实际上过一遍判据:三者需要并行吗?可以并行,但同一个 Agent 顺序跑三遍也就是三次调用,延迟差异在几秒级,对代码审查场景无意义。需要 prompt 隔离吗?三个视角的差异完全可以用同一个 Agent 的三次不同 prompt 调用(或一次调用里的三段 XML 分区)覆盖。需要权限隔离吗?三者都只是读代码,权限完全相同。

三条全否,所以不该上 MAS。正确做法是单 Agent + 三次带不同 system 段的调用,成本 O(3)、无跨 Agent 状态、无冲突消解需求、trace 是一条线。拆成三个 Agent 之后你反而多了一个新问题——三者结论冲突时听谁的(比如安全说要加校验、性能说这个校验在热路径上),得把上面那整套阶梯搬进来。为一个不存在的问题引入了一套裁决机制,这就是伪 MAS 的典型代价。

那什么时候它真该上 MAS?当"查安全"这个子任务需要独立的工具权限(能调漏洞库、能读生产配置)而其他两个不该有这个权限时——此时权限隔离判据成立,MAS 的钱花得值。

主流框架的核心抽象对照

只对照核心抽象,不写 API——API 半年一变,抽象不变。选型结论见 Agent 开发 · 框架对比(枢纽页给"该选哪个"),本节只给"它们各自把什么做成了一等公民":

框架语言核心抽象哪种拓扑是一等公民值得学的一点
Eino(字节)Go分层编排:Chain(线性)/ Graph(有环、可分支)/ Workflow(字段级映射),组件用 Go 泛型定型Pipeline + Supervisor编排期类型安全——上下游节点的输入输出类型在编译期校验,接错直接编不过。Python 框架只能等运行时炸
LangGraphPython/JS显式状态图:节点 = 函数,边 = 转移条件,全局 State 通过 reducer 合并Blackboard(全局 State 就是黑板)状态图 + checkpointer 让「断点续跑」成为框架能力而非自己实现
AutoGen(MSFT)PythonConversableAgent + GroupChat + GroupChatManagerGroup Chat把"发言权轮转"抽象成可替换的 speaker selection 策略
CrewAIPythonAgent(role/goal/backstory) + Task + Crew(process: sequential/hierarchical)Pipeline / Supervisor角色-任务-流程三件套的心智模型极轻,从 0 到跑通最快
MetaGPTPythonSOP 驱动:把软件公司流程(PM→架构→开发→测试)编码成固定角色与产出物Pipeline(SOP 即固定流水线)「用 SOP 约束 Agent」这个思路本身——固定流程比自由讨论稳定得多

抽象层面的三条结论

  1. Go 生态的差异化优势是编排期类型安全(Eino 的泛型定型)。这不是语法糖:多 Agent 编排的接错概率随节点数上升,能在编译期拦住的错误就不会在第 20 轮烧掉预算之后才暴露。
  2. 框架把哪种拓扑做成一等公民,决定了它的默认成本曲线。AutoGen 的 GroupChat 好用,代价是默认 O(N²);选它就要主动限轮数。
  3. 没有框架内置冲突消解阶梯。GroupChat 的"讨论到共识"、CrewAI 的 hierarchical 都不是裁决协议——本篇那套阶梯目前都得自己写。这也是面试里可以主动讲的差异化点。

具体 API 与参数请以各框架官方文档为准。

沉淀结论

面试常见问题清单

拓扑与通信

  • Q:Multi-Agent 有哪几种协作方式,怎么选? A:五种——Supervisor(中心调度,最常用,O(N))、Pipeline(阶段固定)、Group Chat(需互相质疑,O(N²) 最贵)、Blackboard(长任务共享大状态)、Contract Net(学术常见工业罕见,因为出价靠模型自评而自评系统性高估)。选型看三件事:子任务边界是否清晰(清晰→Supervisor)、是否需要互相质疑(需要→Group Chat 但限轮数)、共享状态是否大(大→Blackboard)。
  • Q:Agent 之间怎么通信? A:三层——消息传递(直接寻址/广播/发布订阅,耦合递减)、共享状态(黑板,必须带版本号做 CAS)、控制权交接(Handoff,令牌 task_id+from+seq,必须幂等)。最大陷阱是消息进彼此上下文导致 token O(N²)。
  • Q:拓扑和通信机制冲突时怎么裁决? A:以一致性要求为先——需要强一致的共享状态就不能用广播(广播无版本语义,并发改同一事实会静默丢写且不可复现),必须黑板 + 版本号。一句话:消息传递的失效是吵到爆窗,黑板的失效是静悄悄丢数据。

冲突消解(高频)

  • Q:A 分别问 B 和 C,两者结果有出入怎么办? A:先分类再走四级阶梯。第 0 级分类:事实(一方错)/ 口径(两边都对,统一口径重算)/ 时序(基于不同时刻快照,取新,旧的作废不是错)/ 幻觉(依据不可溯源,直接判负)。四级阶梯:证据优先 → 置信度加权(self-report × 历史准确率,差距需超阈值)→ 第三方仲裁(换模型、隐去身份、要求连续两次一致)→ 人工闸门(涉不可逆动作,或仲裁反复翻转)。关键:裁决必须在 A 装配上下文之前完成,只有裁决结果进 A 的窗口,否则就是上下文中毒。
  • Q:为什么不用多数投票? A:两条独立原因。① A/B/C 场景实际只有 B、C 两个投票者,1:1 平局时投票在数学上没有输出,它需要奇数投票者。② 即使凑到奇数,同源模型的投票不独立,对模糊输入会犯同向错误——三票一致但全错,还把错答包装成"三方确认",这叫虚假共识。投票的数学基础是错误独立,而这前提在同模型换 prompt 的 MAS 里不成立。所以第 1 级是证据(外部的、与模型无关)。
  • Q:为什么不让 A 自己综合判断? A:模型有能力识别矛盾,但在有任务压力时面对矛盾材料的默认行为是找一个能同时容纳两者的表述("下架并加提示标签")——拼接在语言概率上比否定更安全。前移裁决,是把"带任务倾向的一方做取舍"换成"框架按判据走阶梯",与"不让模型自判操作危险"同理。

失效与选型

  • Q:MAS 特有的坑有哪些? A:五个——死锁/活锁(含礼貌性死锁:都等对方先出方案,靠 maxHops 硬闸 + 编排期 tie-breaker)、责任扩散(无人认领,靠子任务唯一 owner + 禁止无 owner 广播派单)、上下文分裂(同一事实两个版本,靠共享事实只存黑板 + 跨 Agent 一致性断言)、级联幻觉(猜测被当事实多跳传递,靠保留来源链 + 只交接已确认事实)、token 组合爆炸(改拓扑或改传递内容,没有第三条路)。共同点是都不报错;与单 Agent 失效的差异是它们是横向的,检测必须跨 trace 比对。
  • Q:什么时候不该上 MAS? A:三条否决判据全否就别上——无需并行、无需 system prompt 隔离、无需权限边界隔离。固定代价是 token O(N²) + 调试面积膨胀。典型反例:代码审查拆成安全/性能/风格三个 Agent——三条全否,单 Agent 三次不同 prompt 即可;拆开反而凭空多出"三者冲突听谁的"这个新问题。答案反转的条件:「查安全」需要独立工具权限而另两个不该有时。
  • Q:Eino 和 LangGraph / AutoGen 的区别? A:抽象层面——Eino 是 Go 的分层编排(Chain/Graph/Workflow)+ 泛型带来的编排期类型安全,上下游类型接错编译期就报;LangGraph 是显式状态图 + 全局 State(本质是黑板)+ checkpointer 内置断点续跑;AutoGen 把 GroupChat 与发言权轮转策略做成一等公民,代价是默认 O(N²)。共同缺口:没有框架内置冲突消解阶梯。

心法总结

MAS 的价值不在"更多 Agent 更聪明",在于用隔离换可控——不同权限、不同上下文、不同 prompt 的隔离。而所有隔离的代价都由同一件事偿付:信息一致性。 拓扑决定信息流多贵,通信机制决定信息一致不一致,裁决协议决定不一致时怎么收场。三者里最容易被跳过的是第三个,而它恰好是唯一会输出荒谬结果而非报错的那个。

本域延伸

  • 枢纽结论、单 Agent 范式对照与框架选型:Agent 开发
  • 单 Agent 运行时(状态机 / 分层记忆 / 工具沙箱 / 预算幂等 / 多 Agent 下的压缩窗口归属):Agent 运行时深水区
  • 跨 trace 归因、历史准确率的来源、轨迹判分:Agent 评测与线上运营
  • 这套拓扑与裁决在内容创作流水线的落地:互动影游与长视频创作 Agent
  • token 计价与 O(N²) 成本的实际量级:成本与延迟

记忆口诀

  • 五拓扑:Supervisor(中心 O(N))/ Pipeline(单向)/ Group Chat(互见 O(N²))/ Blackboard(共享态)/ Contract Net(竞标,工业罕见)
  • 三层通信:消息传递(直接/广播/订阅)→ 共享状态(黑板 + CAS)→ 交接(令牌 task_id+from+seq,必幂等)
  • 矩阵裁决一句话:强一致共享状态必走黑板+版本号,不走广播
  • 两种失效的性格:消息传递失效是吵(爆窗,看得见);黑板失效是静(丢写,看不见)
  • 冲突第 0 级:先分类——事实(一方错)/ 口径(都对,统一重算)/ 时序(取新,旧的作废)/ 幻觉(不可溯源,直接判负)
  • 四级阶梯:证据优先 → 置信度加权 → 第三方仲裁 → 人工闸门
  • 投票双否:2 个投票者平局无解 + 同源模型投票不独立(虚假共识)
  • 裁决铁律:裁决在装配上下文之前,只有结果进主 Agent 窗口
  • MAS 五失效:死锁活锁 / 责任扩散 / 上下文分裂 / 级联幻觉 / token 爆炸——都不报错,且都是横向的(须跨 trace 比对)
  • 不上 MAS 三否:无需并行 + 无需 prompt 隔离 + 无需权限隔离 → 单 Agent 加工具

内容来源

综合整理:Contract Net Protocol(Smith, 1980)的任务分派思想、AutoGen / LangGraph / CrewAI / MetaGPT / Eino 各自公开文档中的编排抽象、分布式系统的乐观并发控制(CAS / 版本号)与 Saga 补偿模式在 Agent 编排上的迁移、以及站内 Agent 运行时深水区 的单 Agent 结论。

框架生态更新快,具体 API 与参数请以官方文档为准;文中阈值(置信度阈值 0.15、仲裁连续两次一致、Agent 数上限区间)均为经验值,需按自身任务的冲突分布与成本预算重新标定。

自测:合上资料能说清楚吗?

  1. A 分别问 B 和 C,拿回两个矛盾结论。完整说出你的处理流程,包括第 0 级在做什么、为什么裁决不能放在 A 推理之后。
参考答案

第 0 级先分类(不分类就选手段等于用治骨折的方法治感冒):事实冲突(至少一方错,找证据)/ 口径冲突(统计窗口或单位不同,两边可能都对,统一口径重算而非选一边)/ 时序冲突(基于不同时刻快照,两边在各自时刻都对,按时间戳取新,旧结论记为"作废"而非"错误"——记成错误会污染该 Agent 的历史准确率,而准确率正是第 2 级的输入)/ 幻觉冲突(依据不可溯源,直接判负不进后续裁决)。分类要能自动化,前提是每条回复强制带「依据 + 依据时刻 + 可溯源标识」三字段。

然后走四级阶梯:① 证据优先——能工具复算或溯源到 SSOT 的一方胜;② 置信度加权——self-report × 历史准确率,且差距必须超阈值(0.71 vs 0.69 判胜是把噪声当信号);③ 第三方仲裁——换模型、隐去双方身份与置信度(给了它会偏向听起来权威的或直接抄分高的,退化回第 2 级)、要求连续两次同结论才算收敛;④ 人工闸门——涉及不可逆动作,或仲裁结论反复翻转(翻转说明输入本身模糊,再判一百次也不收敛)。

裁决必须在 A 装配上下文之前:立刻的后果——模型面对矛盾材料时倾向拼接而非取舍("下架并加提示标签"),因为"综合考虑"在语言概率上比"Y 是错的"更安全。长期的后果——矛盾结论进了上下文就成为后续所有推理的前提,即上下文中毒;且比普通中毒更难清,因为中毒的是一对互斥事实,压缩时模型会随机保留一条,之后你不知道它按哪条在推理。进 A 窗口的应该是「已裁决:采纳 X,因为 Y」这样一条已闭合的事实,不是两条待判断的开放信息。

  1. 为什么不用多数投票?说出两条独立的原因,以及投票在什么前提下才成立。
参考答案

原因一(数学上没有输出):A/B/C 场景里 A 是提问者和裁决者,不是投票者,实际只有 B、C 两票。1:1 平局时投票机制不产生任何结论——多数投票需要奇数个投票者才有定义。这不是"效果不好",是没有输出。

原因二(同源不独立):投票的全部数学基础是投票者的错误相互独立,独立时多数投票才能把准确率从 p 提到超过 p。但同一模型(同权重、同训练数据)的多个实例对同一段模糊输入会犯同向错误——不是随机分散地错,而是一起错到同一个答案上。结果是最坏的:三票一致、置信度看起来极高、答案是错的,还把错答包装成"三个 Agent 一致确认",让下游更难质疑。这叫虚假共识——投票把相关性误当成了独立验证。

成立前提:投票者来自不同模型且各自有独立信息源。这个前提在大多数 MAS 里不成立(通常是同一模型换 system prompt),所以默认不放进阶梯。这也是为什么第 3 级仲裁 Agent 建议换一个模型。

  1. 对比 Group Chat + 消息传递 与 Supervisor + 黑板 两种组合:各自的主要失效是什么、性质有何本质不同、分别在什么场景下选?
参考答案

Group Chat + 消息传递:主要失效是 token O(N²) + 跑题 + 发言权争夺。5 个 Agent 到第 10 轮时每人上下文里有 45 条别人的发言。性质是「吵」——爆窗、变慢,但看得见,成本曲线会直接告诉你。修法:硬限最大轮数、指定主持人决定发言顺序、每轮强制产出结构化结论而非自由讨论。适用:真正需要互相质疑的场景(方案评审、红蓝对抗),且 Agent 数 ≤ 5。

Supervisor + 黑板:主要失效是 状态竞争(两个 Worker 并发写同一 key 丢写)。性质是「静」——静默失败、不报错、不留痕,只表现为"某个修改好像没生效",且发生率随并发度上升而可发现性始终为零。修法:CAS + 版本号(把静默丢写变成显式 ErrStaleWrite)+ 按子任务划分 key 命名空间从设计上避免共写。适用:Worker 产物大、Planner 只需知道"完成了"的长任务。

本质区别:一个的代价是成本与延迟(可观测、可预算),另一个的代价是正确性(不可观测、需要主动付版本号这笔钱才能观测)。选黑板就必须付版本号——判据是"只要有两个以上 Agent 可能写同一个 key"。

  1. 一个「代码审查系统:安全 Agent + 性能 Agent + 风格 Agent,最后汇总」的设计,该不该上 MAS?用判据回答,并说明什么条件下答案会反转。
参考答案

不该。过三条否决判据:① 需要并行吗?可以并行,但单 Agent 顺序跑三次也就是三次调用,延迟差异秒级,对代码审查无意义。② 需要 system prompt 隔离吗?三个视角完全可以用同一 Agent 的三次不同 prompt 调用(或一次调用里的三段 XML 分区)覆盖。③ 需要权限隔离吗?三者都只读代码,权限完全相同。三条全否 → 单 Agent + 三次带不同 system 段的调用,成本 O(3)、无跨 Agent 状态、trace 是一条线。

拆开的额外代价:凭空多出"三者结论冲突时听谁的"这个新问题(安全说要加校验、性能说这个校验在热路径上),得把整套四级裁决阶梯搬进来——为一个不存在的问题引入一套裁决机制,这就是伪 MAS。加上 MAS 的两项固定代价:token 组合爆炸与调试面积膨胀。

答案反转的条件:当「查安全」需要独立的工具权限(能调漏洞库、能读生产配置)而另两个不该有这个权限时——第 3 条判据成立,权限隔离是单 Agent 给不了的,此时 MAS 的钱花得值。

  1. 交接令牌为什么是 task_id + from + seq 三段?去掉 from 会发生什么?
参考答案

三段各定位一个维度:task_id 定位是哪个任务(跨任务的同名子任务不能互相去重)、from 定位是谁交接的(同一任务里 B→D 和 C→D 是两次不同交接)、seq 定位是第几次(同一对 Agent 之间可能多次交接)。

去掉 from 的后果:同一任务里 B→D 的第 1 次交接和 C→D 的第 1 次交接会撞成同一个 key。第二个到达时被幂等守卫当成重复交接,直接返回上一次的结果并静默跳过执行——一个子任务凭空消失,而且日志上看不出异常(幂等命中通常不打 error)。这个 bug 表现为"C 派的活好像没人做",排查时最容易怀疑 C 或 D 的实现,而根因在守卫的 key 构造。

交接为什么必须幂等:交接消息可能重发——网络重试、调度器崩溃恢复、上游超时后误判失败而重发。不幂等时同一子任务被下游执行两遍;如果它带副作用(发消息、扣费),就是真实事故。

最近更新: 2026/9/10 11:38
Prev
Agent 运行时深水区
Next
Agent 评测与线上运营