Agent Team 协调模型
以 mailbox 为中心的多代理协调模型——设计哲学、原语、工具表面
理想模型——不是已落地的实现。 本文描述的是一套以 mailbox 为中心的 actor 设计(按 member 的 mailbox、
send_message/task_create/task_update/task_complete/team_member_add、working ⇄ 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 | 可寻址 actor | Mailbox 的所有者 |
| 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 | 终态,不再 wake | shutdown 协议完成 / 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 | 否 | 状态变化,中间态,只入队 |
核心语义:
- 消息驱动一轮。Wake-triggering kind 写入未读区 → 收件人 idle 立即 wake;working 时这一轮结束后的 re-check 阶段一并消费。
- 消费即注入。一轮启动时,framework 把所有未读 mailbox 消息按一个 XML 包装(
<team-mailbox>...</team-mailbox>)塞进这一轮的第一条 user-role message,然后才是真实输入(如有)。 - 消费后保留。
consumedAt标记,不删行。Transcript 完整性靠这一条—— member 后续各轮可以回看自己处理过哪些消息。 - 唯一交付通道。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_add | spawning |
spawning | 首轮启动 | working |
working | 一轮自然结束 + 仍有 mailbox 未读 | working(立刻起下一轮) |
working | 一轮自然结束 + mailbox 已清空 | idle |
idle | mailbox 收到 wake-triggering kind 消息 | working(wake) |
working | abort / shutdown 协议完成 | shutdown |
idle | shutdown 协议完成 | shutdown |
没有 done 状态。Member 只在显式 shutdown 时进入终态。这是 stateless wake 模型的直接结果——既然消息可以唤醒 idle
member,member 就没有“工作做完了所以死了”的语义,只有“暂时没事干”。
一条消息如何变成下一轮
这是整个架构最承重的一条通路。
几个语义细节:
- Wake 决策只检查 status + kind,不读 mailbox 内容。“有没有要做的事”由一轮启动后的 consumeUnread 决定。这把 wake 路径做得极薄。
- Re-check 是必要的。消息 X 在一轮进行中到达(
status=working→ wake 决策跳过)如果不在这一轮结束前重新查 mailbox 就会被漏。所以一轮自然结束前必须再查一次未读。 <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_create | Lead | 创建空 team(只有 metadata + Lead 自己),返回 teamId |
team_member_add | Lead | 添加一个 member,带 initialPrompt。可在任意时刻多次调用(不批量) |
team_member_shutdown | Lead | 关停单个 member(走 cooperative shutdown 协议) |
team_dissolve | Lead | 紧急刹车:abort 所有 member,team 标记 dissolved。Happy path 不需要 |
team_status | Lead + Member | snapshot 快照(team + members + tasks 概况) |
通信与任务
| 工具 | 调用者 | 作用 |
|---|---|---|
send_message | Lead + Member | 写收件人 mailbox。to = member name / "lead" / "*"(广播);kind 默认 text(触发 wake) |
task_create | Lead + Member | 在当前 team 的 task list 建任务(title, description, dependencies?, owner?) |
task_update | Lead + Member | 改 owner / status / 字段;这条 = assignment(task_update({ taskId, owner: yourName }) 表达 claim 语义) |
task_complete | owner | 标完成 + 提交 artifact;framework 自动写 task_notification 进 Lead 的 mailbox(不触发 wake) |
关键设计选择:
- 通信工具叫
send_message而不是team_message——强调它不是 team-only,任何对话都可以用 - 没有
claim工具——用task_update({ owner: yourName, status: "in_progress" })表达,语义更显式 task_complete跟task_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 触发 | 外部条件(时间、状态)写入 alert | framework_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
诚实标出本模型不试图解决的问题:
- Push 通道(浏览器通知 / 邮件 / Slack)。模型规定 Lead 何时 wake、何时写对话消息;操作员不在线时如何收到这条消息是 push 基础设施的事,跟模型解耦。任何完整产品需要 push,但 push 不是本模型的一部分。
- SLA / 时间触发器。模型规定 mailbox 收到
framework_alert时 Lead wake,但谁写 alert 是外部 cron / scheduler 的事。模型给出钩点,外部填具体规则。 - Member archetype 库。
team_member_add的initialPrompt是 free-form text。把常用角色做成声明式 archetype(命名 prompt 模板)可以提高稳定性,但不影响模型语义——属于 prompt 工程优化,不属于协调机制。 - Dynamic worker pool。“批量起 worker,然后让它们从共享 task queue 自己拉活”是 task 系统能力的延伸——通过
task_update owner模拟即可,模型不为此引入新原语。
这些都是落地实现时的工作,不是模型本身的缺口。
接下来读什么
- 多 Agent——
task和team的边界判断框架(本文的前置) - Agent Team 实现——把这套理想模型落到当前系统的实现细节