Agent Team 协调模型

以 mailbox 为中心的多代理协调模型——设计哲学、原语、工具表面

理想模型——不是已落地的实现。 本文描述的是一套以 mailbox 为中心的 actor 设计(按 member 的 mailbox、send_message / task_create / task_update / task_complete / team_member_addworking ⇄ idle 的 wake 循环),代码并未采用它。真正落地的是 Agent Team(当前实现) 里以 member 为中心的 DAG——team_create 一次性声明整个 DAG、team_replan 做失败恢复、team_send_message 递送、member 经通用 complete 交付。把本页当作设计意图 / 北极星,而非当前行为——真正落地的细节见 Agent Team(当前实现)

team 内部:全靠 mailbox,没有第二条通道

一个 team 里,Lead 和 member 怎么协同、消息怎么递送、member 干完一轮是死是活、Lead 不在场时后台进展怎么送到操作员眼前——这些“团队内部如何运转”的问题,本页来答。(什么时候该用 team、什么时候用单个 task,是另一层边界,见 多 Agent。)

答案的内核只有一件事:team 内部的一切协调都走 mailbox——没有共享内存、没有事件总线、没有 long-poll。下面先讲支撑这一点的三条设计哲学,再落到四个原语、状态机与工具表面。全文以抽象方式陈述模型(原语、状态、动作、语义),不绑定具体存储 / 进程 / 网络选型。

设计哲学

思想 1:消息即下一轮

Agent 不“挂着等”。 没有“agent 处于休眠状态、等待事件唤醒”这种抽象。

每一次 LLM 调用都是新的一轮(turn),context 从持久化 mailbox 重建。Member 完成一轮工作 → 它的成果通过消息进入 Lead 的 mailbox → Lead 下一轮启动时,这条消息作为 user-role message 注入这一轮的开头,Lead 自然地读到、推理、回应。

这条思想消解了一个伪问题:“agent 一轮结束后,如何让它继续思考”。答案是:不让它继续思考,而是让下一轮自然处理累积的消息。从 LLM 的视角看,agent 没有“中断”和“恢复”,它每次都是新生的,但 mailbox + transcript 提供了连续性。

这条思想也直接定义了 “操作员的会话生命周期 < 工作时长” 这个 team 价值主张如何实现——不是靠保住一个长 stream,而是靠任何一轮都可以从持久化状态冷启动

思想 2:Member 是长生命周期的可寻址实体

Member 不是 stream,是 actor。

如果 member 的生命周期等于它 stream 的生命周期,就会逼出一套副作用契约:“member 必须在一轮结束前 deliver,否则 task 自动 fail”。这个契约是 stream 模型的产物,不是工作的本质

真实的工作经常是“干一轮、给 Lead 看、Lead 调方向、再干一轮”。Member 完成一轮工作后进入 idle 状态,逻辑上它仍然活着、可被寻址、有自己的 mailbox。Lead 后续要追问、修改 criteria、补充信息,直接 send_message 给这个 member,它 wake 起来,在已有 transcript 基础上跑下一轮。

“Idle” 是状态机里的一等公民,不是“已死但还有记录”。具体到实现,idle 可以是 OS 进程挂着、可以是 stream 退出 + 冷启动,模型不规定——模型只规定对调用方而言,member 一直在那里

思想 3:Team / Member / Task 是三个正交原语

不应该把任何两个绑死在一个工具的 discriminated union 里。

原语是什么是什么的容器
Team命名空间 + 成员名册Member 和 Task 的 scoping container
Member可寻址 actorMailbox 的所有者
Task工作单元自身独立存在,只是恰好被 owner 字段关联到 member

把 task 操作和 team 操作压在同一个工具里,把 task 绑死在 team 工具组里——这是耦合的产物,不是工作的本质。

正交化之后:Task 工具组独立,可以脱离 team 使用(简单 chat 里跟踪 5 件杂事不需要起 team);Member assignment 用 task 的 owner 字段,不需要 claim 这个动词;Team 的角色收窄成 metadata + namespace,不再是任何动作的入口。

四个原语

Team

Team {
  id              identity
  conversationId  identity      ← 1:1 对应一个对话会话
  name            string
  status          "active" | "dissolved"
  createdAt, dissolvedAt?
}

Team 是 scoping container,提供:

  • 一个 member 名册的命名空间(同 team 内 member name 唯一)
  • 一个 task list 的命名空间(对应一组 task)
  • 跟对话会话 1:1 绑定——一个对话会话最多一个 active team

Team 本身不持有任何运行状态。它的“状态变化”完全由 member / task 的状态变化派生。

