LLM 面试备考
首页/Agent 架构

多 Agent 协作的基础设施与冲突治理(Multi-Agent Coordination & Conflict Governance)

所属:01-fundamentals / 04-agent-architecture
面试权重:★★★
来源:《深入理解 AI Agent:设计原理与工程实践》第 10 章「多 Agent 协作」(李博杰,github.com/bojieli/ai-agent-book)。书中标注的框架(A2A、AutoGen、Swarm、MetaGPT、灵台等)与数字均有出处,非本仓库实测
相关知识点:multi-agent.md(姐妹篇:多 Agent 的收益判据、代价、六种失败模式清单、LangGraph 共享状态 —— 本篇不重复其内容,只补机制层)、10-tool-discovery.md(协作工具三组原语 spawn / cancel / send_message / list_agents)、11-async-event-driven.md(安全点与取消语义)、memory.md(冲突处理与知识更新)、../../02-interview/design/02-multi-agent-service.md(多智能体设计题)

一句话#

多 Agent 的难点不在「怎么让它们分工」,而在「怎么让它们不互相踩」 —— 而这两件事各有一套成熟答案:分工靠数据平面(共享文件系统交换产物)+ 控制平面(消息、状态、终止、调度),不互相踩靠冲突治理(乐观锁、工作副本隔离、幂等结算点、级联终止)。这套抽象不新鲜 —— 它几乎是操作系统与分布式系统的 LLM 版,所以成熟经验可以整段借用。

为什么重要(面试角度)#

  • multi-agent.md 已经回答了「该不该上多 Agent」和「会怎么坏」。这一篇回答「真上去了,靠什么撑着」。 两篇是判据层与机制层的关系,面试官问完前者,下一句通常就是后者。
  • 它把「多 Agent」接到你已经会的知识上。 多 Agent 系统与操作系统有完整的对应关系(进程 / 内核 / 系统调用 / fork / kill / ps),你不是在学一套新东西,是在给旧知识换名字。这比背框架 API 值钱得多。
  • 它有一个全仓此前零覆盖的高频词:A2A 协议。 Agent Card、任务生命周期、不透明协作三要素,以及它与 MCP 的分工(MCP 管 Agent ↔ 工具,A2A 管 Agent ↔ Agent)。
  • 它给「冲突」的答案比「加个锁」深一层。 乐观锁只能防同一个文件;跨文件的语义冲突要靠工作副本隔离;而并行收口真正难的是**「第一个已验证成功」的幂等结算点** —— 这三个层次能分开讲的人很少。
  • 和你的项目接得上:Writing Copilot 的多智能体故障复盘里,面试官追问的正是「多 agent 冲突谁裁决」(见 ../../02-interview/projects/writing-copilot.md)。这一篇给你的是那题的机制层答案。

⚠️ 边界声明:本篇是《AI Agent》第 10 章的知识梳理,不是你的实战经验。你没搭过多 Agent 平台、没实现过 A2A、没做过并发冲突治理。面试用「这套结构我会这么设计」的口径讲,不要讲成「我们线上就是这么跑的」。可讲成一手经验的只有 Writing Copilot 的中断控制那一小块。

核心内容#

1. 两个正交的设计维度:先把「共享什么」和「听谁的」分开#

维度一:上下文是否共享。 这是最基础的架构决策,它决定信息怎么传。

共享上下文 不共享上下文
形态 后续 Agent 接收前一个的完整轨迹(换 system prompt 和工具集后是新角色,但保留前任的全部记忆) 每个 Agent 有完全独立的上下文与轨迹,看不到对方的"内心活动"
优势 信息不丢失,任何阶段的细节都还能回看 模块化与隔离更好;新增 Agent 不改现有 Agent;某个 Agent 故障不传染
代价 上下文快速膨胀,当前 Agent 被历史干扰 信息同步(状态一致性、丢失与重复);调试要拼多份日志

一个常见误判:以为「共享上下文」就是「一个 Agent 分几个阶段」。书里的定义更严:每阶段切换 system prompt 和工具集后,它就是一个新的 Agent(身份、职责、能力都变了),只是继承了前任的记忆。

不共享上下文就必须显式通信,而通信机制只有三种 —— 它们恰好落在操作系统的 IPC 两大范式里:

机制 对应 IPC 适合
工具调用的参数(把下游 Agent 封装成工具) 消息传递(同步) 需要类型确定、结构清晰
共享文件系统 共享内存 产物较大或需持久化
消息总线(Message Bus) 消息传递(异步投递) 数量多、需解耦

Go 那句广为流传的话可以直接引用来收口:「不要通过共享内存来通信,而要通过通信来共享内存。」

维度二:协作拓扑。 控制权和信息按什么结构流动:

拓扑 形态 适用
对等协作 少量(2–3 个)平等 Agent 多轮互评、迭代改进 需要独立视角与认知多样性
管理者模式 一个中心 Manager 规划调度,子 Agent 各管一块 子任务多、有复杂依赖、需动态调度
去中心化 没有运行时中心控制者,Agent 之间自行发起沟通与移交 需要制衡;或管理者本身是单点故障时

