AstroResearch/docs/architecture/agent/tools.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

22 KiB
Raw Permalink Blame History

工具系统 (Tool System)

Agent 工具系统是 ReAct 循环中 Action → Observation 环节的执行引擎。每个工具遵循 AgentTool trait 向 LLM 暴露 JSON Schema 参数定义,并在 execute 中调用服务层完成实际业务操作。


三层架构

graph TB
    subgraph Layer1["调度层 — AgentRuntime"]
        direction LR
        RT["src/agent/runtime/mod.rs<br/>ReAct 循环<br/>调度 LLM tool_calls → 工具执行 → 结果注入上下文"]
    end

    subgraph Layer2["执行协调层 — Executor"]
        direction LR
        EX["src/agent/runtime/executor.rs<br/>验证 → PreToolUse hooks → 并行调度 → PostToolUse"]
        SX["src/agent/runtime/streaming_executor.rs (流式变体)<br/>流式 tool_use 到达时立即调度 + Sibling Abort<br/>非并发工具独占执行 (executing_non_concurrent 标志)"]
    end

    subgraph Layer3["业务逻辑层 — AgentTool Trait + 工具实现"]
        direction LR
        Tools["src/agent/tools/<br/>每个工具独立子模块,实现 AgentTool trait"]
    end

    Layer1 --> Layer2 --> Layer3

AgentTool Trait

classDiagram
    class AgentTool {
        <<interface>>
        +name() &str
        +description() &str
        +parameters() Value
        +execute(args, ctx) ToolOutput
        +interrupt_behavior() InterruptBehavior
        +is_concurrency_safe(args) bool
        +check_permissions(args) PermissionRule[]
        +causes_sibling_abort() bool
        +execute_with_progress(args, ctx, tx) ToolOutput
        +classifier_summary() String
        +is_readonly() bool
        +defer_loading() bool
    }

    class InterruptBehavior {
        <<enumeration>>
        Cancel   — 可安全中断(只读工具默认)
        Block    — 忽略中断信号直到完成(写入工具)
    }

    class PermissionRule {
        <<enumeration>>
        Deny     — 不可覆盖的拒绝
        Allow    — 显式允许
        Ask      — 需用户确认
    }

    class ToolContext {
        +Arc~AppState~ app_state
        +String session_id
        +bool silent
        +Arc~Mutex~FileStateCache~~ read_file_state
        +Option~UnboundedSender~ sse_tx
        +bool enable_thinking
    }

    class ToolOutput {
        +String content      — 给 LLM 的截断文本
        +bool is_error       — 是否为错误
        +Value metadata      — 给前端的结构化数据
    }

    class ToolRegistry {
        -HashMap~String, Box~AgentTool~~ tools
        -Vec~String~ ordered_names
        +empty() Self
        +new(skill_registry) Self
        +new_with_queue(queue, skill_registry) Self
        +new_with_team(queue, team_manager, skill_registry) Self
        +add_tool(tool)
        +replace_tool(tool)
        +get(name) Option~&dyn AgentTool~
        +definitions() Vec~ToolDefinition~
    }

    AgentTool --> InterruptBehavior
    AgentTool --> PermissionRule
    AgentTool --> ToolContext
    AgentTool --> ToolOutput

方法详解

方法 返回类型 默认值 说明
name() &str (必须) 工具名称,全小写下划线风格,与 LLM function calling name 一致
description() &str (必须) 告知 LLM 何时应调用该工具;作为 base description 注入 system prompt
parameters() Value (必须) JSON Schema 格式的参数定义,直接序列化为 LLM tool definition
execute(args, ctx) ToolOutput (必须) 核心执行逻辑,接收 LLM 传入的参数 + ToolContext 全局状态
interrupt_behavior() InterruptBehavior Cancel 控制用户取消时的行为:Cancel—响应取消信号立即返回错误;Block—忽略取消直到执行完成(保护有副作用的写入操作)
is_concurrency_safe(args) bool false 该工具是否可以与其他工具并发执行。只读工具read_filesearch_papersrag_search 等)覆写为 true;写入工具保持默认 false
check_permissions() Vec<PermissionRule> [] 工具自定义权限规则:Deny{tool, reason} / Allow{tool} / Ask{tool, message}。与 PermissionChecker 管道协同工作
causes_sibling_abort() bool false 该工具错误时是否中止兄弟并行执行。用于 download_paperparse_paper 等关键工具
execute_with_progress(args, ctx, tx) ToolOutput 委托 execute() 长时间操作可覆写,通过 progress_tx 发送进度更新到前端
classifier_summary() String description 第一句,截断到 100 字符 工具简要分类描述(~15 词),供 LLM 判断是否需要加载延迟工具
is_readonly() bool false(保守) 是否只读无副作用。9 个只读工具覆写为 true
defer_loading() bool false(常驻) 是否延迟加载(不在初始 prompt 中。7 个重型/小众工具覆写为 true

工具清单24 个)

