构建mini Claude Code:09 - 从「一次性」到「持久」:Agent Teams 如何让协作真正发生

5 阅读13分钟

构建mini Claude Code:09 - 从「一次性」到「持久」:Agent Teams 如何让协作真正发生

📍 导航指南

这是「从零构建 Claude Code」系列的第九篇。根据你的背景,选择合适的阅读路径:


目录

第一部分:理论基础 🧠

第二部分:TeammateManager 设计 ⚙️

第三部分:代码实现 💻

第四部分:扩展方向 🔭

附录


引言

考虑这个场景:

用户: "帮我实现一个新功能,需要写代码、做 Code Review、跑测试"

Subagent 方案(v7 之前):
  主 Agent → run_task("写代码", subagent_type="general-purpose")  # 完成后消失
  主 Agent → run_task("Code Review", subagent_type="general-purpose")  # 完成后消失
  主 Agent → run_task("跑测试", subagent_type="general-purpose")  # 完成后消失

问题:
  - Reviewer 看不到 Coder 的思路
  - Tester 不知道 Review 发现了什么问题
  - 每个 Agent 都是孤立的,没有上下文传递

这不是真正的协作,这是流水线。

v8_agent.py 引入的 Agent Teams,让每个 Agent 拥有持久的身份、独立的收件箱、跨轮次的记忆——这才是真正的团队协作。

说明:v8_agent.py 在 v7_agent(后台执行 + 持久任务 + 上下文压缩)的基础上,新增了 TeammateManagerMessageBus 和五个团队工具。本文聚焦这个新增的 Agent Teams 系统。


第一部分:理论基础 🧠

Subagent 的本质局限

回顾 v7 的 run_task

def run_task(description: str, prompt: str, subagent_type: str) -> str:
    # 创建独立上下文
    sub_messages = [{"role": "user", "content": prompt}]
    # 运行直到完成
    while True:
        response = client.messages.create(...)
        if response.stop_reason != "tool_use":
            break
        ...
    # 返回结果,上下文丢弃
    return result_text

Subagent 的生命周期:

创建 → 执行 → 返回结果 → 消失
  ↑                         ↑
  孤立的上下文              上下文丢弃

这带来三个根本性的局限:

局限一:无法持续工作

Subagent 是一次性的。它完成任务就消失了。如果任务需要多轮交互——比如 Reviewer 发现问题,Coder 修改,Reviewer 再次检查——Subagent 做不到,因为它没有「下一轮」。

局限二:无法相互通信

两个 Subagent 之间没有通信机制。Coder 完成代码后,Reviewer 只能通过主 Agent 的转述来了解情况,信息在传递中损耗。

局限三:无法共享上下文

每个 Subagent 都从零开始。Reviewer 不知道 Coder 做了哪些权衡,Tester 不知道 Review 发现了什么问题。团队的「集体记忆」不存在。

真实协作需要什么

想象一个真实的软件团队:

Coder:    "我实现了认证模块,用了 JWT,放在 auth/ 目录"
Reviewer: "看了你的代码,token 过期处理有个问题,第 42 行"
Coder:    "明白,我修一下"
Reviewer: "好了,这次没问题了,可以合并"
Tester:   "我看到你们的对话了,我来写对应的测试"

这个过程有几个关键特征:

  1. 持久身份:Coder、Reviewer、Tester 是固定的角色,不是一次性的
  2. 直接通信:他们可以直接对话,不需要通过「主管」转述
  3. 共享上下文:Tester 能看到 Coder 和 Reviewer 的对话历史
  4. 异步协作:Reviewer 在看代码时,Coder 可以去做别的事

核心洞察:一切皆文件

v8_agent.py 的注释里有一句话:

Key insight: "Teammates that can talk to each other."

但实现这个洞察的方式更值得关注:用文件系统作为通信基础设施

每个 Teammate 有一个 JSONL 格式的收件箱文件:

.sessions/20260224_130000/team/inbox/
├── coder.jsonl      ← Coder 的收件箱
├── reviewer.jsonl   ← Reviewer 的收件箱
└── tester.jsonl     ← Tester 的收件箱

发消息 = 写文件。读消息 = 读文件并清空。

这不只是实现细节,这是一种设计哲学:通信状态外化为文件,Agent 的「记忆」不依赖进程内存,而是持久化在文件系统中。

这和上一篇的持久化任务系统(.tasks/)是同一个理念的延伸——一切皆文件