Member

Member {
  id              identity
  teamId          identity
  name            string        ← human-readable, "Sourcer" / "Lead"
  role            "lead" | "member"
  agentType       string        ← 决定它加载哪些工具、哪个 system prompt
  status          "spawning" | "working" | "idle" | "shutdown"
  workspace       path          ← 隔离工作空间
  createdAt, lastTurnAt?
}

Member 是长生命周期的可寻址 actor。生命周期由 status 字段表达:

状态含义转入触发转出触发
spawning已创建,首轮尚未开始team_member_add首轮启动
working当前有一轮在跑mailbox 写入 + wake一轮自然结束 / abort
idle无进行中的轮,等待新消息一轮结束且 mailbox 已清空mailbox 收到 wake-triggering 消息
shutdown终态,不再 wakeshutdown 协议完成 / dissolve(终态)

working ⇄ idle 是核心循环。Member 的“活着”等于这两个状态之间的振荡能力。

Lead 是 member 的特例:role: "lead",name 固定为 "Lead",workspace 是 team root,负责面向用户的综合。其他方面跟普通 member 一模一样——它也有 mailbox(收用户消息和其他 member 的 task notification),它也在 working ⇄ idle 之间循环。从模型角度,Lead 不是“协调者”这个特殊抽象,只是恰好跟对话用户对话的那个 member

Mailbox

MailboxMessage {
  id              identity
  teamId          identity
  toMemberId      identity      ← 收件人
  fromMemberId    identity?     ← 缺省表示框架注入(用户消息、task notification)
  kind            MessageKind   ← 见下表
  content         payload       ← kind-specific 结构
  createdAt
  consumedAt      timestamp?    ← 缺省表示未读
}

Mailbox 是 member 维度的有序消息队列。它是 team 协调的唯一通信机制 ——没有“member 之间共享内存”、没有“事件总线 + 订阅”、没有“long-poll”。一切都是消息。

消息 kind 决定是否触发 wake:

Kind触发 wake语义
user_message用户在对话里发了一条消息(通常给 Lead)
text发送方主动选择打扰收件人——觉得这件事值得对方立刻看
framework_alert客观异常——SLA 超时、task 失败、shutdown 协议
task_notification例行进度,只入队,等下次 wake 一起读
member_status_changed状态变化,中间态,只入队

核心语义:

  1. 消息驱动一轮。Wake-triggering kind 写入未读区 → 收件人 idle 立即 wake;working 时这一轮结束后的 re-check 阶段一并消费。
  2. 消费即注入。一轮启动时,framework 把所有未读 mailbox 消息按一个 XML 包装(<team-mailbox>...</team-mailbox>)塞进这一轮的第一条 user-role message,然后才是真实输入(如有)。
  3. 消费后保留consumedAt 标记,不删行。Transcript 完整性靠这一条—— member 后续各轮可以回看自己处理过哪些消息。
  4. 唯一交付通道。Lead 的工作分配、member 之间的协调、task 完成通知——全部走 mailbox。不存在“member 直接调 Lead 的方法”。

Task

Task {
  id              identity
  teamId          identity?     ← 可缺省;不需要 team 也可以存在 task list
  title           string
  description     string
  status          "pending" | "blocked" | "claimable" | "in_progress" | "completed" | "failed"
  owner           string?       ← member name(不是 id),便于 LLM 引用
  dependencies    string[]
  result          payload?
  failureReason   string?
  createdAt, updatedAt
}

Task 与 team / member 正交:

  • Task 不需要 team 才能存在(teamId 可缺省——独立对话也可以建 task list)
  • Owner 是字符串(member name),不是引用到 member 表——任何 string 都可以(包括 "user" 表示需要用户介入)
  • 依赖图自动推进:task_complete 触发 framework 检查下游 task,把“所有依赖都 completed”的 blocked / pending 升为 claimable
  • Task 状态变化会自动发 mailbox notification 给 owner(若 owner 是 member)

Task 服务的是“工作有结构、需要跟踪进度”的场景,跟“agent 之间协作”是两件事——它们恰好经常一起出现而已。

四个原语的关系

对话会话

   │ 1:1

 Team

   ├─ members[] ───── 每个 member 有自己的 mailbox 和 workspace
   │      │
   │      └─ Lead 是 members 之一(role=lead),其 mailbox 还接收用户对话消息

   └─ tasks[] ─────── owner 字段指向某个 member 的 name(或留空)

四个原语各自独立。它们之间只通过 identity 和 name 引用,没有结构耦合。

Member 生命周期与消息驱动

状态机

