AstroResearch/docs/architecture/agent/team.md
Asfmq cec4b8cf7b feat: Docker 容器化、Cookie 鉴权、Coordinator 编排、FTS5 搜索与 P1-P3 全面收尾
Docker 容器化部署
  - 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
    两种镜像,Docker Compose 一键启动
  - build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
  - 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制

  安全:Cookie-Based 鉴权系统
  - HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
  - 登录/登出/验证接口 + 中间件注入
  - 前端登录页面 + 退出按钮
  - 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
  - 书签脚本 fetch 添加 credentials:'include'

  Coordinator 模式 (P2)
  - 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
  - WorkerPool + Semaphore 并发控制 + 超时保护
  - 前端协调者模式开关

  Hook 系统:UserPromptSubmit 事件 (P2)
  - 第 13 个生命周期事件,fire-and-forget 审计

  FTS5 全文搜索 (P3)
  - agent_sessions_fts + agent_messages_fts 虚拟表
  - search_history Agent 工具 + /api/search/history HTTP 接口
  - 前端防抖搜索框 + 仅当前会话筛选

  工具加载优化 (P3)
  - defer_loading 延迟加载 (7 个重型工具)
  - is_readonly 只读标记 (9 个查询工具)
  - classifier_summary 工具目录供 LLM 按需判断

  模型回退策略 (P3)
  - LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
  - LlmClient model 改为 Arc<RwLock> 支持运行时切换
  - 连续 3 次过载后自动切换

  压缩记忆桥接 (P3)
  - 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)

  git2 依赖修复
  - 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
2026-06-23 20:22:06 +08:00

235 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 多 Agent 团队 (`team/`)
基于**文件邮箱 (file inbox)** 的轻量级多 Agent 协作系统,参考 Claude Code Agent Teams 设计。
## 架构总览
```mermaid
graph LR
subgraph Team["src/agent/team/"]
direction TB
Mod["mod.rs (11 lines)<br/>模块声明与重导出"]
Config["config.rs (53 lines)<br/>TeamConfig, MemberConfig, MemberStatus"]
Inbox["inbox.rs (110 lines)<br/>文件邮箱 (append-only JSONL + drain)"]
Manager["manager.rs (205 lines)<br/>spawn/stop/send/broadcast/check_inbox/list"]
Teammate["teammate.rs (270 lines)<br/>队友 ReAct 循环<br/>(idle poll → react turn → report result)"]
end
Tools["src/agent/tools/team.rs (292 lines)<br/>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<br/>session_id + members[]"]
Handles["HashMap&lt;name, TeamMemberHandle&gt;"]
end
subgraph InboxFS[".team/{session_id}/inbox/"]
L["lead.jsonl"]
S["searcher.jsonl"]
R["reader.jsonl"]
end
subgraph Teammates["Teammates (tokio::spawn)"]
T1["searcher<br/>ReAct loop<br/>max 5 steps"]
T2["reader<br/>ReAct loop<br/>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()` 清理队友和收件箱文件