# 多 Agent 团队 (`team/`) 基于**文件邮箱 (file inbox)** 的轻量级多 Agent 协作系统,参考 Claude Code Agent Teams 设计。 ## 架构总览 ```mermaid graph LR subgraph Team["src/agent/team/"] direction TB Mod["mod.rs (11 lines)
模块声明与重导出"] Config["config.rs (53 lines)
TeamConfig, MemberConfig, MemberStatus"] Inbox["inbox.rs (110 lines)
文件邮箱 (append-only JSONL + drain)"] Manager["manager.rs (205 lines)
spawn/stop/send/broadcast/check_inbox/list"] Teammate["teammate.rs (270 lines)
队友 ReAct 循环
(idle poll → react turn → report result)"] end Tools["src/agent/tools/team.rs (292 lines)
4 个团队工具暴露给 Lead Agent"] Manager --> Tools Teammate --> Manager Inbox --> Manager Inbox --> Teammate ``` ```mermaid graph TD subgraph Lead["Lead Agent (AgentRuntime)"] LR["ReAct Loop"] TR["ToolRegistry"] end subgraph TM["TeamManager"] Config["TeamConfig
session_id + members[]"] Handles["HashMap<name, TeamMemberHandle>"] end subgraph InboxFS[".team/{session_id}/inbox/"] L["lead.jsonl"] S["searcher.jsonl"] R["reader.jsonl"] end subgraph Teammates["Teammates (tokio::spawn)"] T1["searcher
ReAct loop
max 5 steps"] T2["reader
ReAct loop
max 5 steps"] end LR -->|"spawn_teammate"| TM LR -->|"send_teammate_message"| S LR -->|"check_team_inbox"| L LR -->|"team_broadcast"| S LR -->|"team_broadcast"| R TM -->|"tokio::spawn"| T1 TM -->|"tokio::spawn"| T2 T1 -->|"append result"| L T2 -->|"append result"| L T1 -->|"drain inbox"| S T2 -->|"drain inbox"| R ``` ## 通信机制:文件邮箱 消息传递不通过 channel 或共享内存,而是通过 **append-only JSONL 文件**: ``` .team/{session_id}/inbox/ ├── lead.jsonl ← 队友将结果写入此处 ├── searcher.jsonl ← Lead 将任务写入此处 └── reader.jsonl ← Lead 将任务写入此处 ``` ### 消息类型 (`TeamMessage`) | 字段 | 类型 | 说明 | |:---|:---|:---| | `from` | `String` | 发送者名称 | | `to` | `String` | 接收者名称 | | `content` | `String` | 消息内容 | | `msg_type` | `TeamMessageType` | `task` / `result` / `question` / `answer` / `status` | | `timestamp` | `String` | ISO 8601 时间戳 (UTC) | ### 核心操作 - **`append_message(team_dir, agent_name, msg)`**:追加一行 JSON 到接收者文件,自动创建目录 - **`drain_inbox(team_dir, agent_name)`**:读取所有消息行 → 清空文件 → 返回 Vec(破坏性读取) - **`has_pending(team_dir, agent_name)`**:检查文件是否存在且非空(用于快速轮询) > **已知问题**:`drain_inbox()` 在 `read_to_string` 和 `write("")` 之间存在 TOCTOU 竞态条件。若清空前有新消息追入,该消息会丢失。生产环境应使用 advisory file lock 或原子 rename 替代。 ## 队友生命周期 ``` ┌──────────┐ │ SPAWNING │ ← Manager 创建句柄,tokio::spawn 启动循环 └────┬─────┘ ▼ ┌──────────┐ 有新消息到达 ┌──────────┐ │ IDLE │ ─────────────────→ │ WORKING │ │ │ ←──────────────── │ │ │ 5s×12 │ 任务完成/超时 │ ReAct │ │ 轮询 │ │ max 5步 │ └────┬─────┘ └──────────┘ │ 收到取消信号 ▼ ┌──────────┐ │ SHUTDOWN │ ← 发送告别 Status 消息给 Lead,退出循环 └──────────┘ ``` ### IDLE 阶段细节 (`teammate.rs:68-92`) 1. 设置状态为 `Idle` 2. `drain_inbox()` 检查是否有待处理消息 3. 若为空:以 **5 秒间隔轮询 `has_pending()`**,最长 60 秒(12 次 × 5 秒) 4. 在轮询中,若 `cancelled` 标志被设置或 `has_pending()` 返回 true,则提前退出 5. 超时无消息 → 回到步骤 2 6. 有新消息 → 进入 WORKING ### WORKING 阶段细节 (`teammate.rs:103-152`) 1. 设置状态为 `Working` 2. 将收件箱消息作为 `user` 角色消息注入到队友的对话历史中 3. 调用 `run_teammate_react_turn()`:最多 **5 步**的简化 ReAct 循环 4. 若返回结果 → 封装为 `TeamMessageType::Result` 发送到 `lead` 的收件箱 5. 若返回 `None`(错误/超时)→ 静默回到 IDLE(**不通知 Lead 出错**) ## 队友 ReAct 循环 vs 主 Agent 循环 | 特性 | 主 Agent (Lead) | 队友 (Teammate) | |:---|:---|:---| | 最大步数 | 8(可配置) | 5(硬编码 `min(config.max_steps, 5)`) | | 流式输出 | SSE 实时推送到前端 | 无 SSE,静默消费 | | 工具注册表 | 完整(含 subagent、team 工具) | 排除了 subagent 和 team 工具 | | 上下文压缩 | 四层压缩 + CircuitBreaker | 简单 token 估算 + `compact::compress_context` | | Hooks | PreToolUse / PostToolUse / Stop | 无 | | 权限检查 | PermissionChecker | 无 | | 数据库持久化 | agent_messages 表 | 无 | | 后台通知 | BgNotificationQueue | 有 Queue 但未使用 | | Skills | 两层加载 (list + load_skill) | 无,skill_registry 仅用于工具构造 | 队友的工具注册表构造 (`teammate.rs:40-41`): ```rust let tool_registry = ToolRegistry::new_with_queue(Some(queue.clone()), app_state.skill_registry.clone()); ``` `ToolRegistry::new_with_queue()` 不注册 subagent 和 team 工具,防止无限委托链。 ## TeamManager 设计 ### 锁顺序约定 (CRITICAL) 存在两个 `tokio::sync::Mutex` 的嵌套获取: 1. `TeamManager.handles`(外层) 2. `TeamMemberHandle.status`(内层) 所有代码必须遵守 **handles → status** 的顺序,否则死锁。`list_members()` 作为正确顺序的参考实现。 ### 主要操作 | 操作 | 说明 | 锁行为 | |:---|:---|:---| | `spawn(name, role)` | tokio::spawn 队友循环,注册句柄 | 获取 handles 锁 | | `stop(name)` | 设置 cancelled 标志 | 获取 handles 锁 | | `stop_all()` | 停止所有队友 | 获取 handles 锁 | | `send_message(from, to, content, type)` | 追加消息到接收者收件箱文件 | 无锁(纯文件 I/O) | | `broadcast(from, content)` | 向 config.members 中所有非发送者成员追加消息 | 无锁(纯文件 I/O) | | `check_inbox(agent_name)` | drain_inbox 并返回消息 | 无锁(纯文件 I/O) | | `list_members()` | 获取所有队友的 (name, role, status) | handles → 各 status(正确顺序) | ### 队友系统提示词 (`manager.rs:70-93`) 队友收到的是天体物理学研究助手角色设定 + 工具使用指引 + 通信协议说明。系统提示词始终通过 `build_teammate_system_prompt(role)` 生成,**不使用** `MemberConfig.system_prompt` 字段。 ## 4 个团队工具 工具定义在 `src/agent/tools/team.rs`,均设置 `InterruptBehavior::Block`(中断时先完成副作用再停止)。P3 工具分类注解已应用: | 工具名 | 参数 | 功能 | 接收者 | 延迟加载 | 只读 | |:---|:---|:---|:---|:---|:---| | `spawn_teammate` | `name`, `role` | 启动一个后台队友 | — | ✅ `defer_loading` | — | | `send_teammate_message` | `to`, `content` | 向指定队友发送消息 | 队友收件箱 | ✅ `defer_loading` | — | | `team_broadcast` | `content` | 向所有队友广播消息 | 所有队友收件箱 | ✅ `defer_loading` | — | | `check_team_inbox` | `agent_name?` (默认 "lead") | 读取并清空收件箱 | Lead 收件箱 | — | ✅ `is_readonly` | > **延迟加载说明**(P3):`spawn_teammate`、`send_teammate_message`、`team_broadcast` 标记为 `defer_loading=true`,不随常驻工具注入 system prompt,仅当 LLM 判断需要多 Agent 协作时通过 `tool_catalog` 按需加载。`check_team_inbox` 标记为 `is_readonly=true`(只读取文件邮箱,不修改队友状态),在 auto 模式下可跳过权限确认。 典型协作流程: ``` 1. Lead 调用 spawn_teammate(name="searcher", role="ADS文献搜索专家") 2. Lead 调用 send_teammate_message(to="searcher", content="搜索2024年暗物质间接探测综述") 3. Lead 继续自己的 ReAct 循环(可以同时发起另一个 spawn_teammate) 4. 队友在后台执行 ReAct 循环(最多5步),完成后将结果写入 lead.jsonl 5. Lead 调用 check_team_inbox() 读取队友结果 6. Lead 综合队友结果,给出最终回答 ``` ## 与子代理 (`subagent`) 的对比 代码库中存在**两种委托机制**,适用不同场景: | | 子代理 (`subagent`) | 团队 (`team`) | |:---|:---|:---| | 执行模式 | 同步:Lead 等待完成 | 异步:后台运行,Lead 主动轮询 | | 并发度 | 每次调用 1 个 | 可同时运行多个队友 | | 上下文 | 任务完成后丢弃 | 跨任务累积(直到压缩) | | 通信方式 | 工具调用 → 返回摘要 | 文件邮箱:写入/轮询 | | Hooks | PreToolUse / PostToolUse / SubagentStart / SubagentStop | 无 | | 权限检查 | PermissionChecker | 无 | | 进度透传 | SSE 事件到父代理 | 无 | | DB 持久化 | agent_messages 表 | 无 | | 适用场景 | "去完成这个子任务,给我摘要" | "留在后台,持续处理我分配的任务" | ## 当前状态:未接入 > **重要**:`TeamManager` 和 4 个团队工具已完整实现,但当前**未接入 AgentRuntime**。 > > `AgentRuntime::new()` 调用 `ToolRegistry::new_with_queue()`,而非 `ToolRegistry::new_with_team()`。团队工具通过独立的 `add_team_tools()` 函数接入,但该函数在当前代码路径中从未被调用。 > > 实际可用的委托功能由 `subagent` 工具提供(通过 `SubAgentTool` 已接入)。 > > 接入方法:在 `AgentRuntime::new()` 和 `with_config()` 中,创建 `TeamManager` 实例并用 `ToolRegistry::new_with_team()` 替代 `new_with_queue()`。 ## 改进建议 1. **修复 drain_inbox 竞态条件**:使用 advisory file lock 或原子 rename 2. **队友错误传播**:`run_teammate_react_turn()` 返回 `None` 时应通知 Lead 3. **消息关联 ID**:添加 `correlation_id` 字段,支持请求-响应匹配 4. **降低锁持有时间**:将 `send_message` / `broadcast` 的文件 I/O 移出锁临界区(当前已是无锁,但工具层仍持有 `team_manager` 锁) 5. **队友名称去重**:spawn 同名队友前检查是否已有活跃队友 6. **会话级清理**:Agent 会话结束时调用 `stop_all()` 清理队友和收件箱文件