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

426 lines
18 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"]
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
}
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` 发送进度更新到前端 |
---
## 工具清单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_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 自管理工具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_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() ─────────────────────────────────────────┐
│ 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 调用
```
### 并发模型细节
```rust
// 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 注册流程
```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 个基础工具
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)
```
### 文件系统安全
```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 读取完整内容
```
---
## 目录结构
```
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 — 大输出磁盘持久化
```