文件系统工具6 个)— filesystem/

工具 并发安全 中断行为 功能
read_file Cancel 读取文件,带 FileStateCache mtime 去重;相同 offset/limit 且文件未修改时返回 FILE_UNCHANGED_STUB 占位符
grep_files Cancel 正则搜索文件内容
glob_files Cancel 通配符匹配文件路径
run_bash Cancel Shell 命令执行,有独立超时和输出截断
file_write Block 创建/覆盖文件
file_edit Block 精确字符串替换(基于 old_string 匹配)

安全约束 (filesystem/security.rs)

  • 路径沙箱:只允许 library_dirskills_dir、项目根目录三个根路径
  • 路径穿越防护:字符串级拒绝含 ..~ 的路径
  • canonicalize() 解析符号链接后再做前缀匹配,防止 symlink 绕过

天文科研工具8 个)— astro/

工具 并发安全 功能
search_papers ADS + arXiv 跨库联合检索,自动合并去重,关联本地馆藏状态与引用关系
get_paper_metadata 获取单篇文献完整元数据(作者、期刊、关键词、摘要、引用数、下载/解析状态)
download_paper PDF 下载,支持 Obscura 反爬绕过和多通道 fallback
parse_paper PDF/HTML → Markdown 解析MinerU/ar5iv/IOP/A&A 等多引擎)
get_paper_content 读取已解析的论文全文 Markdown
rag_search 向量相似度检索 + LLM 答案生成(基于 sqlite-vec
query_target CDS Sesame 天体目标查询IAU 名称解析 + 坐标/类型)
save_note 高亮批注持久化到数据库

Agent 自管理工具6 个)

工具 并发安全 中断行为 功能
todo_write Cancel 任务规划,支持 pending/in_progress/completed 状态 + blockedBy DAG 依赖;验证只能有一个 in_progress 任务
compress_context Cancel 设置手动压缩标志位,下一轮 LLM 调用前由 Runtime 执行压缩(纯幂等操作)
load_skill Cancel 按需加载 SKILL.md 技能文件。支持 inline 模式(直接返回内容)和 fork 模式(启动子代理按技能指引执行任务)
save_memory Block 跨会话记忆持久化。写入时门控:质量检查(过短/模糊/瞬时/代码模式)+ Jaccard 70% 去重
ask_user N/A Block 暂停 ReAct 循环向用户提问。oneshot 通道机制:创建问题 → SSE 推送前端 → 阻塞等待 → 5 分钟超时。子代理中不可用silent 模式)
search_history Cancel FTS5 跨会话历史搜索。支持会话标题/摘要和消息内容全文检索,避免重复研究已完成的工作

高级编排工具4 个)

工具 并发安全 中断行为 功能
subagent Block 上下文隔离的子代理。子代理拥有完整工具访问权,但仅最终摘要返回父代理。支持自定义 max_steps默认5最大10
bg_task_run Block 后台异步执行慢速操作(仅支持 download_paperparse_paper)。结果通过 BgNotificationQueue 在下一轮前注入
bg_task_check Cancel 查询后台任务状态(可指定 task_id 或列出全部)
团队 4 工具 Block spawn_teammate / send_teammate_message / team_broadcast / check_team_inbox — 基于文件收件箱的多 Agent 协作

执行流水线

从 LLM 返回 tool_calls 到结果注入 messages[] 的完整数据流:

LLM stream → tool_calls[]
    │
    ▼