第二部分:TeammateManager 设计 ⚙️

三个核心组件

Agent Teams 由三个组件构成:

┌─────────────────────────────────────────────────────────┐
│ Agent Teams 系统                                         │
│                                                         │
│  1. TeammateManager                                     │
│     管理 Teammate 的生命周期                              │
│     spawn / list / 状态追踪                              │
│                                                         │
│  2. MessageBus                                          │
│     文件系统消息队列                                      │
│     send / read_inbox / broadcast                       │
│                                                         │
│  3. Teammate Loop                                       │
│     每个 Teammate 的独立 Agent 循环                       │
│     运行在独立线程,有自己的 messages 历史                  │
└─────────────────────────────────────────────────────────┘

MessageBus:文件即通信

MessageBus 是整个系统的核心。它把「发消息」和「读消息」映射到文件操作:

发消息(send):
  sender="lead", to="coder", content="请实现认证模块"
  → 追加到 inbox/coder.jsonl
  → {"type": "message", "from": "lead", "content": "...", "timestamp": 1234567890}

读消息(read_inbox):
  name="coder"
  → 读取 inbox/coder.jsonl 的所有行
  → 清空文件(drain 语义)
  → 返回消息列表

为什么用 JSONL 而不是普通文本?

JSONL(每行一个 JSON)的优势:
  - 结构化:消息有类型、发送者、时间戳
  - 追加安全:多个线程同时写不会互相覆盖
  - 易解析:逐行读取,不需要解析整个文件
  - 可审计:文件保留了完整的消息历史

消息类型(VALID_MSG_TYPES):

VALID_MSG_TYPES = {
    "message",              # 普通消息
    "broadcast",            # 广播消息
    "shutdown_request",     # 请求关闭
    "shutdown_response",    # 关闭响应
    "plan_approval_response" # 计划审批响应
}

类型系统让 Teammate 能区分不同性质的消息,做出不同的响应。

工作流:团队如何协作

主 Agent(lead)
    │
    ├─ spawn_teammate("coder", "实现认证模块")
    │       │
    │       └─ 独立线程启动,开始工作
    │
    ├─ spawn_teammate("reviewer", "Review coder 的代码")
    │       │
    │       └─ 独立线程启动,等待消息
    │
    │  ... lead 继续其他工作 ...
    │
    │  [coder 完成代码]
    │  coder → send_message("reviewer", "代码在 auth/ 目录,请 Review")
    │                │
    │                └─ 写入 inbox/reviewer.jsonl
    │
    │  [reviewer 的下一轮循环]
    │  reviewer → read_inbox()  ← 读到 coder 的消息
    │  reviewer → 开始 Review
    │  reviewer → send_message("coder", "第 42 行有问题")
    │
    │  [coder 的下一轮循环]
    │  coder → read_inbox()  ← 读到 reviewer 的反馈
    │  coder → 修复问题
    │  coder → send_message("lead", "修复完成")
    │
    │  [lead 的下一轮循环]
    │  lead → read_inbox()  ← 读到 coder 的完成通知
    │  lead → 继续下一步

关键点:整个过程中,lead 不需要轮询或等待。每个 Teammate 在自己的线程里运行,通过文件系统异步通信。


第三部分:代码实现 💻

MessageBus 类

class MessageBus:
    def __init__(self, inbox_dir: Path):
        self.dir = inbox_dir
        self.dir.mkdir(parents=True, exist_ok=True)

    def send(self, sender: str, to: str, content: str,
             msg_type: str = "message", extra: dict = None) -> str:
        if msg_type not in VALID_MSG_TYPES:
            return f"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}"
        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")
        return f"Sent {msg_type} to {to}"

    def read_inbox(self, name: str) -> list:
        inbox_path = self.dir / f"{name}.jsonl"
        if not inbox_path.exists():
            return []
        messages = [json.loads(l) for l in inbox_path.read_text().strip().splitlines() if l]
        inbox_path.write_text("")  # drain
        return messages

    def broadcast(self, sender: str, content: str, teammates: list) -> str:
        count = sum(1 for name in teammates
                    if name != sender and
                    not self.send(sender, name, content, "broadcast").startswith("Error"))
        return f"Broadcast to {count} teammates"

read_inbox 的 drain 语义很重要:读完就清空。这保证了每条消息只被处理一次,不会重复消费。

