第 13 章:Agent Teams —— 从独行到协作
Subagent 是一次性函数调用——spawn、执行、返回摘要、销毁;Agent Team 的核心跃迁在于将 Agent 从无状态的函数调用升级为持久化的协作实体:每个 teammate 有身份、有状态、有自己的邮箱,能跨越多轮对话持续存在并相互通信。
13.1 从 Subagent 到 Team 的进化
Subagent 的局限
上一章的 Subagent 模型可以类比为同步函数调用:
result = subagent(prompt) # 阻塞等待,返回后子 Agent 销毁这意味着三个根本限制:
- 无状态:每次调用都从空白上下文开始,无法记住上次做了什么
- 无身份:不知道自己是谁,也不知道"团队里还有谁"
- 无通信:子 Agent 之间无法直接交流,所有信息必须经过父 Agent 中转
当任务足够大——比如"一个 Agent 写前端、一个写后端、一个跑测试"——Subagent 模型就力不从心了。三个 subagent 各自独立执行,没有办法在发现接口不兼容时实时协商修改。
Teammate 模型
Team 模式引入了根本性的改变:
Subagent: spawn → execute → return summary → destroyed
Teammate: spawn → WORKING → IDLE → WORKING → ... → SHUTDOWN关键区别:
| 维度 | Subagent | Teammate |
|---|---|---|
| 生命周期 | 一次性 | 持久化,可在 idle/working 之间切换 |
| 身份 | 匿名 | 有名字、角色、状态 |
| 通信 | 只能通过父 Agent 中转 | 任意 teammate 之间直接通信 |
| 上下文 | 执行完即丢弃 | 在线程中持续累积 |
| 运行方式 | 同步阻塞 | 独立线程,异步运行 |
| 状态存储 | 无 | config.json + JSONL 邮箱 |
Teammate 的本质是一个持续运行的 Agent 循环,绑定在一个独立线程上,拥有自己的 messages[]、system prompt 和工具集。它不是"完成一个任务就退出",而是"完成任务后进入 idle,等待新消息唤醒"。
13.2 Team 架构
整个 Team 系统由三个核心组件构成:TeammateManager 负责生命周期管理,MessageBus 负责消息传递,JSONL 文件充当持久化邮箱。
文件系统布局
.team/
├── config.json ← 团队花名册 + 成员状态
└── inbox/
├── alice.jsonl ← alice 的收件箱(append-only)
├── bob.jsonl ← bob 的收件箱
└── lead.jsonl ← lead 的收件箱整个团队的协作状态完全由文件系统承载——无数据库、无消息队列、无网络调用。
TeammateManager
TeammateManager 是团队的注册中心,核心职责有三:
- 维护 config.json:记录所有成员的 name、role、status
- Spawn 成员:创建线程并启动 Agent 循环
- 查询团队状态:列出所有成员及其当前状态
class TeammateManager:
def __init__(self, team_dir: Path):
self.dir = team_dir
self.config_path = self.dir / "config.json"
self.config = self._load_config()
self.threads = {}
def _load_config(self) -> dict:
if self.config_path.exists():
return json.loads(self.config_path.read_text())
return {"team_name": "default", "members": []}config.json 的结构:
{
"team_name": "default",
"members": [
{"name": "alice", "role": "coder", "status": "working"},
{"name": "bob", "role": "tester", "status": "idle"}
]
}spawn() 方法的逻辑:先检查成员是否已存在(如果存在且状态为 idle 或 shutdown,则复用;否则报错),然后创建 daemon 线程启动 Agent 循环:
def spawn(self, name: str, role: str, prompt: str) -> str:
member = self._find_member(name)
if member:
if member["status"] not in ("idle", "shutdown"):
return f"Error: '{name}' is currently {member['status']}"
member["status"] = "working"
member["role"] = role
else:
member = {"name": name, "role": role, "status": "working"}
self.config["members"].append(member)
self._save_config()
thread = threading.Thread(
target=self._teammate_loop,
args=(name, role, prompt),
daemon=True,
)
self.threads[name] = thread
thread.start()
return f"Spawned '{name}' (role: {role})"注意 daemon=True:当主进程退出时,所有 teammate 线程自动终止,无需手动清理。但这也意味着 teammate 如果在写文件中途被杀,文件会处于不完整状态——这正是后面 shutdown 协议要解决的问题。
MessageBus
MessageBus 是通信层,提供三个操作:send(向指定 teammate 发送消息)、read_inbox(读取并清空自己的收件箱)、broadcast(向所有 teammate 广播)。
class MessageBus:
def __init__(self, inbox_dir: Path):
self.dir = inbox_dir
self.dir.mkdir(parents=True, exist_ok=True)
def send(self, sender, to, content, msg_type="message", extra=None):
msg = {
"type": msg_type,
"from": sender,
"content": content,
"timestamp": time.time(),
}
if extra:
msg.update(extra)
with open(self.dir / f"{to}.jsonl", "a") as f:
f.write(json.dumps(msg) + "\n")
def read_inbox(self, name):
inbox_path = self.dir / f"{name}.jsonl"
if not inbox_path.exists():
return []
messages = []
for line in inbox_path.read_text().strip().splitlines():
if line:
messages.append(json.loads(line))
inbox_path.write_text("") # drain
return messages
def broadcast(self, sender, content, teammates):
count = 0
for name in teammates:
if name != sender:
self.send(sender, name, content, "broadcast")
count += 1
return f"Broadcast to {count} teammates"消息格式是一个扁平 JSON 对象:
{
"type": "message",
"from": "alice",
"content": "API 模块重构完毕,新接口在 /api/v2",
"timestamp": 1711234567.89
}5 种消息类型覆盖了所有通信需求:
| 类型 | 方向 | 用途 |
|---|---|---|
message | 任意 → 任意 | 普通文本通信 |
broadcast | 一 → 全体 | 全局通知 |
shutdown_request | lead → teammate | 请求关机 |
shutdown_response | teammate → lead | 批准/拒绝关机 |
plan_approval_response | lead ↔ teammate | 计划审批 |
Teammate 的 Agent 循环
每个 teammate 在独立线程中运行一个完整的 Agent 循环。关键设计:每轮 LLM 调用前,先检查收件箱,将新消息注入 messages[] 作为新的 user 输入。
def _teammate_loop(self, name, role, prompt):
sys_prompt = f"You are '{name}', role: {role}. Use send_message to communicate."
messages = [{"role": "user", "content": prompt}]
tools = self._teammate_tools()
for _ in range(50): # 安全上限防止无限循环
inbox = BUS.read_inbox(name) # 1. 检查邮箱
for msg in inbox:
messages.append({"role": "user", "content": json.dumps(msg)})
response = client.messages.create( # 2. LLM 调用
model=MODEL, system=sys_prompt,
messages=messages, tools=tools, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use": # 3. 非工具调用则退出
break
results = []
for block in response.content: # 4. 执行工具
if block.type == "tool_use":
output = self._exec(name, block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
})
messages.append({"role": "user", "content": results})
# 循环结束,状态回到 idle
member = self._find_member(name)
if member and member["status"] != "shutdown":
member["status"] = "idle"
self._save_config()数据流全景:
graph TB
subgraph "Lead Agent(主线程)"
L_LOOP[Agent 循环]
L_INBOX[lead.jsonl]
end
subgraph "Teammate: alice(线程 1)"
A_LOOP[Agent 循环]
A_INBOX[alice.jsonl]
end
subgraph "Teammate: bob(线程 2)"
B_LOOP[Agent 循环]
B_INBOX[bob.jsonl]
end
subgraph "共享文件系统"
CONFIG[config.json]
FS[项目文件]
end
L_LOOP -->|send_message| A_INBOX
L_LOOP -->|send_message| B_INBOX
A_LOOP -->|send_message| B_INBOX
A_LOOP -->|send_message| L_INBOX
B_LOOP -->|send_message| L_INBOX
A_LOOP -->|read/write| FS
B_LOOP -->|read/write| FS
L_LOOP -->|read/write| FS
L_LOOP -->|spawn/update| CONFIGLead 与 Teammate 的工具差异
Lead(主 Agent)和 Teammate 拥有不同的工具集:
| 工具 | Lead | Teammate | 说明 |
|---|---|---|---|
| bash / read_file / write_file / edit_file | 有 | 有 | 基础文件和命令操作 |
| spawn_teammate | 有 | 无 | 只有 lead 能创建新成员 |
| list_teammates | 有 | 无 | 只有 lead 能查询团队状态 |
| send_message | 有 | 有 | 双方都能发消息 |
| read_inbox | 有 | 有 | 双方都能读自己的邮箱 |
| broadcast | 有 | 无 | 只有 lead 能广播 |
这种设计体现了一个架构原则:管理权限集中于 lead,通信能力对等。Teammate 不能创建新成员,但可以自由地向任何人发消息,包括直接向其他 teammate 发消息而无需经过 lead 中转。
13.3 JSONL Mailbox 协议
为什么选 JSONL
JSONL(JSON Lines)作为邮箱格式的选择看似简单,背后有严格的工程理由:
Append-only 写入:open(path, "a") 是 POSIX 原子操作(对于合理大小的单行写入)。多个线程同时向同一个 .jsonl 文件追加消息时,不需要显式加锁——操作系统的文件追加操作在大多数场景下保证原子性。
# 写入:追加一行 JSON
with open(inbox_path, "a") as f:
f.write(json.dumps(msg) + "\n")无锁设计:对比使用 SQLite 或共享内存的方案,JSONL 文件不需要数据库连接、不需要 mutex、不需要信号量。代价是读写不能同时发生——但 drain-after-read 模式恰好避免了这个问题。
可增量读取:每行是一个完整的 JSON 对象,可以逐行解析。即使文件在追加过程中被读取,前面已完成的行仍然是有效的 JSON。
可调试:直接 cat alice.jsonl 就能看到所有消息历史。对比二进制协议或内存中的消息队列,JSONL 的可观测性是免费的。
对比其他候选方案:
| 方案 | 原子写入 | 无锁 | 可调试 | 问题 |
|---|---|---|---|---|
| JSONL | 是(append) | 是 | 是 | 读写竞争需 drain 模式 |
| SQLite | 是(事务) | 否 | 一般 | 需要连接管理、WAL 模式 |
| 共享内存 + mutex | N/A | 否 | 差 | 进程崩溃时 mutex 泄露 |
| Redis/消息队列 | 是 | 是 | 一般 | 引入外部依赖 |
| 单个 JSON 文件 | 否(全量覆写) | 否 | 是 | 并发写入直接损坏文件 |
JSONL 的核心优势:零依赖 + 并发安全 + 人类可读。对于进程内多线程通信这个场景,它是最简方案。
Drain-After-Read
收件箱的读取采用 drain 模式:读取全部内容后立即清空文件。
def read_inbox(self, name):
inbox_path = self.dir / f"{name}.jsonl"
if not inbox_path.exists():
return []
messages = []
for line in inbox_path.read_text().strip().splitlines():
if line:
messages.append(json.loads(line))
inbox_path.write_text("") # 清空 = drain
return messages为什么必须 drain?
- 防止重复处理:如果不清空,下次
read_inbox会再次读到同样的消息,teammate 的 LLM 上下文中会出现重复的 user message - 控制文件增长:没有 drain 的话,长时间运行的 teammate 邮箱文件会无限增长
- 简化状态管理:读完就清空,不需要维护"哪些消息已处理"的标记
潜在问题:read_text() 和 write_text("") 之间存在竞争窗口——如果在这个间隙有新消息追加,新消息会被一起清空。在当前的线程模型中,这个窗口极小(微秒级),对实际使用影响不大。但在分布式场景中,需要更精细的机制(如 rename-and-recreate 或 file locking)。
收件箱轮询
Teammate 的 Agent 循环在每轮 LLM 调用前检查邮箱:
for _ in range(50):
inbox = BUS.read_inbox(name) # 轮询
for msg in inbox:
messages.append({"role": "user", "content": json.dumps(msg)})
response = client.messages.create(...)轮询频率由 Agent 循环的迭代速度决定——每次工具调用后就检查一次。这不是定时轮询(无 sleep),而是事件驱动式轮询:有工具调用就检查,没有就退出循环。
Lead 的处理方式略有不同,收到的邮箱消息被包裹在 <inbox> 标签中:
inbox = BUS.read_inbox("lead")
if inbox:
messages.append({
"role": "user",
"content": f"<inbox>{json.dumps(inbox, indent=2)}</inbox>",
})
messages.append({
"role": "assistant",
"content": "Noted inbox messages.",
})<inbox> 标签让 LLM 能够区分"用户输入"和"teammate 发来的消息"。紧跟的 assistant 回复 "Noted inbox messages." 是为了满足 API 的消息交替要求(user → assistant → user → ...)。
13.4 Team 协议
消息总线提供了通信能力,但通信本身不等于协作。协作需要协议——双方就"我发什么、你回什么、状态怎么变"达成一致。
关机协议:Graceful Shutdown
问题:直接杀线程(thread.join() 或进程退出时 daemon 线程被强制终止)会导致 teammate 的工作中途被打断——文件写了一半、git commit 执行到一半、代码修改不完整。
解决方案:shutdown 请求-响应协议,通过消息总线协商关机。
stateDiagram-v2
[*] --> pending: lead 发送 shutdown_request
pending --> approved: teammate 回复 approve=true
pending --> rejected: teammate 回复 approve=false
approved --> [*]: teammate 完成清理并退出
rejected --> pending: lead 可选择重新请求Lead 侧:生成唯一 request_id,发送 shutdown_request 类型的消息,并在 tracker 中记录状态为 pending。
shutdown_requests = {} # {request_id: {"target": name, "status": "pending"}}
def handle_shutdown_request(teammate: str) -> str:
req_id = str(uuid.uuid4())[:8]
shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
BUS.send(
"lead", teammate, "Please shut down gracefully.",
"shutdown_request", {"request_id": req_id},
)
return f"Shutdown request {req_id} sent to '{teammate}' (status: pending)"Teammate 侧:通过 shutdown_response 工具回复,决定是批准还是拒绝。批准后,Agent 循环设置退出标志。
if tool_name == "shutdown_response":
req_id = args["request_id"]
approve = args["approve"]
with _tracker_lock:
if req_id in shutdown_requests:
shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"
BUS.send(
sender, "lead", args.get("reason", ""),
"shutdown_response", {"request_id": req_id, "approve": approve},
)
return f"Shutdown {'approved' if approve else 'rejected'}"Teammate 循环中处理退出标志:
should_exit = False
for _ in range(50):
# ...收件箱检查、LLM 调用...
for block in response.content:
if block.type == "tool_use":
output = self._exec(name, block.name, block.input)
if block.name == "shutdown_response" and block.input.get("approve"):
should_exit = True
if should_exit:
break
# 循环结束后更新状态
member["status"] = "shutdown" if should_exit else "idle"
self._save_config()为什么 teammate 有权拒绝关机?因为 teammate 正在执行的任务可能处于不可中断的关键阶段(如正在写入一组相互依赖的文件,中途停止会导致不一致)。拒绝关机让 teammate 有机会完成当前操作后再接受关机。
计划审批 FSM
问题:当 lead 给 teammate 分配一个高风险任务(如"重构 auth 模块"),teammate 直接开始执行。如果 teammate 的实现思路有问题,等到发现时已经改了一堆代码。
解决方案:在执行前加一个审批关卡——teammate 提交计划,lead 审阅后批准或驳回。
sequenceDiagram
participant T as Teammate
participant BUS as MessageBus
participant L as Lead
T->>T: plan_approval(plan="1. 抽取接口<br>2. 重构实现<br>3. 更新测试")
T->>BUS: send(plan, type=plan_approval_response)
Note over T: 生成 request_id=xyz<br>plan_requests[xyz] = {status: pending}
BUS->>L: 消息到达 lead 收件箱
L->>L: 审阅计划文本
alt 批准
L->>BUS: plan_approval(request_id=xyz, approve=true)
BUS->>T: {approve: true, feedback: "LGTM"}
T->>T: 开始执行计划
else 驳回
L->>BUS: plan_approval(request_id=xyz, approve=false, feedback="先加测试")
BUS->>T: {approve: false, feedback: "先加测试"}
T->>T: 修改计划后重新提交
endTeammate 侧提交计划:
if tool_name == "plan_approval":
plan_text = args.get("plan", "")
req_id = str(uuid.uuid4())[:8]
plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"}
BUS.send(
sender, "lead", plan_text, "plan_approval_response",
{"request_id": req_id, "plan": plan_text},
)
return f"Plan submitted (request_id={req_id}). Waiting for lead approval."Lead 侧审阅计划:
def handle_plan_review(request_id, approve, feedback=""):
req = plan_requests[request_id]
req["status"] = "approved" if approve else "rejected"
BUS.send(
"lead", req["from"], feedback, "plan_approval_response",
{"request_id": request_id, "approve": approve, "feedback": feedback},
)
return f"Plan {req['status']} for '{req['from']}'"统一的 Request-Response 模式
观察 shutdown 和 plan approval 两个协议,底层模式完全相同:
1. 发起方生成 request_id
2. 发起方通过 MessageBus 发送请求
3. 发起方在 tracker 中记录 {request_id: {status: "pending"}}
4. 接收方通过 MessageBus 回复,携带同一个 request_id
5. 发起方根据回复更新 tracker 状态为 approved 或 rejected两个协议的唯一区别:
| 维度 | Shutdown | Plan Approval |
|---|---|---|
| 发起方 | Lead | Teammate |
| 接收方 | Teammate | Lead |
| Tracker | shutdown_requests | plan_requests |
| 附加数据 | reason | plan text, feedback |
状态机(FSM)完全复用:
[pending] ─── approve ──→ [approved]
│
└──── reject ───→ [rejected]这种设计的好处是可扩展:如果未来需要新协议(如代码审查请求、资源申请),只需定义新的 tracker 和消息类型,FSM 逻辑不变。
边界情况处理
Teammate 崩溃后的恢复
当前实现中,如果 teammate 的 Agent 循环因 API 异常退出,其状态在 config.json 中会被标记为 idle(而非 shutdown)。这允许 lead 重新 spawn 同名 teammate 继续工作——因为 spawn() 检查 status not in ("idle", "shutdown"),只有 working 状态的成员不可重新创建。
member = self._find_member(name)
if member:
if member["status"] not in ("idle", "shutdown"):
return f"Error: '{name}' is currently {member['status']}"
member["status"] = "working" # 复用已有成员但恢复后的 teammate 从空白上下文开始——之前的 messages[] 在线程退出时已丢弃。它能感知到的唯一"记忆"是文件系统上的工作产物和邮箱中可能残留的未读消息。
消息丢失检测
当前实现没有消息确认机制。消息可能在以下场景"丢失":
- Drain 竞争:
read_text()和write_text("")之间有新消息追加,新消息被一起清空 - Teammate 退出:teammate 循环退出后,后续发给它的消息仍然会被追加到 .jsonl 文件中,但没有人读取
生产级改进方向:
- 给每条消息加 sequence number,接收方检测序号连续性
- 使用 rename-and-recreate 替代 drain:先
rename(alice.jsonl, alice.jsonl.processing),创建新的空alice.jsonl,然后处理旧文件中的消息
死锁避免
理论上,如果 alice 等待 bob 的回复,bob 又等待 alice 的回复,就形成死锁。但当前实现不存在阻塞等待——teammate 的循环是 poll 模型(每轮检查邮箱),不会因为"等消息"而阻塞线程。最坏情况是两个 teammate 都在空转(不断调用 LLM 但没有新消息),直到达到 50 轮上限自然退出。
这也暴露了 poll 模型的代价:空转浪费。如果 teammate 在等待其他 teammate 的回复,它每轮循环仍然会调用 LLM(消耗 token),尽管没有新消息需要处理。更高效的方案是引入条件变量或事件通知,让等待中的 teammate 真正 sleep 直到收件箱非空。
Tracker 的线程安全
shutdown_requests 和 plan_requests 两个 dict 被多个线程并发访问(lead 线程读写、teammate 线程读写),因此需要锁保护:
_tracker_lock = threading.Lock()
# 写入
with _tracker_lock:
shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
# 读取 + 更新
with _tracker_lock:
if req_id in shutdown_requests:
shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"注意 MessageBus 的 send() 方法不需要加锁——因为 JSONL 的 append 写入在 OS 层面是原子的(单行写入,不超过 PIPE_BUF 大小)。
13.5 Team 模式在生产中的体现
Lead 的工具集从 9 到 12
从 Subagent 到 Team 再到 Team Protocol,lead 的工具集逐步扩展:
| 阶段 | 工具数 | 新增工具 |
|---|---|---|
| 基础 Agent | 4 | bash, read_file, write_file, edit_file |
| Team(无协议) | 9 | + spawn_teammate, list_teammates, send_message, read_inbox, broadcast |
| Team + Protocol | 12 | + shutdown_request, shutdown_response(查询), plan_approval |
每次扩展都遵循同一模式:新增工具 → 新增 handler → 注册到 TOOL_HANDLERS。架构不变,能力叠加。
消息调度设计差异
不同的 Agent Team 实现在消息调度上有根本性的设计差异:
文件邮箱模型(本章实现):每个 teammate 有独立的 .jsonl 文件作为收件箱,消息通过文件追加传递。优点是零依赖、可调试;缺点是 poll 模型有延迟和空转浪费。
内存消息队列:用 queue.Queue 或 asyncio.Queue 替代文件。优点是零延迟、无序列化开销;缺点是进程退出后消息丢失、不可调试。
中心化调度器:所有消息经过一个中心 dispatcher,由它决定路由。优点是可以实现优先级调度、负载均衡;缺点是 dispatcher 成为单点瓶颈。
事件驱动模型:teammate 注册事件监听器,新消息到达时触发回调而非 poll。优点是无空转浪费;缺点是回调地狱、调试困难。
生产系统通常混合使用:进程内用内存队列(低延迟),跨进程/跨机器用持久化消息(可靠性)。本章的 JSONL 方案虽然朴素,但它清晰地展示了消息传递的核心语义——谁发的、发给谁、什么内容、什么时间——而这四个字段在任何方案中都是不变的。
ACP (Agent Communication Protocol) 的设计思路
当 Agent Team 从单机多线程扩展到多机多进程甚至多服务时,需要一个标准化的通信协议。ACP (Agent Communication Protocol) 试图定义这样一个标准。
ACP 的核心设计思路:
Agent 即服务:每个 Agent 暴露为一个 HTTP 端点,通过标准的 REST API 接收消息和返回结果。消息不再写入本地文件,而是通过网络传递。
消息格式标准化:所有消息遵循统一的 schema,包含 sender、receiver、content、metadata。这与本章的 {"type", "from", "content", "timestamp"} 结构高度相似——差异主要在传输层(HTTP vs 文件追加)而非数据层。
能力声明:每个 Agent 声明自己的 capabilities(能处理什么类型的消息、提供什么工具),类似本章中 _teammate_tools() 返回的工具列表。调用方根据声明选择合适的 Agent。
生命周期管理:Agent 的创建、状态查询、关闭通过标准 API 操作。这与 TeammateManager 的 spawn / list_all / shutdown 功能对应。
从本章的实现到 ACP 的跃迁:
| 本章实现 | ACP 等效概念 |
|---|---|
.team/config.json | Agent Registry(注册中心) |
.team/inbox/*.jsonl | Message Channel(消息通道) |
spawn_teammate() | POST /agents(创建 Agent) |
list_teammates() | GET /agents(查询 Agent 列表) |
send_message() | POST /agents/{id}/messages(发送消息) |
read_inbox() | GET /agents/{id}/messages(拉取消息) |
shutdown_request | DELETE /agents/{id}(请求关闭) |
本质没变:注册 → 通信 → 协议。文件系统是最简单的消息通道,HTTP API 是最通用的消息通道,但上层的协议模式(request-response、FSM、tracker)是独立于传输层的。
Team vs Subagent 的选择
并非所有多 Agent 场景都需要 Team 模式。选择依据:
flowchart TD
A[多 Agent 任务] --> B{Agent 之间需要通信?}
B -->|否| C{任务间有时序依赖?}
B -->|是| D[Team 模式<br>持久化 + 邮箱]
C -->|否| E[并行 Subagent<br>同一消息多个 tool_use]
C -->|是| F[串行 Subagent<br>依次 spawn]
D --> G{需要结构化协议?}
G -->|否| H[基础 Team<br>message + broadcast]
G -->|是| I[Team + Protocol<br>shutdown + plan approval]关键判断标准:Agent 之间是否需要直接通信。如果任务可以完全分解为互不相关的子任务(各自独立执行,最后合并结果),Subagent 就够了。如果 Agent A 的输出会影响 Agent B 的决策,或者 Agent A 需要在执行过程中向 Agent B 请求协助,则需要 Team 模式的消息通信能力。
第二个判断标准:Agent 是否需要长期存活。一个只跑 3 轮工具调用的子任务用 Subagent 更经济。一个需要持续监听消息、等待指令的 Agent(如 CI 监控 Agent、code review Agent)则必须是 Teammate。