两个维度是正交的:你可以「不共享上下文 + 管理者模式」,也可以「共享上下文 + 对等」。先分别定这两维,再谈具体框架 —— 这也是绕过「Agent Swarm」这类营销词的唯一办法:同一个词既可能指 Swarm 式 handoff 网络,也可能指「主 Agent 动态创建上百个子 Agent」的规模化管理者模式,名字一样、结构相反。

一个高频追问:角色转换该替换 system prompt,还是加载 Skill?

选择 工具可见性 前缀缓存 约束能力
transfer_to_agent(替换 system prompt + 工具集) 只暴露当前角色工具 每次切换都改请求前缀,从变化点起缓存通常无法复用 强:越界工具在 schema 层就不可见
Skill(固定 prompt,按需把 SKILL.md 追加到轨迹) 通常固定暴露工具全集 静态前缀不变,Skill 内容落在轨迹末尾,已有缓存可继续复用 弱:Skill 是行为指令,硬权限仍要 Harness 门

判据:差异只在知识、流程、写作风格 → 用 Skill;差异涉及权限、工具隔离、合规边界,或需要在运行时强制禁止某类动作 → 用独立 Agent / transfer_to_agent,并在 Harness 层用代码限制工具调用。

2. 数据平面:Agent 眼里的文件系统有四个区域#

Agent 访问的不是单一存储,而是一棵虚拟文件系统 —— 来源、生命周期、权限各异的存储被"挂载"到同一目录树下,Agent 通过统一的 read_file / write_file / list_dir 访问,底层可能是本地临时盘、持久对象存储、第三方云盘 API 或只读资源包。

一句值得记住的话:相当一部分并发冲突与信息泄露,源于把本应隔离的区域混置。 这棵目录树相当于 Agent 的地址空间,四类区域就是权限各异的"内存段"。

区域 可见性 生命周期 读写 并发控制
① Agent 专属工作区(Scratchpad) 仅该 Agent 随实例销毁 读写 不需要(私有)
② 多 Agent 共享空间 所有协作 Agent + 用户 随任务,需持久化 读写 需要(乐观锁 / worktree)
③ 外部挂载资源(Drive / Notion / Wiki…) 视外部授权 由外部源决定 多为只读,写需谨慎 由外部源负责
④ 系统内置资源(Skills / 模板 / 共享工具定义) 所有 Agent 跨会话稳定 只读 不需要(只读)

每一区域解决一个具体问题,也就各有一处要防的东西:

  • ① 私有工作区 一举两得:既避免多个 Agent 的临时文件互相覆盖,又保持主 Agent 上下文精简 —— 子 Agent 的试错留在自己工作区,只把最终产物提交到共享空间。这正是「子 Agent 返回结构化摘要而非全量轨迹」在存储层的体现。
  • ② 共享空间 是并发冲突的高发处(乐观锁、worktree 都作用在这里),也是用户上传 / 下载的入口。
  • ③ 外部挂载 有三项不同于本地存储的特性,必须显式处理:受外部权限约束(用户在源系统的权限决定可见范围)、延迟高且一致性弱(每次读是网络往返、数据可能已被外部改动,只能按最终一致性对待)、以按需只读为主(写回会污染用户的真实数据)。统一文件接口把这些差异掩盖了,所以要在挂载层显式管理只读/可写、超时与凭证边界。
  • ④ 内置资源 全局共享、只读、跨会话稳定,可被所有 Agent 并发读取而无需并发控制 —— 这是四区域里唯一"免费"的一类。

「文件路径作为通用接口」是这套设计的价值所在:Agent 之间传产物、主 Agent 给子 Agent 交接输入、乃至跨组织 A2A 交换产物,传的都是轻量的路径字符串,而不是把内容塞进上下文窗口。

3. 控制平面:消息、状态、终止、调度#

文件系统解决了产物交换,协作还需要一条控制平面。这一节就是操作系统里 fork / ps / kill / wait 的对应物。

① 消息传递:点对点 vs 消息总线,都要带"信封"。 点对点(A 直接调 send_message_to_agent_b)适合拓扑固定、Agent 少的场景;Agent 一多就出问题 —— 连接数随 Agent 数呈平方增长,且要求收发双方同时在线,此时改用消息总线:发布到总线、按订阅转发,发送方不必知道消费者是谁。两种形态下消息都应携带结构化信封:发送者 ID、目标(指定或广播)、消息类型(task_assigned / status_update / result / terminate)和 JSON 负载。统一的信封保证接收方能可靠路由与解析,也让协作链路可追溯 —— 这是多 Agent 调试的关键。

