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

10 KiB
Raw Blame History

多 Agent 团队 (team/)

基于文件邮箱 (file inbox) 的轻量级多 Agent 协作系统,参考 Claude Code Agent Teams 设计。

架构总览

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
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_stringwrite("") 之间存在 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)

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

延迟加载说明P3spawn_teammatesend_teammate_messageteam_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() 清理队友和收件箱文件