首页
/ learn-claude-code s09: Agent Teams —— 持久团队与基于文件邮箱的多智能体协作

learn-claude-code s09: Agent Teams —— 持久团队与基于文件邮箱的多智能体协作

2026-09-05 17:14:43作者:曹令琨Iris

本篇基于 learn-claude-code 课程的 s09: Agent Teams 章节(docs/ja/s09-agent-teams.md)与配套实现 agents/s09_agent_teams.py 展开。该章在「单个 Agent Loop + 基础工具」之上引入 Team 机制:Lead 可以生成拥有独立身份、独立线程和独立消息上下文的持久团队成员(teammate),成员之间通过追加式 JSONL 文件邮箱(inbox)完成异步通信。读完本篇,你将理解 teammate 的完整生命周期(spawn → WORKING → IDLE → … → SHUTDOWN)、MessageBus 的读写与排空(drain)语义、9 个 Lead 工具与 6 个 teammate 工具的分工,并能本地跑起这套多智能体原型。

一、为什么需要 Agent Teams:s04 与 s08 的缺口

课程在前几章已经建立了两类「把活交给别人干」的机制,但它们都无法支撑真正的团队协作(章节脉络见 docs/ja/s09-agent-teams.md 中的 s01 > s02 > … > [ s09 ] > s10 > s11 > s12):

  • Subagent(s04):一次性(disposable)的。它被生成、执行任务、返回一份摘要,然后消亡。它没有身份(identity),跨次调用之间也没有任何记忆;
  • Background tasks(s08):只能执行 shell 命令,无法做 LLM 引导的决策。

文档给出的结论是:真正的团队协作需要三样东西——(1) 能跨越多轮 prompt 持续存在的持久 agent;(2) 身份与生命周期管理;(3) agent 之间的通信通道。s09 正是围绕这三点展开,其 Harness 层的定位是「Team mailboxes:多个模型通过文件协调」(Multiple models, coordinated through files)。

二、总体设计:.team/ 目录与 teammate 生命周期

s09 的全部状态都落在磁盘上的 .team/ 目录中,文档给出的核心结构图如下(原样继承自 docs/ja/s09-agent-teams.md):

Teammate lifecycle:
  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN

Communication:
  .team/
    config.json           <- team roster + statuses
    inbox/
      alice.jsonl         <- append-only, drain-on-read
      bob.jsonl
      lead.jsonl

              +--------+    send("alice","bob","...")    +--------+
              | alice  | -----------------------------> |  bob   |
              | loop   |    bob.jsonl << {json_line}    |  loop  |
              +--------+                                +--------+
                   ^                                         |
                   |        BUS.read_inbox("alice")          |
                   +---- alice.jsonl -> read + drain ---------+

对应到源码(agents/s09_agent_teams.py):

  • TEAM_DIR = WORKDIR / ".team"INBOX_DIR = TEAM_DIR / "inbox",其中 WORKDIR = Path.cwd(),即团队目录建在你启动脚本的当前目录下;
  • config.json 保存团队名册(roster):team_namemembers 数组,每个成员含 namerolestatus 三个字段(见 TeammateManager._load_config 的默认值 {"team_name": "default", "members": []}agents/s09_agent_teams.py#L133-L136);
  • inbox/ 下每个 agent(含 lead)各有一个 .jsonl 文件,一行一条 JSON 消息,追加写、读时排空(append-only, drain-on-read)。

文件邮箱的选择意味着:不依赖任何消息队列中间件,协作状态天然可审计(cat .team/inbox/alice.jsonl 即可看到通信历史,在排空前);代价是 drain-on-read 使其是「收件箱」而非「队列」——一条消息只会被消费一次。

三、MessageBus:send / read_inbox / broadcast 三件套

文档「How It Works」第 3 条给出的核心抽象是 MessageBus

class MessageBus:
    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):
        path = self.dir / f"{name}.jsonl"
        if not path.exists(): return "[]"
        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]
        path.write_text("")  # drain
        return json.dumps(msgs, indent=2)

