AstroResearch/docs/architecture/agent/team.md
Asfmq f6df9d8136 feat: Agent 思考模式前端可控、子代理全链路持久化、权限系统、工具 ID 追踪体系、前端面板与文档架构重构
- AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking
  开关,仅千问/DashScope 时启用
  - 完善权限系统,支持细粒度的权限控制和用户权限申请
  - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构
  - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识
  - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name
  - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段
  - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参
  - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段
  - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化
  - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放
  - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义
  - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档
  - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南
  - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明
2026-06-18 01:21:02 +08:00

233 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`(中断时先完成副作用再停止):
| 工具名 | 参数 | 功能 | 接收者 |
|:---|:---|:---|:---|
| `spawn_teammate` | `name`, `role` | 启动一个后台队友 | — |
| `send_teammate_message` | `to`, `content` | 向指定队友发送消息 | 队友收件箱 |
| `team_broadcast` | `content` | 向所有队友广播消息 | 所有队友收件箱 |
| `check_team_inbox` | `agent_name?` (默认 "lead") | 读取并清空收件箱 | Lead 收件箱 |
典型协作流程:
```
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()` 清理队友和收件箱文件