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

494 lines
22 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.

# 工具系统 (Tool System)
Agent 工具系统是 ReAct 循环中 **Action → Observation** 环节的执行引擎。每个工具遵循 `AgentTool` trait 向 LLM 暴露 JSON Schema 参数定义,并在 `execute` 中调用服务层完成实际业务操作。
---
## 三层架构
```mermaid
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
```mermaid
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_file`、`search_papers`、`rag_search` 等)覆写为 `true`;写入工具保持默认 `false` |
| `check_permissions()` | `Vec<PermissionRule>` | `[]` | 工具自定义权限规则:`Deny{tool, reason}` / `Allow{tool}` / `Ask{tool, message}`。与 `PermissionChecker` 管道协同工作 |
| `causes_sibling_abort()` | `bool` | `false` | 该工具错误时是否中止兄弟并行执行。用于 `download_paper`、`parse_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_dir`、`skills_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_paper`、`parse_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 调用
```
### 并发模型细节
```rust
// 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_bash`、`file_write` 独占)
- 连续的并发安全工具自动合并到一个并行批次
- `is_concurrency_safe(args)` 是**输入感知**的:同一工具可能因参数不同而安全属性不同
- `InterruptBehavior::Block` 保护写入操作不被用户取消打断
---
## ToolRegistry 注册流程
```mermaid
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)
```
### 文件系统安全
```rust
// 路径穿越检测
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 注入防护
所有数据库操作使用参数化查询,无字符串拼接:
```rust
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_summary`、`is_readonly`、`defer_loading` 三个 trait 方法共同支持两维度工具分类:
### 只读 vs 写入分类 (`is_readonly`)
只读工具无副作用(不修改文件系统、不触发下载、不写入数据库),可在 Auto 模式下跳过权限确认直接执行。
| 分类 | `is_readonly` | 工具 |
|:---|:---|:---|
| 只读 (9) | `true` | `read_file`、`grep_files`、`glob_files`、`search_papers`、`get_paper_metadata`、`get_paper_content`、`rag_search`、`query_target`、`check_team_inbox` |
| 写入/副作用 | `false`(默认) | 其余所有工具(包括 `run_bash`、`file_write`、`download_paper`、`save_note` 等) |
### 常驻 vs 延迟加载 (`defer_loading`)
常驻工具定义注入初始系统 prompt延迟工具仅在 LLM 需要时通过 `load_tool` 动态加载,减少 prompt 体积。
| 分类 | `defer_loading` | 工具 |
|:---|:---|:---|
| 常驻 (18) | `false`(默认) | 核心文件 I/O、论文搜索/检索、用户交互等高频工具 |
| 延迟 (7) | `true` | `parse_paper`、`download_paper`、`bg_task_run`、`bg_task_check`、`spawn_teammate`、`send_teammate_message`、`team_broadcast`、`search_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 — 大输出磁盘持久化
```