源码中的完整实现(agents/s09_agent_teams.py#L78-L118)在此之上补充了三处关键细节:

  1. 消息类型白名单校验send() 的第一步是检查 msg_type 是否在 VALID_MSG_TYPES 中,非法类型直接返回错误字符串而不是写盘(agents/s09_agent_teams.py#L83-L86)。s09 声明了 5 种消息类型(agents/s09_agent_teams.py#L68-L74):

    消息类型 含义 s09 中是否处理
    message 普通文本消息
    broadcast 群发给所有 teammate
    shutdown_request 请求优雅关闭 声明,s10 实现
    shutdown_response 同意/拒绝关闭 声明,s10 实现
    plan_approval_response 同意/拒绝计划审批 声明,s10 实现

    文件头注释明确说明「5 message types (all declared, not all handled here)」——s09 先把协议位占好,shutdown 握手与计划审批留给下一章 s10 落地,这正是课程「逐层加码」的写法。

  2. read_inbox() 默认 drain。实现中返回的是 list(而非文档示意中的 JSON 字符串),并带 clear: bool = True 参数:clear=True 时读完即 write_text("") 清空文件;clear=False 时只读不清。这个开关被 CLI 的 /inbox 命令复用(见第六节)。

  3. broadcast() 的发送者排除broadcast(sender, content, teammates) 遍历名册成员,跳过发送者本人后逐个以 msg_type="broadcast" 写入对方邮箱,并返回 Broadcast to N teammatesagents/s09_agent_teams.py#L112-L118)。

四、TeammateManager:名册管理与线程化 spawn

4.1 名册的加载与持久化

文档第 1 条机制:TeammateManagerconfig.json 维护团队名册:

class TeammateManager:
    def __init__(self, team_dir: Path):
        self.dir = team_dir
        self.dir.mkdir(exist_ok=True)
        self.config_path = self.dir / "config.json"
        self.config = self._load_config()
        self.threads = {}

实现位于 agents/s09_agent_teams.py#L125-L145。名册变更通过 _save_config() 写回(json.dumps(..., indent=2)),_find_member(name) 按名字线性查找成员。

4.2 spawn():生成、状态检查与线程启动

文档第 2 条机制给出 spawn 的骨架:

def spawn(self, name: str, role: str, prompt: str) -> str:
    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)
    thread.start()
    return f"Spawned teammate '{name}' (role: {role})"

实际实现(agents/s09_agent_teams.py#L147-L165)比示意更完整,值得逐行注意:

  • 同名复用与状态保护:如果名册中已存在同名成员,且其 status 既不是 idle 也不是 shutdown(例如正在 working),直接返回 Error: '{name}' is currently {status};若处于 idle,则复用该成员记录、更新 role 并置回 working,而不是重复 append 造成名册里出现两个同名条目;
  • 持久身份的落地方式:成员记录先写入 config.json、再启动线程,因此即使进程重启,名册里的历史成员依然存在(只是线程没了)——这就是「身份持久于单次 prompt 之上」的最小实现;
  • daemon 线程:每个 teammate 是一个 threading.Thread(..., daemon=True),主进程退出时随之终止。

五、teammate 的 agent loop:每轮 LLM 调用前先查收件箱

文档第 4 条机制:每个 teammate 在每次 LLM 调用之前检查自己的 inbox,把收到的消息注入上下文:

def _teammate_loop(self, name, role, prompt):
    messages = [{"role": "user", "content": prompt}]
    for _ in range(50):
        inbox = BUS.read_inbox(name)
        if inbox != "[]":
            messages.append({"role": "user",
                "content": f"<inbox>{inbox}</inbox>"})
        response = client.messages.create(...)
        if response.stop_reason != "tool_use":
            break
        # execute tools, append results...
    self._find_member(name)["status"] = "idle"

完整实现(agents/s09_agent_teams.py#L167-L205)的关键参数与边界:

要素 取值 说明
迭代上限 for _ in range(50) 每个 teammate 最多 50 轮 LLM 调用,防止失控循环
单次输出 max_tokens=8000 与 Lead 侧一致
system prompt You are '{name}', role: {role}, at {WORKDIR}. Use send_message to communicate. Complete your task. 身份 = 名字 + 角色 + 工作目录
注入方式 收到的每条消息以 {"role": "user", "content": json.dumps(msg)} 追加 消息是结构化 JSON,模型可读取 fromtypecontent
异常处理 client.messages.create 抛异常即 break 退出循环 失败不重试,直接落位 idle
收尾 除非状态已是 shutdown,否则写回 status = "idle"_save_config() WORKING → IDLE 的落盘

teammate 侧的工具由 _teammate_tools() 定义(agents/s09_agent_teams.py#L223-L238),共 6 个:bashread_filewrite_fileedit_file(注释标明「these base tools are unchanged from s02」),外加协作专用的 send_messageread_inbox。注意 teammate 的 send_messagemsg_type 参数在 schema 里带 enum: list(VALID_MSG_TYPES),即协议约束直接暴露给模型;而 teammate 没有 broadcast——群发是 Lead 的权限。_exec() 分发函数(agents/s09_agent_teams.py#L207-L221)中,send_message 以 sender 的 teammate 名字作为消息的 from 字段写入目标邮箱,read_inbox 则读取并排空自己的邮箱。

六、Lead 侧:9 个工具、Lead inbox 与 /team、/inbox 命令

Lead 与 teammate 的区别在于工具集更大、且拥有「团队视角」。Lead 的分发表 TOOL_HANDLERSagents/s09_agent_teams.py#L310-L321)与工具定义 TOOLSagents/s09_agent_teams.py#L324-L343)共 9 个工具:

工具 说明 备注
bash 执行 shell 命令 危险命令拦截 + 120s 超时(见第七节)
read_file / write_file / edit_file 文件读写与精确替换 均受 _safe_path 约束
spawn_teammate 生成持久 teammate TEAM.spawn(name, role, prompt),入参 name/role/prompt 均必填
list_teammates 列出全部成员及 status 对应 TeammateManager.list_all(),输出形如 Team: default + alice (coder): idle
send_message 向指定 teammate 的 inbox 发消息 sender 固定为 "lead"msg_type 有枚举约束
read_inbox 读取并排空 lead 的 inbox json.dumps(BUS.read_inbox("lead"), indent=2)
broadcast 向全体 teammate 群发 BUS.broadcast("lead", content, TEAM.member_names())

Lead 自己的 agent loop(agents/s09_agent_teams.py#L346-L379)与 teammate loop 结构同构,但有两点不同:(1) 它是 while True 的无限循环(单轮对话内),而 teammate 有 50 轮上限;(2) 它在每轮 LLM 调用前执行 BUS.read_inbox("lead"),非空时以 <inbox>{...}</inbox> 包裹 JSON 后追加为一条 user 消息——这意味着 teammate 回复 Lead 的消息会在 Lead 的下一轮模型调用中被「自然」看到。

主循环(agents/s09_agent_teams.py#L382-L404)提供两个本地命令:

  • /team:打印 TEAM.list_all(),即带状态的名册;
  • /inbox:打印 BUS.read_inbox("lead", False)——注意 clear=False只读不排空,让你可以反复查看 Lead 邮箱而不会把消息消费掉。

七、工程细节:s09 中的安全边界

s09 在协作机制之外保留了从 s02 继承的工具安全基线(agents/s09_agent_teams.py#L255-L307),在多 agent 并发写文件的场景下尤为关键:

  • 路径越界拦截_safe_path() 把相对路径解析到 WORKDIR 后校验 path.is_relative_to(WORKDIR)../ 逃逸直接抛 Path escapes workspace 错误。由于 Lead 和所有 teammate 共享同一工作目录,这道约束防止某个 teammate 把文件写进仓库之外;
  • 危险命令黑名单_run_bash() 检查 rm -rf /sudoshutdownreboot> /dev/ 等子串并返回 Error: Dangerous command blocked
  • 超时与输出截断:shell 命令 120 秒超时,输出(stdout+stderr)截断到 50000 字符,避免任何一个 teammate 的失控命令把上下文撑爆。

需要说明的是,这些是教学级别的轻量防护,并非沙箱;s09 中多个线程并发访问文件时也没有跨进程文件锁——从源码结构看,课程把「并发正确性」留给了后续章节的演进。

八、与 s08 的差异对照

文档「What Changed From s08」表格(原样继承自 docs/ja/s09-agent-teams.md):

Component Before (s08) After (s09)
Tools 6 9 (+spawn/send/read_inbox)
Agents Single Lead + N teammates
Persistence None config.json + JSONL inboxes
Threads Background cmds Full agent loops per thread
Lifecycle Fire-and-forget idle -> working -> idle
Communication None message + broadcast

一句话概括:s08 的线程里跑的是「命令」,s09 的线程里跑的是「完整的 agent loop」;s08 没有任何持久状态,s09 则有了名册 + 邮箱两层磁盘状态。

九、动手运行(Try It)

9.1 环境准备

依赖见 requirements.txtanthropic>=0.25.0python-dotenv>=1.0.0(另有 pyyaml>=6.0 供后续章节使用)。脚本通过 load_dotenv(override=True) 读取环境变量(agents/s09_agent_teams.py#L56-L62),因此需要在仓库根目录(或 .env 文件)中准备好:

export ANTHROPIC_API_KEY=sk-...
export MODEL_ID=<你的模型 ID>        # 必填,os.environ["MODEL_ID"]
export ANTHROPIC_BASE_URL=<可选>    # 若设置,会自动清除 ANTHROPIC_AUTH_TOKEN

9.2 启动与示例指令

文档「Try It」给出的运行方式与五条示例指令:

cd learn-claude-code
python agents/s09_agent_teams.py
  1. Spawn alice (coder) and bob (tester). Have alice send bob a message.
  2. Broadcast "status update: phase 1 complete" to all teammates
  3. Check the lead inbox for any messages
  4. 输入 /team 查看带状态(working / idle)的团队名册
  5. 输入 /inbox 手动检查 Leader 的收件箱(只读、不排空)

运行过程中,每个 teammate 的每次工具调用都会打印 [名字] 工具名: 输出前 120 字符agents/s09_agent_teams.py#L195),Lead 侧打印 > 工具名: 与输出前 200 字符,可以据此实时观察多个 agent loop 的交叉执行。跑完后检查 .team/config.json.team/inbox/*.jsonl,能直接看到名册状态和通信记录(在对应 inbox 被 drain 之前)。

十、局限与后续演进

基于 s09 的源码与文档,可以归纳出几个明确的边界,它们也正是课程后续章节的主题:

  • 消息类型只声明不处理shutdown_request / shutdown_response / plan_approval_response 在 s09 中仅存在于白名单,实际的关闭握手与计划审批协议在下一章 s10(Team Protocols)中实现;
  • drain-on-read 的收件箱语义read_inbox 读后即清空,且 Lead 的 agent_loopread_inbox 工具都可能消费 Lead 邮箱——从源码结构看,消息「谁能看到」完全取决于谁先调用,没有投递确认机制;
  • 50 轮硬上限与异常即停:teammate 达到 50 轮或模型调用抛异常就直接置为 idle,没有失败重试与错误上报通道。

值得注意的是,仓库后续章节 s13_agent_teams 将这套 mailbox 机制演进为更完整的 Team 运行时(.mailboxes/ 邮箱、任务看板原子认领、worktree 工作目录绑定、类型化协议与计划闸门),其回归测试 tests/test_agent_teams_runtime.py 中已包含 s09 尚未覆盖的防御,例如拒绝 ../escape 这类不安全邮箱收件人、以及禁止 lead/agent 等保留名被注册为普通 teammate。如果你想理解 s09 这套设计在并发与安全性上「长成什么样」,建议对照阅读 s13_agent_teams/code.py 与上述测试文件;而 s09 本身的价值,在于用约 400 行代码展示了多智能体协作的最小闭环:持久身份(config.json)+ 独立 agent loop(线程)+ 文件邮箱(JSONL inbox)+ 每轮注入(inbox check),四者缺一不可。

<输出文章>

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384