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 系统依赖
235 lines
10 KiB
Markdown
235 lines
10 KiB
Markdown
# 多 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<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<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()` 清理队友和收件箱文件
|