第9课:Agent Teams——多Agent协作:团队与邮箱系统
一个Agent干不完的事,交给一支团队。用TeammateManager+JSONL邮箱,让多个持久Agent各司其职、异步通信。
系列导读
这是**《12课拆解Claude Code架构》系列的第 9 课,也是阶段四(团队协作)的开篇课**。
前八课我们造了一个单兵作战的完整 Agent 系统:第 1 课建了 Agent Loop,第 2 课加了工具注册,第 3 课引入规划,第 4 课用 Subagent 隔离上下文,第 5 课加载技能,第 6 课压缩上下文,第 7 课实现任务持久化,第 8 课把慢操作丢后台。
但单兵再强,也有上限——一个 Agent 不可能同时写代码、跑测试、审查安全。
第 9 课的格言:
"任务太大一个人干不完,要能分给队友"
这一课,我们给 Agent 装上 TeammateManager 和 MessageBus,让它从独行侠变成团队领导。
单兵作战的天花板
Subagent(第 4 课)解决了上下文隔离,但有三个硬伤:
一次性。 生成、干活、返回、消亡。下次需要,从零开始。
无身份。 所有 Subagent 都是匿名的。你没法说 "上次你审查代码发现的那个问题"——因为上次那个 Subagent 已经不存在了。
无通信。 Subagent 只能跟父 Agent 单向汇报。两个 Subagent 之间没法直接沟通。
Background Tasks(第 8 课)更受限——只能跑 shell 命令,做不了 LLM 引导的决策。
真正的团队需要三样东西:持久 Agent + 身份管理 + 通信通道。
| 需求 | Subagent(s04) | Background(s08) | Teams(s09) |
|---|---|---|---|
| LLM 推理 | 有 | 无 | 有 |
| 持久身份 | 无 | 无 | 有 |
| Agent 间通信 | 无 | 无 | 有 |
| 生命周期 | 用完即弃 | 进程级 | spawn/idle/working/shutdown |