当前触发下一个
(none)team_member_addspawning
spawning首轮启动working
working一轮自然结束 + 仍有 mailbox 未读working(立刻起下一轮)
working一轮自然结束 + mailbox 已清空idle
idlemailbox 收到 wake-triggering kind 消息working(wake)
workingabort / shutdown 协议完成shutdown
idleshutdown 协议完成shutdown

没有 done 状态。Member 只在显式 shutdown 时进入终态。这是 stateless wake 模型的直接结果——既然消息可以唤醒 idle member,member 就没有“工作做完了所以死了”的语义,只有“暂时没事干”。

一条消息如何变成下一轮

这是整个架构最承重的一条通路。

sequenceDiagram actor Sender participant FW as Framework participant Box as Mailbox participant Stream as Member Turn participant LLM Note over Sender,LLM: ① 写入阶段 Sender->>FW: send_message(to=alice, kind, content) FW->>Box: 持久化未读 FW-->>Sender: { delivered: true } Note over FW,Stream: ② Wake 决策 FW->>FW: 检查 kind 是否 wake-triggering alt kind 不触发 wake(task_notification 等) Note right of FW: 入队,等下次 wake 时一起读 else kind 触发 wake FW->>FW: 检查 alice.status alt status === "idle" FW->>FW: launchNextTurn(alice) else status === "working" Note right of FW: 不做事 —— 当前一轮结束<br/>时 re-check 会扫到 end end Note over Stream,LLM: ③ Turn 启动 FW->>Stream: 加载 transcript Stream->>Box: consumeUnread(alice) → 标记 consumedAt Box-->>Stream: messages with new content Stream->>Stream: 拼装 prompt<br/>(transcript + <team-mailbox/> 包装 + 真实输入) Stream->>LLM: completion LLM-->>Stream: response Note over Stream,Box: ④ Turn 结束 + re-check Stream->>Box: SELECT unread WHERE toMemberId=alice alt 仍有未读(wake-triggering 或非) Stream->>FW: continueTurn(alice) Note right of Stream: 立刻进下一轮 else 已清空 Stream->>FW: 状态置为 "idle" end

几个语义细节:

  1. Wake 决策只检查 status + kind,不读 mailbox 内容。“有没有要做的事”由一轮启动后的 consumeUnread 决定。这把 wake 路径做得极薄。
  2. Re-check 是必要的。消息 X 在一轮进行中到达(status=working → wake 决策跳过)如果不在这一轮结束前重新查 mailbox 就会被漏。所以一轮自然结束前必须再查一次未读
  3. <team-mailbox> 包装 教 LLM 区分“agent / framework 报告”和“用户的话”。Lead 的 system prompt 教它对前者 summarize-for-user 而不是 thank-and-reply。

Wake 规则:Member 在 send_message vs task_complete 之间选

这套设计把“是否打扰别人”的决策推给发送方:

  • 完成例行任务 → 只调 task_complete(写 task_notification,不触发 wake)
  • 完成的任务里包含需要立刻看到的信号 → task_complete + send_message("lead", "...")(后者写 text,触发 wake)

发送方的 system prompt 教它这条 discipline:写 text 等于打扰收件人一次,要珍惜。例行进度积累着,Lead 下一次因为别的原因 wake 时一并看到。

用户消息的特例:打断

如果用户在 Lead 正 working 时发了消息:

  • user_message 写入 Lead 的 mailbox
  • Framework 额外中止 Lead 当前这一轮
  • 这一轮的结束流程一次性消费包括新用户消息在内的所有未读,立刻起下一轮

这是“用户打断”语义,跟“普通消息排队”区分开——用户改变意图时不应该等当前这一轮跑完。

并发约束

Per-member 同时只允许一轮在跑。第二个 wake 信号到达时如果这一轮还在跑,什么都不做—— 这一轮结束的 re-check 会扫到。

工具表面

按“做一件事 = 一个工具”的颗粒度拆。

Team / Member 生命周期

工具调用者作用
team_createLead创建空 team(只有 metadata + Lead 自己),返回 teamId
team_member_addLead添加一个 member,带 initialPrompt。可在任意时刻多次调用(不批量)
team_member_shutdownLead关停单个 member(走 cooperative shutdown 协议)
team_dissolveLead紧急刹车:abort 所有 member,team 标记 dissolved。Happy path 不需要
team_statusLead + Membersnapshot 快照(team + members + tasks 概况)

通信与任务

