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

18 KiB
Raw 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"]
    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
    }

    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 发送进度更新到前端

工具清单23 个)

文件系统工具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 自管理工具5 个)

工具 并发安全 中断行为 功能
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 模式)

高级编排工具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() ─────────────────────────────────────────┐
│  FuturesUnordered 并发调度:                                    │
│   每个 PreparedCall → tokio::spawn(async {                   │
│     tokio::select! {                                         │
│       timeout(tool_timeout_secs) → 执行工具                   │
│       cancel_fut (每 250ms 轮询) → 返回错误                   │
│     }                                                        │
│   })                                                         │
│                                                               │
│  中断处理:                                                    │
│    InterruptBehavior::Block → 忽略 cancel_fut等完成          │
│    InterruptBehavior::Cancel → 响应取消,注入错误             │
│                                                               │
│  渐进式结果处理 (while exec_futs.next()):                     │
│    完成即处理,快工具不因慢工具阻塞                             │
└──────────────────────────────────────────────────────────────┘
    │
    ▼  (每个工具完成后逐个处理)
    │
    ├─ 发送 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: FuturesUnordered 中的每个 future
Box::pin(async move {
    let interrupt_behavior = tool.interrupt_behavior();
    let is_blocking = interrupt_behavior == InterruptBehavior::Block;

    tokio::select! {
        res = tokio::time::timeout(timeout_dur, tool_fut) => {
            // 正常完成或超时
        }
        _ = cancel_fut => {
            // 仅当 !is_blocking 时此分支可达
            // Block 工具的 cancel_fut loop 不 break
        }
    }
})

关键特性:

  • 所有工具放入同一个 FuturesUnordered,不区分串行/并行批次
  • is_concurrency_safe 声明为语义标记(引导 LLM 并发调用),执行时全部并发
  • 实际串行化依赖工具内部的 mutex/文件锁
  • 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 个基础工具
    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 命中率

注册表工厂方法

方法 工具数 用途
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 读取完整内容

目录结构

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 — 带质量门控的跨会话记忆
└── persist.rs          # maybe_persist_tool_result — 大输出磁盘持久化