核心架构:领导 + 队友 + 邮箱
.team/
├── config.json ← 团队名册
└── inbox/
├── coder.jsonl ← coder 的收件箱
├── tester.jsonl ← tester 的收件箱
└── leader.jsonl ← leader 的收件箱
+----------+ spawn() +---------+
| Leader | ───────────→ | Coder | ← 独立线程,独立 Agent Loop
| Agent | spawn() +---------+
| | ───────────→ | Tester | ← 独立线程,独立 Agent Loop
+----+-----+ +---------+
| ↑
+── MessageBus ───────────+
send() / broadcast()
(append JSONL, drain-on-read)三个核心模块:
- TeammateManager:维护团队名册,spawn/shutdown 队友
- MessageBus:append-only 的 JSONL 收件箱,drain-on-read 读取
- 队友循环:每个队友在独立线程中运行自己的 Agent Loop,每轮 LLM 调用前检查收件箱
核心拆解:三个模块
模块一:MessageBus——JSONL 收件箱
class MessageBus:
def __init__(self, inbox_dir=".team/inbox"):
self.inbox_dir = inbox_dir
os.makedirs(inbox_dir, exist_ok=True)
def send(self, from_name: str, to_name: str, content: str):
"""往目标队友的收件箱追加一条消息"""
msg = {"from": from_name, "to": to_name,
"content": content, "timestamp": time.time()}
path = os.path.join(self.inbox_dir, f"{to_name}.jsonl")
with open(path, "a") as f:
fcntl.flock(f, fcntl.LOCK_EX)
f.write(json.dumps(msg, ensure_ascii=False) + "\n")
fcntl.flock(f, fcntl.LOCK_UN)
def broadcast(self, from_name: str, content: str, team: dict):
for name in team:
if name != from_name:
self.send(from_name, name, content)
def read_inbox(self, name: str) -> list[dict]:
"""读取并清空收件箱(drain-on-read)"""
path = os.path.join(self.inbox_dir, f"{name}.jsonl")
if not os.path.exists(path):
return []
with open(path, "r+") as f:
fcntl.flock(f, fcntl.LOCK_EX)
lines = f.readlines()
f.seek(0)
f.truncate() # 读完即清
fcntl.flock(f, fcntl.LOCK_UN)
return [json.loads(l) for l in lines if l.strip()]三个操作:send 追加一行 JSONL,broadcast 群发,read_inbox 读取全部并清空。
为什么是 drain-on-read?消息只需处理一次。读完就清掉,不需要已读标记,不需要游标。简单到不可能出 bug。
模块二:TeammateManager——spawn 与生命周期
@dataclass
class TeammateConfig:
name: str
system_prompt: str
status: str = "idle" # idle | working | shutdown
class TeammateManager:
def __init__(self):
self.team = load_team_config() # 从 .team/config.json 加载
self.bus = MessageBus()
self.threads: dict[str, threading.Thread] = {}
def spawn(self, name: str, system_prompt: str) -> str:
config = TeammateConfig(name=name, system_prompt=system_prompt)
self.team[name] = config
save_team_config(self.team)
thread = threading.Thread(
target=self._teammate_loop, args=(name,), daemon=True)
self.threads[name] = thread
thread.start()
return f"队友 {name} 已创建并启动"
def send_task(self, to_name: str, task: str) -> str:
self.bus.send("leader", to_name, task)
self.team[to_name].status = "working"
save_team_config(self.team)
return f"任务已发送给 {to_name}"
def shutdown(self, name: str) -> str:
self.team[name].status = "shutdown"
save_team_config(self.team)
return f"队友 {name} 已关闭"config.json 长这样:
{
"coder": {
"name": "coder",
"system_prompt": "你是代码编写专家,专注于写出干净、可测试的代码。",
"status": "working"
},
"tester": {
"name": "tester",
"system_prompt": "你是测试工程师,专注于编写全面的测试用例。",
"status": "idle"
}
}生命周期是一个简洁的状态机:spawn() → IDLE → 收到消息 → WORKING → 完成 → IDLE → ... → SHUTDOWN
为什么用文件而不用内存字典? 队友可能在不同进程。文件是最简单的跨进程共享状态的方式,Agent 重启后团队名册还在——持久化是免费的。
模块三:队友循环——收件箱驱动的 Agent Loop
def _teammate_loop(self, name: str):
config = self.team[name]
while config.status != "shutdown":
messages = self.bus.read_inbox(name)
if not messages:
time.sleep(1)
config = self.team.get(name, config)
continue
config.status = "working"
save_team_config(self.team)
for msg in messages:
result = self._run_teammate_agent(
system=config.system_prompt, task=msg["content"])
self.bus.send(name, msg["from"], f"[{name} 完成] {result}")
config.status = "idle"
save_team_config(self.team)
def _run_teammate_agent(self, system: str, task: str) -> str:
"""执行一轮独立的 Agent Loop——跟第1课完全一样"""
msgs = [{"role": "user", "content": task}]
for _ in range(20):
resp = client.messages.create(
model=MODEL, system=system,
messages=msgs, tools=TOOLS, max_tokens=8000)
msgs.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
return next((b.text for b in resp.content
if hasattr(b, "text")), "(无文本回复)")
results = []
for b in resp.content:
if b.type == "tool_use":
results.append({"type": "tool_result",
"tool_use_id": b.id,
"content": dispatch_tool(b.name, b.input)})
msgs.append({"role": "user", "content": results})
return "(达到最大轮次)"核心逻辑:轮询收件箱 → 有消息就跑 Agent Loop → 结果发回 → 回到 idle。
队友不是函数调用,是独立的 Agent 实例——有自己的 messages、自己的工具、自己的上下文。跟第 1 课的 while 循环完全一样。
集成到领导 Agent
领导通过 spawn_teammate、send_to_teammate、team_status 三个新工具管理团队。关键改动只有一处——每轮 LLM 调用前检查自己的收件箱,接收队友回报:
def leader_loop(messages: list):
manager = TeammateManager()
while True:
inbox = manager.bus.read_inbox("leader")
if inbox:
reports = "\n".join(
f"[来自 {m['from']}] {m['content']}" for m in inbox)
messages.append({"role": "user",
"content": f"<teammate-reports>\n{reports}\n</teammate-reports>"})
response = client.messages.create(
model=MODEL, system=LEADER_SYSTEM,
messages=messages, tools=ALL_TOOLS, max_tokens=8000)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
# ... 标准工具执行逻辑(同前)实际运行:团队协作的样子
s09 >> 实现一个计算器模块,要有测试领导: 这个任务需要两个角色
↓
spawn_teammate("coder", "你是代码编写专家...") → coder 启动
spawn_teammate("tester", "你是测试工程师...") → tester 启动
↓
send_to_teammate("coder", "实现 calc.py,支持加减乘除")
领导: "已分配任务给 coder,等待完成..."
↓
[coder 在后台独立工作: 创建 calc.py → 验证 → 结果发回 leader]
↓
leader 收件箱收到报告
领导: "coder 完成了,把代码发给 tester"
↓
send_to_teammate("tester", "为 calc.py 编写单元测试并运行")
↓
[tester 在后台独立工作: 读取 → 写测试 → pytest → 结果发回 leader]
↓
领导汇总: "计算器模块已完成,4个测试全部通过。"每个队友都有自己的 Agent Loop。 领导只管分配和汇总,不管细节。

