learn-claude-code s09: Agent Teams —— 持久团队与基于文件邮箱的多智能体协作
本篇基于 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_name与members数组,每个成员含name、role、status三个字段(见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)在此之上补充了三处关键细节:
-
消息类型白名单校验。
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 落地,这正是课程「逐层加码」的写法。
-
read_inbox()默认 drain。实现中返回的是list(而非文档示意中的 JSON 字符串),并带clear: bool = True参数:clear=True时读完即write_text("")清空文件;clear=False时只读不清。这个开关被 CLI 的/inbox命令复用(见第六节)。 -
broadcast()的发送者排除。broadcast(sender, content, teammates)遍历名册成员,跳过发送者本人后逐个以msg_type="broadcast"写入对方邮箱,并返回Broadcast to N teammates(agents/s09_agent_teams.py#L112-L118)。
四、TeammateManager:名册管理与线程化 spawn
4.1 名册的加载与持久化
文档第 1 条机制:TeammateManager 用 config.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,模型可读取 from、type、content |
| 异常处理 | client.messages.create 抛异常即 break 退出循环 |
失败不重试,直接落位 idle |
| 收尾 | 除非状态已是 shutdown,否则写回 status = "idle" 并 _save_config() |
WORKING → IDLE 的落盘 |
teammate 侧的工具由 _teammate_tools() 定义(agents/s09_agent_teams.py#L223-L238),共 6 个:bash、read_file、write_file、edit_file(注释标明「these base tools are unchanged from s02」),外加协作专用的 send_message 与 read_inbox。注意 teammate 的 send_message 的 msg_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_HANDLERS(agents/s09_agent_teams.py#L310-L321)与工具定义 TOOLS(agents/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 /、sudo、shutdown、reboot、> /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.txt:anthropic>=0.25.0、python-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
Spawn alice (coder) and bob (tester). Have alice send bob a message.Broadcast "status update: phase 1 complete" to all teammatesCheck the lead inbox for any messages- 输入
/team查看带状态(working / idle)的团队名册 - 输入
/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_loop与read_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),四者缺一不可。
<输出文章>
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00