② 状态查询:这是控制平面里最容易被低估的一环。 主 Agent 派出子 Agent 后,若无从获知其进展,既无法判断该不该继续等,也无法在它卡住时介入。直觉做法是定义 get_subagent_status(agent_id) —— 但这个拉取式接口并不实用:子 Agent 一经创建就开始跑,完成或失败时自然会通知,并不需要主 Agent 显式轮询。更自然的做法是回到两大通信范式:

  • 用消息传递获取状态:主 Agent 发一句"进展如何?",子 Agent 在合适时回复,全程异步、不阻塞自己 —— 就像经理用 IM 问进度,不要求下属立刻停手。反过来说,子 Agent 也该在关键节点主动汇报。无论问还是报,状态本身宜采用统一的状态机词汇(执行中 / 需要输入 / 已完成 / 失败)—— A2A 协议正是把任务生命周期标准化成这一组状态。
  • 用共享文件系统获取状态:最彻底的形态是轨迹持久化(trajectory persistence)—— 子 Agent 把自己的完整轨迹实时序列化为 JSON,追加写入日志文件(通常每会话一个文件、每行一个事件,即 JSONL)。主 Agent 无需任何状态上报协议,直接读文件就能看到它全部执行过程:正在调哪个工具、最近在想什么、是不是卡在反复失败的重试上。用进程的语言说,这相当于直接读另一个进程的内存。

    ⚠️ 但它不宜作为主要方式:轨迹动辄数万 token,主 Agent 读完还得自行提炼,费时费 token。

  • 更合理的是约定进度文件:启动子 Agent 时约定"把进度写到 progress.md",子 Agent 每完成一项就更新。主 Agent 随时读这个轻量文件即可 —— 相当于两块进程在共享内存里划出一小块约定格式的状态区。它还有一个额外用途:检测卡住状态 —— 文件最后修改时间超过 N 分钟没变,即可判定无活动、触发超时兜底。

这三条合起来是一张分层表,值得背下来当检查清单:要完整过程用轨迹持久化(重、用于调试与审计);要进展概览用进度文件(轻、用于调度决策与卡死检测);要低延迟通知用消息(异步、用于事件驱动的协调)。

③ 执行终止:两种强度,以及"孤儿 Agent"。 并行协作里常见「一个成功、其余无需继续」,此时终止有两种强度 —— Unix 用户会认出这正是 SIGTERM 与 SIGKILL 的区别:

  • 优雅终止(首选):发 terminate 信号 → 子 Agent 在当前步骤的安全点响应 → 先清理资源(关浏览器会话、写未完成文件、释放锁)→ 返回 ack → 退出。
  • 强制终止(兜底):直接终止进程,仅在优雅信号无响应时用,代价是可能遗留悬挂资源与未完成写入。

还剩一个问题:主 Agent 终止后,仍在跑的子 Agent 怎么办? 最简洁的工程做法借鉴 Go 的 context —— 终止沿创建关系向下级联:取消一个 Agent,它派生的所有子 Agent 随之取消,从根上杜绝无人认领的孤儿 Agent。反过来,若确实需要脱离父级长期运行的后台 Agent(类似 Unix 的 nohup),就让它从一棵新的生命周期树起步(对应 context.Background()),显式声明不随父级终止。

④ 资源与调度:稀缺的不是 CPU,是 token 和并发额度。 进程世界稀缺的是 CPU 时间和内存,Agent 世界稀缺的是 token 和并发额度 —— 并发数设上限,避免几十个 Agent 同时打满 API 配额。调度通常落在管理者 Agent 或运行时身上:启动子 Agent 时设定步数或 token 预算、超限即止;困难任务给强模型、机械任务给低成本模型;并发到上限而更紧急的任务到来时,打断执行中的子 Agent,这就是抢占。

管理者 Agent 相对传统调度器的独特优势:它有推理能力。 它可以并行启动多个子 Agent 探索同一问题,根据各自进展决定给谁更多资源、砍掉哪个看起来误入歧途的 —— 像公司内部赛马。但这一领域远不如操作系统调度成熟,它决定系统的成本上限,应在架构阶段就纳入考虑。

4. 冲突治理:三个层次,别停在「加个锁」#

multi-agent.md 的六种失败模式已给出分类与对策;这一节补它的机制层 —— 每一条对策具体怎么做才不出漏洞。

层次一:同文件写冲突 → 乐观锁。 每个文件维护版本号(或最后修改时间戳):读时记下版本号,写时校验是否仍与读取时一致;不一致就写入失败,Agent 重新读最新版、在此基础上重做。代价是偶尔需要重试,换来的是数据一致性。

层次二:跨文件语义冲突 → 乐观锁管不了,要更高层的语义校验。 典型案例:Agent A 重编全书图片编号,Agent B 同时改某章内容并引用旧编号。两者操作的是不同文件,文件层面完全没有冲突,但结果是 B 的引用全部失效。这一类更隐蔽也更危险,需要的是"这个改动会不会让别处的引用失效"这种语义级校验,而不是文件锁。

层次二的最实用工程解:工作副本隔离。 在「多个 Coding Agent 并发改同一代码库」这个最常见场景里,业界主流做法是给每个 Agent 分配独立的 Git 分支或 worktree,各自在副本上并行修改、互不干扰,把冲突集中推迟到最后的合并点。合并点是人或另一个 Agent 可以显式检查与裁决的地方 —— 这比"事中互相踩"可控得多。