┌─ validate_and_prepare() ─────────────────────────────────────┐
│  1. 死循环检测 (DuplicateDetector):                            │
│     连续相同 (tool_name, args) ≥ threshold → 注入错误消息     │
│  2. JSON 参数解析: 失败则注入 tool_result 错误                │
│  3. 修复空 tool_call_id (UUID 前缀)                           │
└──────────────────────────────────────────────────────────────┘
    │
    ▼  PreparedCall[]
    │
    ├─ 发送 ToolCall SSE 事件 → 前端实时渲染
    │
    ├─ PreToolUse hooks (顺序执行)
    │   ├─ 可拦截 (Block) / 修改参数 (MutateInput) / 注入上下文 (AppendContext)
    │   └─ mutated_args + additional_contexts 收集
    │
    ▼
┌─ execute_parallel() — ToolPartitioner 批次调度 ──────────────┐
│                                                               │
│  Phase 3a: 构建非拒绝工具的 (原索引, PreparedCall) 映射       │
│  Phase 3b: ToolPartitioner::partition() 分区                  │
│    · 连续 concurrency_safe 工具 → 并行批次                    │
│    · 非 concurrency_safe 工具 → 独占串行批次                  │
│    · 示例: [read, grep, bash, read, write]                    │
│      → [read∥grep], [bash], [read], [write]                   │
│                                                               │
│  Phase 3c: 逐批次执行                                         │
│    · 并行批次 → FuturesUnordered 并发 (tokio::spawn each)     │
│    · 串行批次 → 逐个执行 (前一个完成后才启动下一个)           │
│                                                               │
│  中断处理(每工具内):                                         │
│    tokio::select! {                                          │
│      timeout(tool_timeout_secs) → 执行工具                    │
│      cancel_fut (每 250ms 轮询) → 返回错误                    │
│    }                                                         │
│    InterruptBehavior::Block → 忽略 cancel_fut等完成          │
│    InterruptBehavior::Cancel → 响应取消,注入错误             │
│                                                               │
│  辅助函数:                                                    │
│    execute_single_tool() — 单工具超时+取消+执行               │
│    process_single_result() — 结果处理+SSE推送+TTL持久化+Hook │
└──────────────────────────────────────────────────────────────┘
    │
    ▼  (每个工具完成后逐个处理)
    │
    ├─ 发送 ToolResult SSE 事件 (tool_call_id 精确匹配)
    │
    ├─ maybe_persist_tool_result()
    │   ├─ content ≤ max_chars → 直接传递
    │   └─ content > max_chars → 写入 disk + 返回 <persisted-output> stub
    │       幂等写入 (create_new),同 tool_call_id 不重复写
    │
    ├─ PostToolUse hooks → 审计日志 / 指标采集 / 输出修改
    │
    ├─ 持久化到 agent_messages 表 (fire-and-forget)
    │
    └─ 推入 messages[] → 下一轮 LLM 调用

并发模型细节

// executor.rs Phase 3c: 逐批次执行
for batch in &batches {
    if batch.is_parallel {
        // 并行批次: FuturesUnordered 内并发执行
        let mut exec_futs: FuturesUnordered<_> = batch.calls.iter()
            .map(|prep| execute_single_tool(...))
            .collect();
        while let Some(result) = exec_futs.next().await {
            process_single_result(...).await;
        }
    } else {
        // 串行批次: 逐个执行(非并发安全工具独占)
        for prep in &batch.calls {
            let result = execute_single_tool(...).await;
            process_single_result(...).await;
        }
    }
}

关键特性:

  • ToolPartitioner::partition() 将工具调用分为并行批次和串行批次
  • 并行批次内使用 FuturesUnordered 最大化并发
  • 串行批次内逐个执行(如 run_bashfile_write 独占)
  • 连续的并发安全工具自动合并到一个并行批次
  • is_concurrency_safe(args)输入感知的:同一工具可能因参数不同而安全属性不同
  • InterruptBehavior::Block 保护写入操作不被用户取消打断

ToolRegistry 注册流程