工具调用者作用
send_messageLead + Member写收件人 mailbox。to = member name / "lead" / "*"(广播);kind 默认 text(触发 wake)
task_createLead + Member在当前 team 的 task list 建任务(title, description, dependencies?, owner?)
task_updateLead + Member改 owner / status / 字段;这条 = assignment(task_update({ taskId, owner: yourName }) 表达 claim 语义)
task_completeowner标完成 + 提交 artifact;framework 自动写 task_notification 进 Lead 的 mailbox(不触发 wake)

关键设计选择:

  • 通信工具叫 send_message 而不是 team_message——强调它不是 team-only,任何对话都可以用
  • 没有 claim 工具——用 task_update({ owner: yourName, status: "in_progress" }) 表达,语义更显式
  • task_completetask_update 分开——完成是有副作用的(自动写 mailbox notification + 触发依赖图推进),独立工具让这件事在 prompt 描述上更明确

模型能承载哪些工作形状

模型本身不绑定任何具体业务领域。下面列模型在抽象层面能承载的几种工作形状,以及对应的协调机制:

工作形状关键属性模型机制
单 member 短任务一个 member,一轮出结果initialPrompt 启动 → 一轮 → idle / shutdown
多 member 独立并行N 个 member,无协调,各自交付spawn N 个,各自跑各自那一轮,Lead 通过 task_notification 累积进度
多 member 跨时长协作Member 跨多轮存在,Lead 中途调方向idle ⇄ working 循环 + send_message(text) follow-up
依赖图工作流任务有 X → Y 的依赖关系,系统自动推进task dependencies + refreshTaskStatuses + 自动 mailbox notification
阈值 / SLA 触发外部条件(时间、状态)写入 alertframework_alert kind → 触发 Lead wake

本节只列这些形状本身;多 Agent 用具体案例把它们走一遍。

一个示意:Lead → member 长期协作的最小 trace

抽象到无业务领域的写法:

[op 提交一个长时长任务]
Lead 第 1 轮:
  team_create + team_member_add({ name: "Worker", initialPrompt: "..." })

Worker 首轮:
  执行一轮工作 → 产出 → task_complete → send_message(lead, "本轮结论...")
    → kind=text → Lead.mailbox 写入 + 触发 wake
  一轮结束 → idle

Lead 第 2 轮(因 Worker 的 text wake):
  读 mailbox: 1 条 text + 1 条 task_notification
  写对话消息给 op
  决策是否 send_message(Worker, "下一轮重点...")

如果 op 当时不在线:
  对话消息落到 chat 历史,operator 下次回来看到
  push 通道(如已接通)同时推一条提醒

[op 中途调方向]
op → chat 输入新消息
  → user_message 进 Lead.mailbox + 触发 wake(若 working 触发 abort)
  → Lead 这一轮:读到 + send_message(Worker, "调方向: ...")
  → Worker 收到 text → wake → 应用新方向 → 下一轮 → idle

这一段 trace 对任何长时长 Lead-Worker 协作模式都成立——把 Worker 换成“Sourcer”、“OnboardingCoord”、“ResearchBot”,形状不变。具体业务模式见 多 Agent

与 task subagent 的边界

多 Agent的边界规则不变。简言之:

工具
操作员留在屏幕前等吗?会 + worker 独立task
操作员留在屏幕前等吗?会 + 阶段需要协调team(可选)
操作员留在屏幕前等吗?不会team

Team 在它本职场景(操作员会离开 + 工作长 / 需要跨阶段协调)成立。Task 在同步、独立、操作员在场的场景里更便宜更直接。

模型的 non-goals

诚实标出本模型不试图解决的问题:

  1. Push 通道(浏览器通知 / 邮件 / Slack)。模型规定 Lead 何时 wake、何时写对话消息;操作员不在线时如何收到这条消息是 push 基础设施的事,跟模型解耦。任何完整产品需要 push,但 push 不是本模型的一部分。
  2. SLA / 时间触发器。模型规定 mailbox 收到 framework_alert 时 Lead wake,但谁写 alert 是外部 cron / scheduler 的事。模型给出钩点,外部填具体规则。
  3. Member archetype 库team_member_addinitialPrompt 是 free-form text。把常用角色做成声明式 archetype(命名 prompt 模板)可以提高稳定性,但不影响模型语义——属于 prompt 工程优化,不属于协调机制。
  4. Dynamic worker pool。“批量起 worker,然后让它们从共享 task queue 自己拉活”是 task 系统能力的延伸——通过 task_update owner 模拟即可,模型不为此引入新原语。

这些都是落地实现时的工作,不是模型本身的缺口。

接下来读什么

  • 多 Agent——taskteam 的边界判断框架(本文的前置)
  • Agent Team 实现——把这套理想模型落到当前系统的实现细节
这页有帮助吗?