层次三:并行收口的竞态 —— 结算点必须是「第一个已验证成功」。 这一层最少人讲。多个 Agent 并行搜索、一个命中后其余应停,于是要定义结算点:

event = next_event()
if event.type == RESULT:
    if verify(event.artifact, hidden_checks):     # 先验证
        if not settle_once(event):                # 再原子地认领
            continue
        broadcast_cancel(to = workers - {event.worker_id})
        await_all_ack_or_timeout()
        return assemble(event.artifact, evidence = event.evidence)

两个细节决定成败:

  • verify 不能省:「第一个声称成功」不等于「第一个已验证成功」。 直接按第一个返回结果收口,就是把一个未验证的结论当成结论。
  • settle_once 必须幂等(通常由锁或事务保护)。 否则两个几乎同时到达的成功事件会触发两次汇总 —— 这就是竞态条件(Race Condition):Agent A 和 B 在同一毫秒各自报告"我找到了",若主 Agent 收 A 的报告就开始汇总、紧接着又收 B 的触发第二次汇总,就会产生重复结果或互相矛盾的状态。解法是第一个报告到达后立即锁定状态,后续报告识别为重复并忽略。

这一层还有一个前置纪律:区分「失败」与「未完成」。 广播 terminate 之后要 await_all_ack_or_timeout() —— 等所有确认或超时,因为你无法保证每个子 Agent 都还活着并会回复。

横向的一层:防共因失效。 同一模型 + 相似上下文产生的多条结论不能自动当作独立证据;工程上要有意识地制造差异(不同模型、不同上下文、不同数据来源),并用命名空间、资源配额、速率限制防止相同决策同时冲击共享资源(实证:30 个同时上线的 Agent 有 18 个创建了同名 Git 分支 —— 详见 multi-agent.md)。

一句话把三层串起来:同文件用乐观锁,跨文件用工作副本,收口用幂等结算点。 别人只答第一层时,你答到第三层就是分层。

5. 跨组织:A2A 协议,以及它和 MCP 的分工#

前面所有机制都假设所有 Agent 由同一个团队开发、跑在同一个系统内 —— 此时参数传递、共享文件、消息总线三种机制足够用。但当协作跨越组织边界(你的 Agent 要调用另一家公司的 Agent),就需要标准化的互操作协议。 2025 年 Google 发布、后捐赠给 Linux 基金会托管的 A2A(Agent2Agent)就是干这个的。它之于 Agent,就是网络协议之于进程。

三个核心要素:

要素 内容 解决什么
Agent Card 一份描述 Agent 能力的元数据文档(发布在约定公开地址下):能做什么、支持哪些输入输出模态、如何认证 跨组织的能力发现(相当于 Agent 的"名片")
任务生命周期管理 把协作单元建模为任务(Task),带明确状态机(已提交 / 进行中 / 需要输入 / 已完成 / 失败),原生支持长任务与流式进度更新 跨组织任务的状态对齐
不透明协作 Agent 之间只交换任务与产物(Artifact),不暴露内部提示词、思考过程和工具实现 与「不共享上下文」原则一致,也是跨组织必要的安全属性

与 MCP 的分工要说准 —— 这是最容易被问混的一处:

MCP 解决 Agent ↔ 工具的互操作;A2A 解决 Agent ↔ Agent 的互操作。 前者管"Agent 能调什么",后者管"Agent 能找谁、怎么托付任务"。A2A 并不取代三种通信机制,而是在它们之上、跨信任边界的标准化层。

因此它有一个明确的适用边界:同一团队内部的多 Agent 系统直接用消息总线即可;只有协作方互不信任、实现互不可见时,才需要 A2A 这样的公开协议。上来就说"我们用 A2A 做内部编排",是典型的用力过猛。

⚠️ 别和另外两个缩写混:09-coding-agent.md 里提到的 A2UI(界面描述协议)与 AG-UI(事件/传输协议)都不是 agent-to-agent 协议。三个缩写的字母表差不多,管的事完全不同。

6. 这一篇和 multi-agent.md 怎么配合着答#

两篇是同一章的两个切面,面试时按问题类型分流,不要互相顶替:

面试官问的是 去哪一篇 关键词
该不该上多 Agent? multi-agent.md 判据层:是否引入单 Agent 拿不到的新信息;先单 Agent、遇瓶颈再拆
多 Agent 会怎么坏? multi-agent.md 六种失败模式、错误复合 pnp^n、崩溃故障 vs 拜占庭故障、《Why Do Multi-Agent LLM Systems Fail?》(14 模式 / 3 类 / kappa 0.88)
真上去了靠什么撑着? 本篇 数据平面(虚拟文件系统四区域)+ 控制平面(消息 / 状态 / 终止 / 调度)
两个 Agent 打起来了怎么办? 本篇 §4 乐观锁 → 工作副本 → 幂等结算点;预设优先级 / 所有权 / 权限边界
跨公司协作怎么做? 本篇 §5 Agent Card / 任务状态机 / 不透明协作;A2A vs MCP