sequenceDiagram
    participant RT as AgentRuntime::new()
    participant TR as ToolRegistry
    participant Tools as 工具实例

    RT->>TR: new_with_queue(queue, skill_registry)
    TR->>TR: add_base_tools()
    Note over TR: 注册 19 个基础工具(不含 search_history该工具在 Runtime 中按需注入)
    TR->>Tools: read_file, grep_files, glob_files, run_bash
    TR->>Tools: file_write, file_edit
    TR->>Tools: search_papers, get_paper_metadata
    TR->>Tools: download_paper, parse_paper, get_paper_content
    TR->>Tools: rag_search, query_target, save_note
    TR->>Tools: todo_write, compress_context, ask_user
    TR->>Tools: load_skill(skill_registry)
    TR->>Tools: subagent (默认实例)

    opt queue.is_some()
        TR->>TR: add_background_tools(queue)
        TR->>Tools: bg_task_run + bg_task_check
    end

    RT->>TR: add_tool(SaveMemoryTool)
    Note over TR: MemoryManager 需要共享状态,动态注入

    RT->>TR: replace_tool(SubAgentTool::new_with_hooks(...))
    Note over TR: 替换为带 PermissionChecker + HookRegistry 的增强版

    TR->>TR: definitions()
    Note over TR: HashMap 值收集 → 按 name 字母序排序
    Note over TR: 排序保证跨调用稳定性 → 提升 prompt cache 命中率

    RT->>TR: resident_definitions()
    Note over TR: 返回常驻工具defer_loading=false→ 注入初始 prompt

    RT->>TR: deferred_definitions()
    Note over TR: 返回延迟工具defer_loading=true→ 供 LLM 按需加载

    RT->>TR: tool_catalog()
    Note over TR: 所有工具的 name + classifier_summary → 供 LLM 判断延迟工具需求

注册表工厂方法

