HuanCode Docs

第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)

三个核心模块:

  1. TeammateManager:维护团队名册,spawn/shutdown 队友
  2. MessageBus:append-only 的 JSONL 收件箱,drain-on-read 读取
  3. 队友循环:每个队友在独立线程中运行自己的 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_teammatesend_to_teammateteam_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 而不用数据库

  1. 零依赖。 open("a") 就是写入,readlines() 就是读取。
  2. append-only 天然并发安全。 配合 fcntl.flock,多进程不会互相覆盖。
  3. 可调试。 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 把测试报告发给 alice

bob 没有被指示去跑测试——他根据自己的 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并行执行

On this page