一个能立住的开场:「多 Agent 值的判断在 multi-agent 那一层,我先说结论 —— 多数场景先单 Agent;如果真的上,真正的工程工作量不在编排逻辑,而在数据平面和控制平面,以及冲突治理的三个层次。」

常见面试问法#

Q:多个 Agent 之间怎么通信?三种机制怎么选? A:先承认它们就是 IPC 的两大范式。共享文件系统 = 共享内存(Agent 世界的"共享内存"),适合产物较大或需持久化;工具调用参数 = 同步消息传递,适合类型确定、结构清晰;消息总线 = 异步消息传递,适合 Agent 多、需解耦(点对点的连接数随 Agent 数平方增长,且要求双方同时在线)。可以引 Go 那句收口:不要通过共享内存来通信,而要通过通信来共享内存。再加一句实操:无论哪种,消息都应带结构化信封(发送者、目标、类型、JSON 负载),否则链路无法追溯、多 Agent 调试会变成拼图。

Q:不共享上下文的多 Agent,主 Agent 怎么知道子 Agent 在干什么? A:先否掉"轮询状态接口"这个直觉答案 —— get_subagent_status() 并不实用,因为子 Agent 一经创建就开始跑,完成或失败自然会通知。更自然的是回到两大通信范式:① 用消息 —— 主 Agent 发"进展如何"、子 Agent 择机回复,全程异步不阻塞;反过来子 Agent 也该在关键节点主动汇报。状态本身要用统一的状态机词汇(执行中 / 需要输入 / 已完成 / 失败)—— A2A 正是把这组状态标准化了。② 用文件 —— 最彻底的是轨迹持久化(JSONL,每会话一文件、每行一事件),主 Agent 直接读就能看到它全部过程,相当于读另一个进程的内存;但它动辄数万 token、太重。所以常规做法是约定进度文件(如 progress.md),轻量、够用,还能靠"最后修改时间超过 N 分钟没变"检测卡住。

Q:一个 Agent 卡住了或者失控了,怎么终止? A:终止有两种强度,对应 SIGTERM 与 SIGKILL。 优雅终止是首选:发 terminate 信号,子 Agent 在当前步骤的安全点响应,先清理资源(关会话、写未完成文件、释放锁),回 ack 后退出。强制终止只在优雅信号无响应时用,代价是遗留悬挂资源和未完成写入。还要回答一个容易漏的问题:主 Agent 终止后,它派生的子 Agent 怎么办? 借鉴 Go 的 context,终止沿创建关系向下级联 —— 取消一个 Agent,它派生的全部随之取消,从根上杜绝孤儿 Agent;若确实需要脱离父级长期运行的后台 Agent,就让它从新的生命周期树起步并显式声明不随父级终止。最后补一句并发收口的纪律:广播取消后要 await_all_ack_or_timeout() —— 因为不能保证每个子 Agent 都还活着并会回复。

Q:两个 Agent 同时写同一个文件怎么办?乐观锁够吗? A:乐观锁只解决第一层,三层要分开答。 ① 同文件写冲突 → 版本号 + 写前校验,不一致就重新读最新版重做,代价是偶尔重试。② 跨文件的语义冲突 → 乐观锁完全管不了:A 重编图片编号、B 在别处引用旧编号,文件层面毫无冲突,但 B 的引用全失效 —— 这类更隐蔽也更危险,要语义级校验。最实用的工程解是工作副本隔离:给每个 Agent 独立的 Git 分支 / worktree,各自改、把冲突推迟到显式的合并点(这一点在多个 Coding Agent 并发改同一仓库时是业界主流做法)。③ 并行收口的竞态 → 结算点必须是**"第一个已验证成功"而不是"第一个声称成功",且认领动作 settle_once 必须幂等**(锁或事务保护),否则两个几乎同时到达的成功事件会触发两次汇总。一句话总结:同文件用乐观锁,跨文件用工作副本,收口用幂等结算点。

Q:多个 Agent 并行搜索,一个先找到,怎么收口? A:先把"找到"和"验证找到"分开。 步骤是:收到结果 → 先 verify(隐藏检查)→ 通过后再 settle_once 原子认领 → 广播 terminate 给其余 → await_all_ack_or_timeout() → 汇总。两个坑:① 省掉 verify 就等于把第一个未验证的返回值当结论;② settle_once 不幂等就会出现竞态 —— A、B 同一毫秒各报"我找到了",收 A 后开始汇总、紧接着 B 又触发一次汇总,结果是重复或矛盾的状态;解法是第一个到达即锁状态,后续识别为重复并忽略。另外别忘了超时兜底:等 ack 要有上限。"一个 Agent 命中就全部停下"这件事,看起来简单,真正难的是幂等和超时。