三个设计洞见
洞见一:为什么用 JSONL 而不用数据库
- 零依赖。
open("a")就是写入,readlines()就是读取。 - append-only 天然并发安全。 配合
fcntl.flock,多进程不会互相覆盖。 - 可调试。
cat inbox/coder.jsonl就能看到所有消息。
数据库能做同样的事,但 JSONL 是最小复杂度的正确选择。
洞见二:drain-on-read 的精妙之处
每次 read_inbox() 读取全部消息并清空。不需要消息 ID、不需要已读标记、不会重复处理、收件箱不会无限增长。代价是消息不可回溯——但对 Agent 间通信来说,这正是我们要的:消息是任务指令,处理完就不需要了。
洞见三:队友为什么不是函数调用
coder 拿到 "实现计算器" 后,需要自己决定:创建什么文件、怎么组织代码、要不要验证、出错了怎么修。这些决策需要 LLM 推理、工具调用、多轮循环。队友不是被调用的函数,是被委托任务的独立 Agent。
五分钟跑起来
# 进入项目目录
cd learn-claude-code
# 启动第九课
python agents/s09_agent_teams.py任务一:组建团队 + Agent 间通信
创建两个队友,让他们互相发消息:
s09 >> Spawn alice (coder) and bob (tester). Have alice send bob a message.实际执行记录:
> spawn_teammate: Spawned 'alice' (role: coder)
> spawn_teammate: Spawned 'bob' (role: tester)
[alice] send_message: Sent message to bob ← alice 主动给 bob 发消息
[bob] read_inbox: [{
"type": "message",
"from": "alice",
"content": "Hey Bob! 👋 I'm Alice, one of the coders on the team..."
}]关键观察: 领导 spawn 了 alice 和 bob,然后指示 alice 发消息。bob 的收件箱(JSONL 文件)收到了 alice 的消息。整个通信走的是文件系统——没有网络、没有消息队列。
同时 bob 自主开始探索项目,读代码、跑测试:
[bob] bash: 列出项目文件...
[bob] read_file: hello.py, utils.py, test_utils.py ...
[bob] bash: pytest → ===== test session starts =====
[bob] send_message: Sent message to alice ← bob 把测试报告发给 alicebob 没有被指示去跑测试——他根据自己的 tester 角色自主决定探索代码并运行测试。
任务二:广播消息给全队
向所有队友发送状态更新:
s09 >> Broadcast "status update: phase 1 complete" to all teammates实际执行记录:
> broadcast: Broadcast to 2 teammates
→ 📩 alice (coder)
→ 📩 bob (tester)broadcast 遍历团队列表,往每个队友的 JSONL 收件箱追加一条消息。领导不需要知道谁在线——写文件就是发消息,读文件就是收消息。
任务三:重启后团队还在
退出后重新启动,验证团队持久化:
s09 >> Check the lead inbox for any messages
> read_inbox: [] ← 收件箱为空,但团队还在
s09 >> /team ← 内置命令查看团队状态
Team: default
alice (coder): idle
bob (tester): idle
s09 >> /inbox ← 内置命令查看收件箱
[]关键验证: 重启后 alice 和 bob 仍然存在(config.json 持久化),状态为 idle。收件箱为空是因为上次的消息已经被 drain 读走了。团队配置跨会话存活,这是 spawn/shutdown 生命周期管理的价值。
总结:你刚造了什么
| 组件 | 之前(s08) | 之后(s09) |
|---|---|---|
| Agent 数量 | 单一 Agent | 领导 + N 个队友 |
| 持久化 | 无团队状态 | config.json + JSONL 收件箱 |
| 生命周期 | 一次性(Subagent) | spawn → idle → working → shutdown |
| 通信机制 | 无 Agent 间通信 | MessageBus(send + broadcast + drain) |
| 新增代码 | — | ~150 行(TeammateManager + MessageBus) |
核心原则:最小化通信复杂度。 JSONL 收件箱 + drain-on-read,没有消息队列中间件、没有 RPC 框架、没有数据库——用文件系统实现了一个完整的 Agent 间通信协议。

下一课预告
第 9 课的团队依赖领导手动分配。但如果队友能自己发现需要做什么——coder 写完代码自动通知 tester、tester 跑完测试自动通知 reviewer?
第 10 课:自治协作 —— 从领导派发到自组织团队。
# 预告:s10 的自治协作
rules = {
"coder": {"on_complete": ["tester"]},
"tester": {"on_complete": ["reviewer"]},
"reviewer": {"on_complete": ["leader"]},
}
# coder 完成 → 自动触发 tester → 自动触发 reviewer → 汇报领导队友不再等指令,完成任务后自动把结果传递给下游。团队变成一条自驱动的流水线。
这是《12课拆解Claude Code架构:从零掌握Agent Harness工程》系列的第 9 课。关注Claw开发者,不错过后续更新。
完整代码和交互式学习平台:github.com/shareAI-lab/learn-claude-code
如果这篇文章对你有帮助,欢迎转发给你的技术团队。
系列目录
- 第1课:用20行Python造出你的第一个AI Agent
- 第2课:给Agent加工具 —— dispatch map模式详解
- 第3课:TodoWrite —— 让Agent先想后做:规划系统
- 第4课:Subagent —— 拆解大任务,上下文隔离
- 第5课:按需加载领域知识——Skill机制
- 第6课:无限对话——上下文压缩三层策略
- 第7课:任务持久化——文件级DAG任务图
- 第8课:后台执行——异步任务与通知队列
- 第9课:Agent Teams——多Agent协作:团队与邮箱系统(本文)
- 第10课:团队协议——状态机驱动的协商
- 第11课:自治Agent——自组织任务认领
- 第12课:终极隔离——Worktree并行执行