方法 工具数 用途
ToolRegistry::empty() 0 受限场景(如记忆提取子代理只需只读 + save_memory
ToolRegistry::new(skill_registry) 19 标准科研 Agent
ToolRegistry::new_with_queue(queue, skill_registry) 21 带后台任务支持
ToolRegistry::new_with_team(queue, team_manager, skill_registry) 25 带团队协作

安全模型

多层权限管道

工具调用
    │
    ├─ 1. AgentTool::check_permissions()   ← 工具自身声明的权限规则
    │
    ├─ 2. PermissionChecker::check()        ← 集中式规则链
    │      ├─ Deny rule (最高优先级,不可覆盖)
    │      ├─ Allow rule (显式允许)
    │      ├─ Ask rule (需用户确认)
    │      └─ Default: Allowed
    │      └─ 支持通配符 "*" 匹配所有工具
    │
    └─ 3. PreToolUse hooks                  ← Hook 可最终拦截 (Block)

文件系统安全

// 路径穿越检测
fn has_path_traversal(path_str: &str) -> bool {
    path_str.contains("..") || path_str.contains('~')
}

// 路径沙箱
fn is_path_allowed(path: &Path, ctx: &ToolContext) -> bool {
    let canonical = path.canonicalize()?;  // 解析所有符号链接
    // 检查是否在 library_dir / skills_dir / current_dir 下
    allowed_roots.iter().any(|root| canonical.starts_with(root))
}

SQL 注入防护

所有数据库操作使用参数化查询,无字符串拼接:

sqlx::query("INSERT INTO agent_messages (...) VALUES (?, ?, ...)")
    .bind(value1)
    .bind(value2)
    // ...

关键集成点

Hook 系统

Hook 触发时机 在工具系统中的用途
PreToolUse 工具执行前 拦截/修改参数(MutateInput)、注入附加上下文(AppendContext)、权限确认(PermissionRequired
PostToolUse 工具执行后 审计日志写入 agent_audit_log、指标采集(MetricsData)、输出修改(MutateOutput

SSE 事件流

ToolCall { id, name, arguments, step }    ← 工具开始执行
  ↓
(如有进度) ToolProgress { id, progress }  ← execute_with_progress 发送
  ↓
ToolResult { tool_call_id, name, output,  ← 执行完成
             is_error, metadata, step }

前端通过 tool_call_id 精确匹配 ToolCall/ToolResult实现 Timeline 渲染。

上下文压缩

三层压缩与工具系统的交互:

层级 触发方式 实现
micro_compact 自动token 超限前) 占位符替换,不涉及工具
auto_compact 自动(estimated_tokens > soft_limit Runtime 检测 → snapshot_compress_restore() → 文件缓存快照 → LLM 摘要压缩 → 恢复最近文件
manual_compact compress_context 工具 设置 pending_manual_compress 标志位,下一轮 LLM 调用前执行。不受熔断器限制

压缩熔断器 (CompactionCircuitBreaker):连续失败多次后打开,阻止进一步压缩以防止无限循环。

工具输出持久化

execute() → ToolOutput { content }
    │
    ├─ content.len() ≤ max_output_chars (默认 4000)
    │   └─ 直接返回给 LLM
    │
    └─ content.len() > max_output_chars
        └─ maybe_persist_tool_result()
            ├─ 写入 {library_dir}/tool-results/{tool_call_id}.txt
            ├─ 使用 create_new 保证幂等
            └─ 返回 <persisted-output> stub含 path + preview
                LLM 可通过 read_file 读取完整内容

工具加载分类P3 特性)

classifier_summaryis_readonlydefer_loading 三个 trait 方法共同支持两维度工具分类:

只读 vs 写入分类 (is_readonly)

只读工具无副作用(不修改文件系统、不触发下载、不写入数据库),可在 Auto 模式下跳过权限确认直接执行。

分类 is_readonly 工具
只读 (9) true read_filegrep_filesglob_filessearch_papersget_paper_metadataget_paper_contentrag_searchquery_targetcheck_team_inbox
写入/副作用 false(默认) 其余所有工具(包括 run_bashfile_writedownload_papersave_note 等)

常驻 vs 延迟加载 (defer_loading)

常驻工具定义注入初始系统 prompt延迟工具仅在 LLM 需要时通过 load_tool 动态加载,减少 prompt 体积。

分类 defer_loading 工具
常驻 (18) false(默认) 核心文件 I/O、论文搜索/检索、用户交互等高频工具
延迟 (7) true parse_paperdownload_paperbg_task_runbg_task_checkspawn_teammatesend_teammate_messageteam_broadcastsearch_history

工作流

初始化:
  resident_definitions() → 注入 system prompt常驻工具 JSON Schema
  tool_catalog()         → name + classifier_summary 清单 → 供 LLM 决策

LLM 运行时:
  分析 task → 查看 catalog → 判断是否需要延迟工具
  → 调用 defer_load_specific(name) → 注入单工具 definition
  → 正常执行工具调用

目录结构

src/agent/tools/
├── mod.rs              # AgentTool trait + ToolRegistry + ToolContext + ToolOutput + 辅助函数
├── filesystem/         # 文件 I/O (6 工具)
│   ├── mod.rs          # re-export
│   ├── read.rs         # read_file    — 带 FileStateCache mtime 去重
│   ├── grep.rs         # grep_files   — 正则搜索
│   ├── glob.rs         # glob_files   — 通配符匹配
│   ├── bash.rs         # run_bash     — Shell 命令执行(超时+截断)
│   ├── write.rs        # file_write   — 文件创建/覆盖
│   ├── edit.rs         # file_edit    — 精确字符串替换
│   └── security.rs     # 共享安全验证(路径沙箱 + 穿越检测)
├── astro/              # 天文学工具 (8 工具)
│   ├── mod.rs          # re-export
│   ├── search.rs       # search_papers + get_paper_metadata
│   ├── paper.rs        # download_paper + parse_paper + get_paper_content
│   ├── rag.rs          # rag_search — 向量检索 + LLM 问答
│   ├── target.rs       # query_target — CDS Sesame 查询
│   └── note.rs         # save_note — 高亮批注
├── todo.rs             # todo_write — 任务规划DAG 依赖,持久化到 agent_tasks
├── compress.rs         # compress_context — 手动上下文压缩标志位
├── skill.rs            # load_skill — inline/fork 双模式技能加载
├── subagent.rs         # subagent — 上下文隔离的子代理委派
├── ask_user.rs         # ask_user — oneshot 通道用户交互
├── background.rs       # bg_task_run + bg_task_check — 后台异步任务
├── team.rs             # spawn/send/broadcast/check_inbox — 多 Agent 协作
├── memory.rs           # save_memory — 带质量门控的跨会话记忆
├── search_history.rs   # search_history — FTS5 跨会话历史搜索(延迟加载)
└── persist.rs          # maybe_persist_tool_result — 大输出磁盘持久化