Q:什么时候需要 A2A?它和 MCP 什么关系? A:A2A 解决的是"跨信任边界的 Agent ↔ Agent 互操作",MCP 解决的是"Agent ↔ 工具互操作" —— 一句话分开:MCP 管能调什么,A2A 管能找谁、怎么托付任务。三要素:Agent Card(能力发现的名片)、任务生命周期管理(任务状态机:已提交 / 进行中 / 需要输入 / 已完成 / 失败,原生支持长任务与流式进度)、不透明协作(只交换任务与产物,不暴露内部提示词、思考与工具实现)。关键是说清它不取代什么:它不取代参数传递 / 共享文件 / 消息总线这三种机制,而是架在它们之上的一层。所以适用边界很窄:同一团队内部用消息总线就够了,只有协作方互不信任、实现互不可见时才需要。最后提一句别混:A2UI(界面描述)和 AG-UI(事件/传输)都不是 agent-to-agent 协议。

Q:多 Agent 为什么可以直接借用操作系统的经验? A:因为两者的抽象几乎一一对应:静态前缀(system prompt + 工具定义)↔ 程序可执行文件;轨迹 ↔ 进程内存;LLM ↔ CPU;Agent 运行时 ↔ 内核;工具调用 ↔ 系统调用;spawn_subagent ↔ fork;cancel_subagent ↔ kill;list_agents ↔ ps;子 Agent 返回的结构化摘要 ↔ 退出码与 wait();共享文件系统 / 消息 ↔ 共享内存 / 消息传递。这套设定其实就是 1970 年代 Actor 模型(私有状态、异步消息、可创建新成员)的 LLM 版。所以私有状态、异步消息、生命周期级联、孤儿回收、抢占调度这些成熟经验可以整段借用 —— 也正因为如此,判据不该是"像不像人",而是"这一步在操作系统里对应什么"。

易错点#

易错说法 问题 应该说
「共享上下文就是同一个 Agent 分几个阶段」 定义偏松 切换 system prompt 和工具集后就是一个新 Agent(身份 / 职责 / 能力都变了),只是继承了前任的记忆
「Agent 之间通信方式很多」 没归类 只有三种,且落在 IPC 两大范式里:共享文件系统 = 共享内存;工具参数 = 同步消息;消息总线 = 异步消息
「主 Agent 应该有 get_status 轮询接口」 直觉但不对 子 Agent 完成或失败会自然通知;更自然的是用消息(异步问答 / 主动汇报)或进度文件获取状态
「用轨迹持久化做 Agent 间的信息传递」 太重 轨迹动辄数万 token、还要主 Agent 自己提炼;常规做法是约定进度文件(progress.md),轨迹留给调试与审计
「发个 terminate 就停了」 漏了两件事 ① 优雅终止要先在安全点清理资源(含释放锁)并回 ack;② 要等 ack 或超时,不能假设对方还活着
「取消主 Agent 就行了」 漏了孤儿 终止要沿创建关系向下级联(Go context 的做法),否则留下无人认领的子 Agent;需要长驻的要显式起新的生命周期树
「加乐观锁就解决并发冲突了」 只答了一层 乐观锁只防同一文件;跨文件语义冲突要更高层校验,常用**工作副本隔离(Git 分支 / worktree)**把冲突推迟到合并点
「谁先返回就听谁的」 把"声称"当"验证" 结算点必须是第一个已验证成功;先 verify 再收口
「收口逻辑怎么写都行」 漏了竞态 settle_once 必须幂等(锁 / 事务),否则两个几乎同时的成功事件触发两次汇总
「同一模型给的意见就是独立证据」 共因失效 相似上下文的多个结论不能自动视为独立;要刻意制造差异,并用命名空间 / 配额 / 限流防同批决策冲击同一资源
「内部编排也用 A2A」 用力过猛 同团队内部用消息总线即可;A2A 只在跨信任边界、实现互不可见时才需要
「A2A 和 MCP 是一回事 / 是竞争的」 混淆分工 MCP = Agent ↔ 工具;A2A = Agent ↔ Agent;A2A 不取代三种通信机制,是架在其上的标准化层
「A2A / A2UI / AG-UI 差不多」 缩写陷阱 A2UI 是界面描述协议、AG-UI 是事件/传输协议,都不是 agent-to-agent 协议
「共享区域越方便越好」 混置区域是冲突源 相当一部分并发冲突与信息泄露,源于把本应隔离的区域混置;四区域各有各的可见性与生命周期,先定布局再谈机制
把本章讲成「我们线上就是这么跑的」 触碰仓库硬规矩 这是书里的框架与出处,非一手经验;只能用「这套结构我会这么设计」的口径,一手经验只有 Writing Copilot 的中断控制

自测#

1. 一个多 Agent 文档翻译系统:术语表 Agent、章节翻译 Agent(多实例并行)、全文审校 Agent、一个 Manager。你会怎么设计它的数据平面和控制平面?

答案

先在数据平面上把四个区域划出来(这是最容易被跳过、却最容易出事的一步):