TeammateManager 类

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 = (json.loads(self.config_path.read_text())
                       if self.config_path.exists()
                       else {"team_name": "default", "members": []})
        self.threads = {}

    def spawn(self, name: str, role: str, prompt: str) -> str:
        member = self._find(name)
        if member:
            if member["status"] not in ("idle", "shutdown"):
                return f"Error: '{name}' is currently {member['status']}"
            member.update({"status": "working", "role": role})
        else:
            member = {"name": name, "role": role, "status": "working"}
            self.config["members"].append(member)
        self._save()
        t = threading.Thread(target=self._loop, args=(name, role, prompt), daemon=True)
        self.threads[name] = t
        t.start()
        return f"Spawned '{name}' (role: {role})"

spawn 做了三件事:

  1. 注册成员到 config.json(持久化)
  2. 启动独立线程运行 Agent 循环
  3. 立刻返回,不等待 Teammate 完成

注意 daemon=True:守护线程,主进程退出时自动终止。

Teammate 的 Agent 循环

def _loop(self, name: str, role: str, prompt: str):
    sys_prompt = f"You are '{name}', role: {role}, at {WORKDIR}. Use send_message to communicate. Complete your task."
    messages = [{"role": "user", "content": prompt}]
    tools = [bash, read_file, write_file, edit_file, send_message, read_inbox]

    for _ in range(50):  # 最多 50 轮
        # 每轮开始前检查收件箱
        for msg in BUS.read_inbox(name):
            messages.append({"role": "user", "content": json.dumps(msg)})

        response = client.messages.create(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":
            break  # 任务完成

        results = []
        for block in response.content:
            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(name)
    if member and member["status"] != "shutdown":
        member["status"] = "idle"
        self._save()

这个循环有几个关键设计:

每轮检查收件箱:在每次 LLM 调用之前,先读取收件箱。这保证了 Teammate 能及时响应其他成员的消息。

消息注入 messages:收件箱的消息被追加到 messages 列表,成为 LLM 上下文的一部分。Teammate 的「记忆」就是这个不断增长的 messages 列表。

最多 50 轮:防止无限循环。50 轮对于大多数任务已经足够。

状态更新:循环结束后,状态从 working 变为 idle,可以被重新 spawn。

五个工具

主 Agent(lead)通过五个工具与团队交互:

# 1. 创建 Teammate
{"name": "spawn_teammate",
 "description": "Spawn a persistent teammate agent in its own thread.",
 "input_schema": {"properties": {"name": ..., "role": ..., "prompt": ...}}}

# 2. 查看团队状态
{"name": "list_teammates",
 "description": "List all teammates with name, role, status."}

# 3. 发消息给某个 Teammate
{"name": "send_message",
 "description": "Send a message to a teammate's inbox.",
 "input_schema": {"properties": {"to": ..., "content": ..., "msg_type": ...}}}

# 4. 读取 lead 自己的收件箱
{"name": "read_inbox",
 "description": "Read and drain the lead's inbox."}

# 5. 广播给所有 Teammate
{"name": "broadcast",
 "description": "Send a message to all teammates.",
 "input_schema": {"properties": {"content": ...}}}

Teammate 内部也有 send_messageread_inbox,但它们只能发给其他成员,不能调用 spawn_teammate——这保持了层级结构:lead 负责组建团队,Teammate 负责执行任务。

团队状态持久化

团队配置保存在 config.json

{
  "team_name": "default",
  "members": [
    {"name": "coder", "role": "实现认证模块", "status": "idle"},
    {"name": "reviewer", "role": "Code Review", "status": "working"},
    {"name": "tester", "role": "编写测试", "status": "idle"}
  ]
}

这意味着即使主进程重启,团队的组成和状态也不会丢失。结合 .tasks/ 持久化任务系统,整个工作状态都外化到了文件系统。


第四部分:扩展方向 🔭

方向一:角色专业化

当前所有 Teammate 使用相同的工具集。可以根据角色限制工具:

ROLE_TOOLS = {
    "coder":    ["bash", "read_file", "write_file", "edit_file", "send_message", "read_inbox"],
    "reviewer": ["read_file", "grep", "send_message", "read_inbox"],  # 只读
    "tester":   ["bash", "read_file", "write_file", "send_message", "read_inbox"],
}

def _loop(self, name: str, role: str, prompt: str):
    tools = ROLE_TOOLS.get(role, DEFAULT_TOOLS)
    ...

Reviewer 只有读权限,不能修改文件——这和真实团队的权限设计一致。

方向二:协作协议

当前的通信是非结构化的自然语言。可以定义结构化协议:

# Coder 完成后发送结构化消息
BUS.send("coder", "reviewer", json.dumps({
    "action": "review_request",
    "files_changed": ["auth/jwt.py", "auth/middleware.py"],
    "summary": "实现了 JWT 认证,token 有效期 24 小时",
    "concerns": ["refresh token 的存储方式还不确定"]
}), msg_type="message")

# Reviewer 收到后,能精确知道要 Review 哪些文件

结构化协议减少了歧义,让 Teammate 能更高效地协作。

方向三:跨会话团队

当前 Teammate 的线程在进程退出时终止。结合 config.json 的持久化,可以实现跨会话恢复:

def resume_team(self):
    """恢复上次会话的团队"""
    for member in self.config["members"]:
        if member["status"] == "working":
            # 上次会话中断时正在工作的成员,重新启动
            self.spawn(member["name"], member["role"],
                       f"继续上次的工作。检查你的收件箱获取最新状态。")

结合文件系统的收件箱,Teammate 重启后能从收件箱里恢复上下文,继续未完成的工作。


常见问题 FAQ

Q: Teammate 和 Subagent(Task)有什么区别?

A: 核心区别是生命周期和通信能力。

Subagent(Task):
  - 一次性:完成任务就消失
  - 孤立:没有通信机制
  - 同步:主 Agent 等待结果
  - 适合:独立的、有明确边界的子任务

Teammate:
  - 持久:有自己的 Agent 循环,可以多轮工作
  - 互联:通过 MessageBus 相互通信
  - 异步:在独立线程运行,主 Agent 不需要等待
  - 适合:需要多轮交互、相互依赖的协作任务

Q: Teammate 的收件箱消息会丢失吗?

A: 不会。消息写入 JSONL 文件后持久化在磁盘上,即使进程崩溃也不会丢失。Teammate 重启后读取收件箱,能看到所有未处理的消息。

Q: 多个 Teammate 同时写同一个收件箱会有竞态问题吗?

A: 文件追加(open(path, "a"))在大多数操作系统上是原子的——每次 write 调用要么完整写入,要么不写入。JSONL 格式(每行一条消息)进一步保证了即使多个线程同时写,每条消息也是完整的。

Q: Teammate 的 50 轮限制够用吗?

A: 对于大多数任务足够。50 轮意味着 Teammate 可以调用 50 次工具,处理 50 次收件箱消息。如果任务需要更多轮次,可以调整这个参数,或者让 Teammate 完成后重新 spawn(状态变为 idle 后可以再次 spawn)。

Q: 如何知道所有 Teammate 都完成了?

A: 调用 list_teammates 查看所有成员的状态。当所有成员都是 idle 时,说明当前轮次的工作都完成了。也可以让每个 Teammate 完成后发消息给 lead,lead 通过 read_inbox 收集完成通知。


📝 结语

从 Subagent 到 Teammate,v8_agent 只加了两个类(TeammateManager + MessageBus)和五个工具。但这个改动背后的思想值得细品:

Subagent 模型:
  主 Agent → 派发任务 → 等待结果 → 派发下一个任务
  Agent 是工具,不是协作者

Teammate 模型:
  主 Agent → 组建团队 → 团队自主协作 → 主 Agent 处理结果
  Agent 是协作者,有自己的判断和沟通能力

更深层的意义是:Agent 的「能力」不只是执行工具,还包括持续工作、主动沟通、响应反馈。

一次性的 Subagent 能完成任务,但不能建立关系。持久的 Teammate 能在多轮交互中积累上下文,形成真正的协作——就像真实团队里的工程师,不是做完一件事就消失,而是持续参与、持续贡献。

而这一切的基础,是文件系统。收件箱是文件,团队配置是文件,任务状态是文件。一切皆文件,让 Agent 的状态从进程内存走向持久存储,从单次会话走向跨会话协作。

结合前几篇的能力:

上下文压缩(v5)  → Agent 能长时间运行
持久化任务(v6)  → Agent 能跨会话追踪任务
后台执行(v7)    → Agent 能并行处理任务
Agent Teams(v8) → Agent 能组建团队协作
                    ↓
              真正的「自主 Agent 系统」

四个能力叠加,才能处理真实世界的复杂任务:长时间、多步骤、有依赖、可并行、需协作。

系列导航