区域 在这个系统里是什么 并发控制
① 专属工作区 每个翻译 Agent 实例的草稿、术语试译、调试日志 不需要(私有)——同时避免临时文件互相覆盖、并让 Manager 上下文保持精简
② 共享空间 术语表(JSON/CSV)、各章译文(chapter1_zh.md…)、审校报告;用户在此上传原书、下载成稿 需要:多人(多 Agent)读写同一批文件,术语表尤其会被反复读写 —— 用乐观锁(版本号 + 写前校验);译文按章一文件、天然分片,冲突面小
③ 外部挂载 若术语来源是外部词典 / Wiki 按需只读,写回要谨慎、受外部权限约束、一致性弱
④ 内置资源 翻译指南、风格模板、共享工具定义 只读,可并发读取、无需控制

控制平面四条:

  • 消息:Manager 与各 Agent 之间的任务分派与汇报,带结构化信封(发送者 / 目标 / 类型 / 负载),类型如 task_assigned / status_update / result。Agent 多时用消息总线,避免点对点连接数平方增长。
  • 状态查询:不要给 Manager 做 get_status 轮询。约定每个翻译 Agent 维护一份进度文件("第 3/12 章完成"),Manager 随时读;顺便用它检测卡住 —— 某实例进度文件超过 N 分钟没更新就判定无活动、触发超时。
  • 执行终止:审校发现某章不合格时,Manager 要能取消对应的翻译实例 —— 优雅终止:发 terminate,实例在安全点收尾(写完当前章、释放文件锁)后回 ack。Manager 终止时,它派生的所有翻译实例要沿创建关系级联取消,不留孤儿。
  • 资源与调度:给每个翻译实例设步数与 token 预算;并行的章节数设并发上限,避免打满 API 配额;复杂章节用强模型、机械段落用便宜模型。

这个案例里最值得主动讲的一点:术语表是这个系统最容易出并发问题的地方 —— 它被 Glossary Agent 写、被所有 Translation Agent 读、被 Proofreading Agent 复核。它应该放在共享空间、用乐观锁保护,而不是放在某个 Agent 的私有工作区。

2. 上面那个系统里,两个翻译 Agent 同时发现了一个术语表里没有的新术语,各自推断了一个译法并写回术语表。会发生什么?你怎么防?

答案

先说会发生什么 —— 分两种情况:

  1. 同一文件的写冲突(简单冲突):两个 Agent 几乎同时读术语表、各自新增自己的译法、先后写回,后写的把先写的覆盖掉 —— 第一个 Agent 的译法消失,而且它自己并不知道。
  2. 更隐蔽的是语义冲突:假设两者写的是不同文件(比如各自的和译法记在自己的笔记里),文件层面毫无冲突,但全书术语一致性被破坏 —— 这正是"跨文件语义冲突"的形态,读者会在第 3 章看到"智能体"、第 9 章看到"代理"。

防法分三层:

  • 第一层:乐观锁。 术语表带版本号;Agent 读时记版本,写时校验 —— 发现版本已变就写入失败,重新读最新版再决定(可能发现自己推断的译法和已有的冲突,于是改为沿用或标注待审)。代价是偶尔重试,收益是不丢数据。
  • 第二层:语义级校验。 乐观锁只防同一文件的覆盖,防不住"译法不一致"本身。这里需要的是职责隔离:书里的架构本来就规定 Translation Agent 遇到对照表里没有的新术语,只"推断翻译并标记为待审查",不直接改术语表 —— 把唯一的写权限收敛给 Glossary Agent(或由 Proofreading Agent 统一裁决)。这比加锁更根本:不是让并发写变安全,而是让它不发生。
  • 第三层(若坚持要并发写):工作副本 + 合并点。 让每个 Agent 在自己的副本上改,把冲突集中到显式的合并 / 审校环节由 Proofreading Agent 或人裁决 —— 与多个 Coding Agent 用 Git 分支 / worktree 是同一个套路。

这个题真正的考点是第二层:能不能从"加锁"上升到"改权限与职责划分"。加锁是让并发安全,收敛写权限是让并发消失 —— 后者通常更好。

3. 你的多 Agent 系统有 10 个子 Agent 并行搜索,找到一个就全部停止。为什么"第一个返回就收口"是错的?正确流程是什么?

答案

错在把一个"声称"当成了"结论"。 第一个返回的不一定是第一个真的找到的 —— 它可能返回了一个看似匹配、实际不对的结果;按它收口,整个系统的产出就建立在一个未验证的前提上。这是多 Agent 里最典型的拜占庭式失败:Agent 不会报错,它会继续给出看似可信的错误结论。

正确流程(五步):

  1. 收到 RESULT 事件;
  2. 先 verify(artifact, hidden_checks) —— 用验证器判定,而不是信任返回方;
  3. 验证通过后 settle_once() 原子认领 —— 这一步必须幂等(由锁或事务保护);
  4. 广播 terminate 给其余 worker;
  5. await_all_ack_or_timeout() —— 等全部确认或超时,再 assemble 结果。

三个必须讲到的细节:

  • 为什么 settle_once 要幂等? 因为竞态条件:Agent A 和 B 可能在同一毫秒各自报告"我找到了"。若主 Agent 收 A 的报告就开始汇总、紧接着收 B 的又触发第二次汇总,就会产生重复结果或互相矛盾的状态。解法是第一个报告到达即锁定状态,后续报告识别为重复并忽略。
  • 为什么要 await_all_ack_or_timeout? 因为你无法保证每个子 Agent 都还活着、并且会回 ack —— 没有超时兜底,收口可能永远等下去。
  • 终止本身要走优雅路径:发 terminate 后,子 Agent 在安全点响应并清理资源(关浏览器会话、释放锁)再 ack;只有无响应时才强制终止。

如果全部失败,也要有明确出口:汇总各失败原因、向用户报告"未找到"及各失败原因统计,而不是静默返回空。

4. 有人提议「我们内部这几个 Agent 用 A2A 来编排」。你怎么评价?

答案

大概率是用力过猛 —— 但先别急着否定,先问清一件事:这几个 Agent 是不是同一个团队开发、跑在同一个系统内、彼此信任且实现可见。

如果是(绝大多数内部编排都是),那 A2A 用错了地方。 A2A 的定位是跨信任边界的 Agent ↔ Agent 互操作协议 —— 它的三个要素里,「不透明协作」这一条就说明了它是为"互不信任、实现互不可见"设计的(Agent 之间只交换任务与产物,不暴露内部提示词、思考过程与工具实现)。内部协作里这些恰恰是共享的,用 A2A 只是给消息总线套了一层额外的协议开销。

内部编排应该用什么? 回到三种通信机制:工具调用参数(结构化、同步)、共享文件系统(大产物、持久化)、消息总线(解耦、异步)。书里给的判据很清楚:同一团队内部的多 Agent 系统直接用消息总线即可;只有当协作方互不信任、实现互不可见时,才需要 A2A 这样的公开协议。

那什么时候 A2A 确实正当? 例如:你的 Agent 要调用另一家公司(或另一个部门、另一个安全域)的 Agent —— 你既不知道对方怎么实现,也不该把内部提示词和思考过程交出去,同时需要一种标准方式发现对方能做什么(Agent Card)、对齐任务状态(已提交 / 进行中 / 需要输入 / 已完成 / 失败)。这才是它解决的问题。

顺带把两个容易混的分工说清:MCP 管 Agent ↔ 工具的互操作,A2A 管 Agent ↔ Agent 的互操作 —— 一个管"能调什么",一个管"能找谁、怎么托付"。并且要强调 A2A 不取代三种内部通信机制,它是架在其上的一层。

5. 「多 Agent 系统不就相当于分布式系统吗?那套经验能直接用?」你怎么答?

答案

方向对,而且可以答得更具体 —— 因为对应关系几乎是逐项成立的(书里给了完整对照表):

操作系统 / 分布式系统 多 Agent 系统
程序(可执行文件) 静态前缀(system prompt + 工具定义)
进程的内存 轨迹
CPU LLM
内核 Agent 运行时
系统调用 工具调用
fork spawn_subagent
kill cancel_subagent
ps list_agents
退出码与 wait() 子 Agent 返回的结构化摘要
共享内存 / 消息传递 共享文件系统 / 消息

这套抽象并不新鲜:私有状态、异步消息、可创建新成员,正是 1970 年代 Actor 模型的基本设定 —— 多 Agent 系统不妨看作它的 LLM 版本。所以成熟经验确实可以整段借用:生命周期级联与孤儿回收(Go 的 context)、SIGTERM / SIGKILL 式的两级终止、生产者消费者式的消息总线、锁与版本号、抢占与配额。

但必须紧接着说清楚三处不能照搬的地方:

  • 故障类型不同 —— 这是最根本的一条。 分布式容错理论把故障分崩溃故障(停摆)与拜占庭故障(继续工作但给错误信息)。传统分布式系统大多只需防崩溃;Agent 的故障天生是拜占庭式的 —— 它很少径直停止,而是继续给出看似可信的错误结论,且错误不会主动声明自己是错误。所以传统系统里"等它报错"的兜底思路在这里失效,交叉验证与多数表决这类拜占庭容错手段成了必需品。
  • 传递是有损的。 进程之间传字节是逐位保真的;Agent 之间传语义,每转述一次都是有损的重新编码 —— 这就是"错误的级联放大"的根源。
  • 调度远不成熟。 操作系统调度器有明确的资源与目标函数;Agent 调度的稀缺资源是 token 和并发额度,实践中很大程度上靠管理者 Agent 的推理能力现场判断(谁该多给资源、谁该被砍掉),远不如 OS 调度成熟,而且它直接决定成本上限。

收口:照搬的是结构(数据平面 / 控制平面 / 生命周期),不能照搬的是故障模型 —— 前者省了大量设计工作,后者是 Agent 特有的坑。