# Phase 2: Tool System — 方案设计 > 定稿日期:2026-06-03 ## 背景与目标 AG Core Phase 0(Foundation)已完成 LLM 调用周期基础设施,Phase 1(Prompt Engineering)已完成提示词组合与模板化。Phase 2 的目标是补齐**工具系统**能力,实现 LLM 驱动的工具定义、注册、调用、权限控制,以及 MCP 协议集成。 **核心目标**:让 LLM 能通过 `FinishReason::ToolCalls` 触发工具自动执行,并将结果回传至对话上下文,形成完整的"思考 → 调用 → 反馈"闭环。 --- ## 需求分析 ### 功能需求 | 模块 | 需求 | 验收条件 | |------|------|---------| | `BaseTool` trait | 工具抽象接口:名称、描述、参数、执行、权限声明 | 实现 trait 后可注册到 Registry | | `ToolRegistry` | 工具注册、发现、按名称调用 | 注册 3 个工具后能按名称查找到并执行 | | `McpClient` | MCP 协议客户端(stdio transport) | 能启动 MCP 服务器子进程、列出工具、调用工具 | | `PermissionChecker` | 工具执行前权限校验 | 禁止无权限的工具执行,返回结构化错误 | | 自动 Tool 循环 | LlmCycle 收到 ToolCalls 后自动执行工具并回传 | 一个包含工具调用的对话能完整执行 2+ 轮 | | 流式 Tool 事件 | 流式模式下发射 `ToolExecutionCompleted` 事件 | 流式调用中工具执行完成后触发对应事件 | | 工具调用历史持久化 | 自动工具循环产生的 Tool/Assistant 消息正确追加到 `messages` | 查看 `cycle.messages()` 能获取完整工具交互轨迹 | ### 非功能需求 - 所有公开 API 必须带 `///` 文档注释 - 无新增 `unwrap()` 调用 - `BaseTool` 的 `execute()` 必须为 `async` - 工具执行错误必须结构化为 `ToolError`,不允许 panic - MCP 客户端超时默认 30 秒,可配置 - 自定义工具与 MCP 工具通过同一 `ToolRegistry` 管理,对 LlmCycle 透明 - 权限检查在工具执行之前,阻断后返回错误而非静默跳过 - `BaseTool::execute()` 签名必须预留扩展点(`ToolContext` 注入),确保未来 Skill/Agent 层可在不修改 trait 签名的情况下注入 session_id、cancellation_token 等上下文信息 - 自动 tool 循环应考虑 token 消耗——工具定义随每轮请求重复发送,工具结果直接追加到对话历史,需提供结果大小限制和截断策略 --- ## 方案设计 ### 模块结构 ``` src/ tools.rs # tools 模块根:声明子模块 + 重导出公共 API tools/ base.rs # BaseTool trait — 工具抽象接口 registry.rs # ToolRegistry — 工具注册表 permission.rs # PermissionChecker — 权限校验器 mcp.rs # McpClient — MCP 协议客户端 error.rs # ToolError — 工具系统错误类型 ``` `tools.rs` 根模块声明: ```rust // tools.rs pub mod base; pub mod error; pub mod mcp; pub mod permission; pub mod registry; pub use base::{BaseTool, ToolContext}; pub use error::ToolError; pub use mcp::McpClient; pub use permission::{Permission, PermissionChecker, PermissionConfig}; pub use registry::{ToolEntry, ToolInvocation, ToolRegistry}; ``` `lib.rs` 添加: ```diff pub mod llm; pub mod prompt; +pub mod tools; ``` ### 1. BaseTool trait — 工具抽象接口 ```rust // tools/base.rs use async_trait::async_trait; use serde_json::Value; use crate::tools::error::ToolError; use crate::tools::permission::Permission; /// 工具抽象接口 —— 所有工具(自定义或 MCP)最终都实现此 trait。 #[async_trait] pub trait BaseTool: Send + Sync { /// 工具名称(唯一标识,用于 LLM 的 tool_calls.name 匹配)。 fn name(&self) -> &str; /// 工具描述(LLM 据此决定是否调用此工具)。 fn description(&self) -> &str; /// 工具参数定义(JSON Schema 格式,传递给 LLM 的 tool.parameters)。 fn parameters(&self) -> Value; /// 声明工具所需的权限列表。 fn required_permissions(&self) -> Vec { Vec::new() } /// 执行工具调用。 async fn execute(&self, args: Value) -> Result; } ``` **设计说明**: - `name()` 返回 `&str` 而非 `String`,避免每次调用克隆 - `parameters()` 返回 `serde_json::Value`,与现有 `OpenaiToolDefinition.parameters` 类型一致 - `required_permissions()` 提供默认空实现,简化无敏感操作的工具定义 - `execute()` 接收 `Value`(JSON 对象)作为参数,返回 `Value` 作为结果,与 OpenAI API 的 arguments/output 格式一致 ### 2. ToolRegistry — 工具注册表 ```rust // tools/registry.rs use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; use serde_json::Value; use crate::llm::types::{OpenaiToolDefinition, ToolDefinition}; use crate::tools::base::BaseTool; use crate::tools::error::ToolError; use crate::tools::permission::{Permission, PermissionChecker}; /// 工具调用记录 —— 用于追踪和调试。 pub struct ToolInvocation { pub tool_name: String, pub input: Value, pub output: Result, } /// 工具注册表 —— 管理工具注册、发现、调用。 pub struct ToolRegistry { tools: HashMap>, permission_checker: Option>, } impl ToolRegistry { pub fn new() -> Self; /// 设置权限检查器(可选,不设置则不检查权限)。 pub fn with_permission_checker(mut self, checker: PermissionChecker) -> Self; /// 注册一个工具。 pub fn register(&mut self, tool: Arc) -> Result<(), ToolError>; /// 批量注册工具。 pub fn register_all(&mut self, tools: Vec>) -> Result<(), ToolError>; /// 注销一个工具。 pub fn unregister(&mut self, name: &str) -> Option>; /// 按名称查找工具。 pub fn get(&self, name: &str) -> Option>; /// 获取所有已注册工具的名称列表。 pub fn list_tools(&self) -> Vec; /// 获取所有工具的 ToolDefinition 列表(用于传递给 LLM)。 pub fn definitions(&self) -> Vec; /// 调用一个工具(含权限检查)。 pub async fn invoke(&self, name: &str, args: Value) -> Result; /// 批量执行工具调用(并行执行互不依赖的工具)。 pub async fn invoke_all( &self, calls: Vec<(String, Value)>, ) -> Vec; } ``` **核心逻辑**: - `invoke()`:查找工具 → 权限检查 → 执行 → 返回 `ToolInvocation` - `invoke_all()`:对多个工具调用并行执行(使用 `tokio::join!` 或 `futures::join_all`),适用于 LLM 同时发出多个 tool_calls 的场景 - `invoke_all()` 应对每个工具执行添加超时控制(通过 `tokio::time::timeout`),超时时间由 `CycleConfig::tool_timeout_secs` 配置,默认 60 秒,防止单个工具长时间阻塞整个循环 - `definitions()`:将注册的工具批量转换为 `Vec`,供 LlmCycle 传递 LLM - `ToolRegistry` 不持有 `PermissionChecker` 的生命周期(使用 `Arc`),允许多个 Registry 共享同一个 Checker **使用示例**: ```rust let mut registry = ToolRegistry::new() .with_permission_checker(checker); registry.register(Arc::new(WeatherTool))?; registry.register(Arc::new(FileReadTool))?; // 获取 ToolDefinitions 传递给 LLM let tools = registry.definitions(); // 收到 LLM 的 tool_calls 后执行 let calls = vec![ ("get_weather".into(), json!({"city": "Beijing"})), ("read_file".into(), json!({"path": "/tmp/data.txt"})), ]; let results = registry.invoke_all(calls).await; ``` ### 3. PermissionChecker — 权限校验器 ```rust // tools/permission.rs /// 权限级别枚举。 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Permission { /// 只读(读取文件、查询数据库等)。 Read, /// 写入(创建/修改文件、插入数据等)。 Write, /// 删除(删除文件、记录等)。 Delete, /// 网络访问(HTTP 请求等)。 Network, /// Shell 命令执行。 Shell, /// 文件系统操作(除读/写/删之外的 FS 操作)。 FileSystem, /// 自定义权限(可通过 namespaced 字符串扩展)。 Custom(String), } /// 权限配置。 pub struct PermissionConfig { /// 允许的权限列表(空 = 全部允许)。 pub allowed: Vec, /// 拒绝的权限列表(优先级高于 allowed)。 pub denied: Vec, /// 是否允许未声明权限的工具执行(默认为 true)。 pub allow_unspecified: bool, } impl Default for PermissionConfig { fn default() -> Self { Self { allowed: vec![Permission::Read, Permission::Network], denied: vec![Permission::Delete, Permission::Shell], allow_unspecified: true, } } } /// 权限检查器。 pub struct PermissionChecker { config: PermissionConfig, } impl PermissionChecker { pub fn new(config: PermissionConfig) -> Self; /// 检查指定权限是否允许执行。 pub fn check(&self, tool_name: &str, permissions: &[Permission]) -> Result<(), ToolError>; } ``` **权限判定规则**: 1. 如果权限在 `denied` 中 → 拒绝 2. 如果权限在 `allowed` 中 → 允许 3. 如果 `allowed` 非空且权限不在其中 → 拒绝(白名单模式) 4. 如果 `allowed` 为空 → 按 `allow_unspecified` 判定 ### 4. McpClient — MCP 协议客户端 MCP(Model Context Protocol)是一种基于 JSON-RPC 的协议,用于 LLM 与外部工具系统通信。Phase 2 实现其 **最小可行子集**,优先实现 stdio transport。 > **传输方式说明**:MCP 协议版本 2025-03-26 定义了两种标准传输——`stdio` 和 `Streamable HTTP`。原有的 `HTTP+SSE` 传输(2024-11-05)已被官方废弃,新实现不应采用。`Streamable HTTP` 通过单一 HTTP 端点同时支持 JSON 响应和 SSE 流式升级,是 HTTP 场景的推荐方案。 ```rust // tools/mcp.rs use std::process::{Child, Command, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; use serde_json::Value; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::{ChildStdin, ChildStdout, Command as TokioCommand}; use tokio::sync::Mutex; use crate::tools::base::BaseTool; use crate::tools::error::ToolError; /// MCP 协议版本。 const MCP_VERSION: &str = "2025-03-26"; /// MCP 传输方式。 pub enum McpTransport { /// 通过子进程 stdin/stdout 通信。 Stdio { command: String, args: Vec, }, /// Streamable HTTP 传输(MCP 2025-03-26 引入,替代已废弃的 HTTP+SSE)。 /// 客户端通过单一 HTTP 端点与 MCP Server 通信,支持 JSON 和 SSE 流式响应。 StreamableHttp { url: String, headers: Option>, }, } /// MCP 客户端 —— 与 MCP 服务器通信。 pub struct McpClient { transport: McpTransport, server_name: String, /// 已初始化的工具列表(缓存)。 tools: Vec, /// 是否已初始化。 initialized: AtomicBool, /// 超时时间(秒)。 timeout_secs: u64, } /// MCP 服务器暴露的工具(缓存结构)。 struct McpTool { name: String, description: Option, input_schema: Value, } impl McpClient { /// 创建一个 MCP 客户端。 pub fn new(server_name: impl Into, transport: McpTransport) -> Self; /// 设置超时时间。 pub fn with_timeout(mut self, secs: u64) -> Self; /// 连接并初始化(发送 initialize 请求,获取服务器能力声明)。 pub async fn connect(&mut self) -> Result<(), ToolError>; /// 列出服务器支持的工具(调用 tools/list)。 pub async fn list_tools(&mut self) -> Result, ToolError>; /// 调用一个工具(调用 tools/call)。 pub async fn call_tool(&self, name: &str, args: Value) -> Result; /// 关闭连接(终止子进程)。 pub async fn close(&mut self) -> Result<(), ToolError>; /// 将 MCP 客户端转换为 BaseTool 适配器列表(用于注册到 ToolRegistry)。 pub fn into_tools(self) -> Vec>; } ``` **MCP 协议交互流程**: ``` 客户端 MCP 服务器 │ │ ├── initialize request ──────► │ │ { protocolVersion, capabilities } │◄──── initialize response ── │ │ { protocolVersion, serverInfo, capabilities } │ │ ├── initialized notification ──► │ │ │ ├── tools/list ──────────────► │ │◄── tools/list result ──────── │ │ { tools: [{ name, description, inputSchema }] } │ │ ├── tools/call ─────────────► │ │ { name, arguments } │◄── tools/call result ──────── │ │ { content: [{ type, text }] } │ │ ├── shutdown ───────────────► │ │◄── shutdown ───────────── │ ``` **关于 stdio transport 实现**: - 使用 `tokio::process::Command` 启动子进程 - stdin 写入 JSON-RPC 请求(每行一个 JSON 对象) - stdout 读取 JSON-RPC 响应(使用 `BufReader` 逐行读取) - 每个请求关联一个 `id`(递增整数),通过 `id` 匹配请求和响应 - 进程退出时自动关闭 **关于 MCP Server 的管理**: - Phase 2 **不** 实现 MCP Server 框架,只实现 Client - MCP Server 由外部提供(如 `npx @anthropic/mcp-server-filesystem`) - 用户需要提供 MCP Server 的启动命令和参数 **工具缓存说明**: - `McpClient` 在 `list_tools()` 时缓存工具列表,避免每次调用都重新请求 - 缓存假设:MCP Server 的工具列表在运行时不会频繁变更(如插件式加载场景除外) - 如需刷新,可通过新增 `refresh_tools()` 方法或基于 TTL(如 60 秒)自动失效 ### 5. ToolError — 错误类型 ```rust // tools/error.rs use thiserror::Error; #[derive(Error, Debug)] pub enum ToolError { #[error("工具 '{0}' 未注册")] NotFound(String), #[error("工具 '{0}' 执行失败: {1}")] ExecutionFailed(String, String), #[error("工具 '{0}' 参数无效: {1}")] InvalidArguments(String, String), #[error("权限被拒绝: 工具 '{0}' 需要 {1:?} 权限")] PermissionDenied(String, String), #[error("MCP 协议错误: {0}")] McpError(String), #[error("MCP 未初始化: {0}")] McpNotInitialized(String), #[error("MCP 超时: {0}")] McpTimeout(String), #[error("IO 错误: {0}")] Io(#[from] std::io::Error), #[error("其他错误: {0}")] Other(String), } ``` ### 6. LlmCycle 扩展 — 自动 Tool 循环 **核心设计**:在 `LlmCycle` 中新增 `submit_with_tools()` 方法,自动处理 tool 执行循环。 ```rust // llm/cycle.rs 新增 use crate::tools::registry::ToolRegistry; use crate::tools::error::ToolError; impl LlmCycle { /// 提交消息并自动处理工具调用循环。 /// /// 流程: /// 1. 发送请求(含工具定义) /// 2. 检查响应中的 finish_reason /// 3. 如果是 ToolCalls → 执行工具 → 回传结果 → 重复 1 /// 4. 如果是 Stop/Length → 返回最终响应 pub async fn submit_with_tools( &mut self, prompt: String, registry: &ToolRegistry, ) -> Result { let tools = registry.definitions(); let max_turns = self.config.max_turns.unwrap_or(10); // 注:CycleConfig.max_turns 默认值为 None,实现时需修改 Default 为 Some(10) let mut turn = 0; self.messages.push(OpenaiChatMessage::user_text(prompt)); loop { turn += 1; if turn > max_turns { return Err(LlmError::Other(format!( "达到最大工具循环轮次 ({})", max_turns ))); } // 发送请求 let response = self.submit_request(&tools).await?; // 检查是否需要执行工具 let should_execute = matches!( response.stop_reason, Some(FinishReason::ToolCalls) | None ) && has_tool_calls(&response.message); if !should_execute { return Ok(response); } // 解析 tool_calls 并执行 let tool_calls = extract_tool_calls(&response.message); let results = registry.invoke_all(tool_calls).await; // 回传工具结果 for result in results { let content = match &result.output { Ok(value) => serde_json::to_string(value) .unwrap_or_else(|_| "{}".to_string()), Err(e) => format!("错误: {}", e), }; self.messages.push( OpenaiChatMessage::tool_result(result.tool_name.clone(), content) ); } } } /// 内部请求方法(与 submit 共享重试逻辑,但不 push user message)。 async fn submit_request( &mut self, tools: &[ToolDefinition], ) -> Result { // ... 提取 submit() 中的 request → response 逻辑(不含 user prompt push) } } ``` **关键设计决策**: | 决策 | 选择 | 理由 | |------|------|------| | 循环方式 | 同步循环(单线程串行) | 工具执行依赖前一轮结果,串行更安全 | | 最大轮次 | `CycleConfig.max_turns`,默认 `Some(10)` | 防止无限循环(LLM 反复调用工具)。**注意**:当前 `CycleConfig` 默认值为 `None`,实现时需将 `Default` 改为 `Some(10)` | | 工具并行 | `invoke_all()` 互不依赖的工具并行 | LLM 可能一次发出多个 tool_calls(parallel_tool_calls) | | 工具超时 | `CycleConfig::tool_timeout_secs`,默认 60 | 防止单个工具长时间阻塞循环。`invoke_all()` 使用 `tokio::time::timeout` 包装 | | 错误处理 | 工具执行错误以文本回传 LLM,而非终止循环 | LLM 可自行从错误中恢复 | | 消息追踪 | 所有工具交互通过 `self.messages` 持久化 | 调用方能通过 `cycle.messages()` 查看完整轨迹 | **Token 消耗分析**: 自动 tool 循环的 token 消耗主要来自三个来源: | 来源 | 说明 | 影响程度 | |------|------|---------| | 工具定义重复发送 | `definitions()` 在每轮请求中携带全部工具的 JSON Schema | 注册工具数 × 平均定义大小 × 轮数。20 个工具 × 500B × 5 轮 ≈ 50KB 输入 token | | 工具结果追加历史 | 每次工具执行结果完整追加到 `messages`,后续请求重发全部历史 | 最显著的 token 泄漏源。大结果(如向量搜索 Top-50)单次可能 ~15KB,多轮累加 | | Value→String 序列化 | 工具结果 `serde_json::to_string()` 后 JSON 字符串膨胀 ~20-30% | 线性的常量损耗 | **影响估算**: | 场景 | 工具相关 token 占比 | 说明 | |------|-------------------|------| | 单次简单查询 | <5% | 可忽略 | | 文件读取+分析(3-4 轮) | ~30% | 工具结果逐步累积 | | 网页搜索+总结(3-5 轮) | ~40% | 工具结果包含页面内容 | | 多工具数据 pipeline(5-10 轮) | ~60%+ | 需关注压缩和限制策略 | **缓解方向**(Phase 2 不强制实现,但设计需可扩展): - **结果大小限制**:工具执行结果超过阈值时自动截断(如 `CycleConfig::max_tool_result_bytes`) - **自动压缩**:现有的 Auto-compaction 需感知工具消息,避免压缩掉 LLM 后续依赖的数据 - **工具定义缓存**:基础工具定义变化极少,未来可考虑客户端侧缓存(需等 provider 支持) **错误分类与处理策略**: 工具执行错误需要区分"可恢复"和"不可恢复"两类,不可恢复的错误应终止循环而非回传 LLM: | 错误类型 | 处理策略 | 理由 | |---------|---------|------| | `ToolError::ExecutionFailed` | 回传 LLM(文本) | LLM 可能下次换参数或换方式重试 | | `ToolError::InvalidArguments` | 回传 LLM(文本) | LLM 可自动修正参数 | | `ToolError::NotFound` | 终止循环,返回 `LlmError` | LLM 无法注册工具,重试无意义 | | `ToolError::PermissionDenied` | 终止循环,返回 `LlmError` | 安全敏感,不应允许重试 | | `ToolError::McpError` | 终止循环,返回 `LlmError` | MCP 链路故障,重试大概率失败 | | `ToolError::McpTimeout` | 终止循环,返回 `LlmError` | 或可考虑重试 1 次后终止 | | `ToolError::Io` | 终止循环,返回 `LlmError` | IO 错误通常是环境问题 | | `ToolError::Other` | 回传 LLM(文本) | 兜底,保守回传 | 实现上可在 `ToolError` 上添加 `is_recoverable()` 方法,或在 `submit_with_tools()` 中通过 `match` 分支判断。 **submit_request() 重构说明**: 提取 `submit_request()` 作为 `submit_with_tools()` 的内部方法时,需确保不影响现有方法的行为。重构后的方法职责矩阵: | 方法 | Push user msg | Compaction | Retry | Call provider | Handle response | |------|:---:|:---:|:---:|:---:|:---:| | `submit()` | ✅ | ✅ | ✅ | → `submit_request()` | ✅ | | `submit_messages()` | ❌ | ✅ | ✅ | → `submit_request()` | ✅ | | `submit_with_tools()` | ✅ | ✅ | ✅ | → `submit_request()` | ✅* | | `submit_request()` | ❌ | ❌ | ✅ | ✅ | ✅ | *`submit_with_tools()` 在 `submit_request()` 返回后额外检查 `ToolCalls`,执行工具后递归调用自身。 **流式模式支持**: `submit_stream()` 的增强方案:新增 `submit_stream_with_tools()`,在流式事件层面支持自动 tool 循环。 ```rust impl LlmCycle { pub async fn submit_stream_with_tools( &mut self, prompt: String, registry: &ToolRegistry, ) -> Result + Send>>, LlmError> { // 1. 使用 submit_stream() 获取初始事件流 // 2. 监听 TurnComplete { reason: ToolCalls } // 3. 触发时:通过 ToolRegistry 执行工具 // 4. 发射 ToolExecutionCompleted 事件(由 submit_stream_with_tools 负责,非底层 stream parser) // 5. 将工具结果注入 messages // 6. 自动发起下一轮请求(递归) // 7. 直到 finish_reason 为 Stop } } ``` **事件发射时序**: ``` submit_stream_with_tools("查天气") │ ├─ AssistantTextDelta "我来查一下北京的天气..." ← 底层 stream parser 发射 ├─ ToolExecutionStarted { tool_name, input, id } ← submit_stream_with_tools 发射 ├─ TurnComplete { reason: ToolCalls } ← 底层 stream parser 发射 │ ├── [自动] 执行工具 get_weather({city:"北京"}) │ ├─ ToolExecutionCompleted { tool_name, output, ... } ← submit_stream_with_tools 发射 │ ├─ AssistantTextDelta "北京今天 22°C" ← 底层 stream parser 发射 ├─ TurnComplete { reason: Stop } ← 底层 stream parser 发射 │ └─ (流结束) **事件发射职责划分**:底层 `parse_chunk_stream()` 负责 LLM 原生事件(`AssistantTextDelta`、`TurnComplete`);`submit_stream_with_tools()` 负责工具层事件(`ToolExecutionStarted`、`ToolExecutionCompleted`),在工具执行前/后手动 `yield` 事件。 ``` ### 7. 自定义工具示例 ```rust use agcore::tools::{BaseTool, ToolError}; use async_trait::async_trait; use serde_json::Value; struct WeatherTool; #[async_trait] impl BaseTool for WeatherTool { fn name(&self) -> &str { "get_weather" } fn description(&self) -> &str { "获取指定城市的当前天气" } fn parameters(&self) -> Value { serde_json::json!({ "type": "object", "properties": { "city": { "type": "string", "description": "城市名称" } }, "required": ["city"] }) } async fn execute(&self, args: Value) -> Result { let city = args["city"].as_str() .ok_or_else(|| ToolError::InvalidArguments( "get_weather".into(), "缺少 city 参数".into() ))?; // 模拟天气查询 Ok(serde_json::json!({ "city": city, "temperature": 22, "unit": "°C" })) } } ``` ### 8. 模块依赖关系 ``` tools/ 模块内部依赖: base.rs → 无内部依赖(Permission 枚举 + ToolError) permission → 无内部依赖 registry → base, error, permission mcp → base, error(需通过 registry 注册) error → 无内部依赖 跨模块依赖: tools/ → llm/types (ToolDefinition 类型) llm/cycle → tools/registry (自动 tool 循环) llm/cycle → tools/error (ToolError 转换) ``` --- ### 9. 未来工具化路线扩展性分析 > 本节回答"当前设计是否足以支撑未来常规工具、MCP、Skill、记忆等统一走工具调用路线"。 #### 设计目标 未来所有 Agent 可调用的能力(常规工具、MCP 工具、Skill、记忆操作)都应通过 `BaseTool` trait 统一暴露给 LLM,`ToolRegistry` 作为唯一的工具发现和调用入口,对 `LlmCycle` 透明。 #### 各场景支持度评估 | 场景 | 当前支持度 | 关键瓶颈 | |------|-----------|---------| | 常规工具(天气/计算器) | ✅ 直接可行 | 无 | | MCP 工具(McpClient→BaseTool 适配器) | ✅ 可行 | 适配器模式优雅,MCP 流式/进度能力被 `Value→Value` 约束 | | Memory CRUD(store/recall/forget/update) | ⚠️ 基本可行 | 检索分页、大量结果返回需额外处理 | | 长时运行工具(数据集查询、文件上传) | ❌ 不可行 | 无进度汇报、无 cancellation 机制 | | 多轮确认工具("是否冻结账户?"审批流程) | ❌ 不可行 | 单次调用→单次返回,无法表达"反问→确认"模式 | | Skill 编排(多步骤组合、嵌套执行) | ❌ 不可行 | 无上下文传播(跨步骤传递中间结果)、无工具组合原语 | | Agent 按场景筛选工具子集 | ⚠️ 部分可行 | 无 tag/category 筛选机制 | #### 关键扩展点 **A. `BaseTool::execute()` 签名——预留 `ToolContext` 注入** `BaseTool` 是公开 trait,一旦用户实现并发布 crate,后续 breaking change 成本极高。当前签名: ```rust async fn execute(&self, args: Value) -> Result; ``` 未来扩展路径——新增 `ToolContext` 参数,携带执行上下文: ```rust async fn execute(&self, args: Value, ctx: &ToolContext<'_>) -> Result; ``` `ToolContext` 初始应包含的字段(Phase 2 实现时不必全部实现,但签名需预留参数位置): | 字段 | 用途 | 引入阶段 | |------|------|---------| | `session_id: &str` | 追踪一次对话中所有工具调用的关联性 | Phase 2 | | `trace_id: &str` | 链路追踪,跨工具调用的耗时分布 | Phase 2 | | `cancellation_token: CancellationToken` | 优雅取消正在执行的工具 | Phase 2 | | `progress: Option>` | 进度汇报(数据处理到 50%) | Phase 3 | | `shared_state: Option<&HashMap>` | Skill 跨步骤传递中间结果 | Phase 4 | 这样 Skill/Agent 层在 Phase 4 引入时,`execute` 签名不必改,只需在 `ToolContext` 中增加字段。 **B. `ToolRegistry` 内部结构——引入 `ToolEntry` 元数据** 当前内部是 `HashMap>`,未来扩展为: ```rust pub struct ToolEntry { pub tool: Arc, pub tags: Vec, pub category: String, // "memory", "data", "communication" 等 pub version: Option, pub stats: ToolStats, // 调用次数、平均耗时 } ``` 对应的筛选 API: ```rust pub fn find_by_tag(&self, tag: &str) -> Vec<&ToolEntry>; pub fn find_by_category(&self, category: &str) -> Vec<&ToolEntry>; pub fn groups(&self) -> HashMap<&str, Vec<&ToolEntry>>; ``` **C. 工具返回模式——从单一 `Value` 到 `ToolOutput` 枚举** 当前返回类型 `Result` 只能表达"一次性完整返回"。未来根据需要引入多模式输出: ```rust pub enum ToolOutput { /// 一次性返回完整结果 Final(Value), /// 通过 channel 逐步流式输出结果 Streamed { initial: Value, rx: Receiver }, /// 需要 LLM 进一步确认后再继续 AwaitingInput { context: Value, prompt: String }, } ``` | 返回模式 | 场景示例 | |---------|---------| | `Final(Value)` | 天气查询、文件读取 | | `Streamed { initial, rx }` | 向量搜索 Top-100 逐批返回 | | `AwaitingInput { context, prompt }` | "检测到可疑交易,是否冻结?" | #### 各能力的引入时序 ``` Phase 2(当前实现) ├─ BaseTool trait (Value→Value, 但签名预留 Context 参数位) ├─ ToolRegistry (HashMap + tag/category 筛选) ├─ PermissionChecker / McpClient / ToolError ├─ submit_with_tools() / submit_stream_with_tools() └─ ToolContext { session_id, trace_id, cancellation_token } Phase 3(Memory 工具化) ├─ MemoryStore trait(扩展 BaseTool) ├─ memory_store / memory_recall / memory_search 等作为工具注册 └─ ToolContext.progress 支持(分批返回检索结果) Phase 4(Agent + Skill + 编排) ├─ ToolContext.shared_state 支持(跨步骤传递中间结果) ├─ ToolOutput 枚举支持(如需要流式/确认模式) ├─ ToolChain / ToolSelector 工具组合原语 └─ Skill 机制(多步骤编排 + 内部状态) ``` #### 已识别但推迟的设计决策 | 决策 | 推迟原因 | 何时需要 | |------|---------|---------| | `ToolOutput` 枚举 | Phase 2 的所有场景(常规工具/MCP)用 `Value` 足够 | Phase 4 Agent 编排或长时工具 | | 工具 DAG 调度 | Agent 场景后才需要复杂编排 | Phase 4 | | Skill 机制 | 需要先有 Agent 使用工具的实践经验 | Phase 4 | | 工具调用审计持久化 | 可先通过 Hook 点实现简单日志 | Phase 4 | | 用户授权(运行时弹窗确认) | `PermissionChecker` 只做静态策略判定,不处理运行时交互。用户授权属于交互流程,应作为 `ToolOutput::AwaitingInput` 由上层 UI/Agent 层实现 | Phase 4 | --- ## 实现计划 ### Step 1: 创建方案文档 创建 `docs/5-tool-system.md`(即本文档)。 ### Step 2: ToolError - 创建 `src/tools/error.rs` - 定义 `ToolError` 枚举(NotFound / ExecutionFailed / InvalidArguments / PermissionDenied / McpError / McpTimeout / Io / Other) - 运行 `cargo check` 验证 ### Step 3: Permission - 创建 `src/tools/permission.rs` - 定义 `Permission` 枚举 + `PermissionConfig` + `PermissionChecker` - 编写权限判定逻辑(白名单/黑名单/未指定策略) - 编写 5+ 边界测试覆盖:白名单模式、黑名单模式、空列表、自定义权限冲突 - 运行 `cargo test` 验证 ### Step 4: BaseTool trait - 创建 `src/tools/base.rs` - 定义 `BaseTool` trait(name / description / parameters / required_permissions / execute) - 定义 `ToolContext` 结构体(session_id / trace_id / cancellation_token),注入 `execute()` 作为第二个参数 - 创建 `src/tools.rs` 模块根,声明子模块,重导出公共 API - `lib.rs` 添加 `pub mod tools;` - 编写 1 个 MockTool 测试工具并验证 trait 实现 - 运行 `cargo check` 验证 ### Step 5: ToolRegistry - 创建 `src/tools/registry.rs` - 定义 `ToolInvocation` 结构体 + `ToolEntry` 元数据包装(tool + tags + category + stats)+ `ToolRegistry` - 实现核心方法:register / get / list / definitions / invoke / invoke_all / find_by_tag / find_by_category - `invoke_all()` 使用 `futures::future::join_all` 并行执行互不依赖的工具 - `definitions()` 将 `HashMap` 中的工具转换为 `Vec` - 编写 8+ 测试覆盖:注册冲突、空注册表查找、单次调用、批量并行调用、工具执行失败 - 运行 `cargo test` 验证 ### Step 6: LlmCycle 扩展(自动 Tool 循环) - 新增 `cycle_submit.rs` 子模块(或直接在 `cycle.rs` 中扩增,取决于代码量) - 提取 `submit_request()` 内部方法(将 submit() 中的 request→response 逻辑独立) - 实现 `submit_with_tools()` 方法: - 循环:submit_request → 检查 finish_reason → 调用 registry.invoke_all → 回传结果 - `max_turns` 控制,达到上限返回错误 - 工具执行错误以文本回传(LLM 可恢复) - 实现 `submit_stream_with_tools()` 方法: - 组合流式事件流和自动 tool 循环 - 在 TurnComplete(ToolCalls) 后发射 ToolExecutionCompleted - 更新 `CycleConfig` 文档注释,新增 `tool_timeout_secs` 字段,默认值 60 - 将 `CycleConfig::max_turns` 默认值由 `None` 改为 `Some(10)` - 编写 3+ 集成测试:单轮 tool 调用、多轮 tool 调用、达到 max_turns 终止 - 运行 `cargo test` 验证 ### Step 7: McpClient(MCP 协议客户端) - 创建 `src/tools/mcp.rs` - 实现 JSON-RPC 消息结构(Request / Response / Error / Notification) - 实现 stdio transport: - `connect()`:启动子进程,发送 initialize 请求 - `list_tools()`:调用 tools/list,缓存结果 - `call_tool()`:调用 tools/call,解析响应 - `close()`:发送 shutdown 请求,终止子进程 - `StreamableHttp` transport 预留枚举变体,当前返回 "not implemented" 错误,不在 Phase 2 实现 - 实现 `into_tools()`:将 MCP 工具转换为 `Vec>` 适配器 - 设置 30 秒默认超时 - 编写 MCP 协议消息序列化/反序列化测试 + 模拟子进程集成测试 - 运行 `cargo test` 验证 ### Step 8: 收尾 - 更新 `docs/roadmap.md` 标记 Phase 2 完成 - `cargo clippy` — 无警告 - `cargo build` — 完整构建 - 检查所有新公开 API 有 `///` 文档注释 - `cargo test` — 所有测试通过 --- ## 术语表 | 术语 | 说明 | |------|------| | `BaseTool` | 工具抽象接口,所有工具需实现此 trait | | `ToolRegistry` | 工具注册表,管理工具注册、发现、调用 | | `ToolInvocation` | 工具调用记录,包含输入、输出和执行结果 | | `Permission` | 权限级别枚举(Read/Write/Delete/Network/Shell 等) | | `PermissionChecker` | 权限校验器,执行前判定是否允许 | | `McpClient` | MCP 协议客户端,通过 stdio 与 MCP Server 通信 | | `ToolDefinition` | 传递给 LLM 的工具定义(同 `OpenaiToolDefinition`) | | 自动 Tool 循环 | LlmCycle 自动执行 LLM 请求的工具调用并回传结果 | --- ## 风险评估 | 风险 | 概率 | 缓解措施 | |------|------|---------| | MCP 协议规范变化 | 中 | 只实现最小子集(initialize/list_tools/call_tool),封装在 `mcp.rs` 中便于适配 | | MCP 子进程异常退出 | 中 | 实现超时机制 + 错误恢复;进程退出时自动标记为不可用 | | 工具执行死循环(LLM 反复调用工具) | 中 | `max_turns` 硬限制,达到上限后终止循环 | | JSON-RPC 消息竞争(stdio 双工) | 中 | 请求和响应通过 `id` 字段匹配,使用 `Mutex` 保护写操作 + `HashMap` 等待响应,实现复杂度高于接口示意 | | 权限配置过于复杂 | 低 | PermissionConfig 提供合理默认值(允许 Read/Network,拒绝 Delete/Shell),简单场景无需自定义 | | 工具调用参数类型不匹配 | 低 | `execute()` 接收 `Value`,由实现方自行校验;通过 `ToolError::InvalidArguments` 返回结构化错误 | --- ## 验收标准 1. `cargo check` 编译通过 2. `cargo clippy` 无警告 3. 模块文件路径正确:`src/tools.rs` + `src/tools/{base,registry,permission,mcp,error}.rs` 4. `BaseTool` trait 可被自定义工具实现,`name()` / `description()` / `parameters()` / `execute()` 四个方法正常工作 5. `ToolRegistry` 支持注册、查找、列出、注销操作 6. `ToolRegistry::definitions()` 返回正确的 `Vec` 7. `ToolRegistry::invoke()` 执行工具前进行权限检查 8. `ToolRegistry::invoke_all()` 并行执行多个工具调用 9. `PermissionChecker` 根据配置正确判定权限(白名单/黑名单/默认策略) 10. `LlmCycle::submit_with_tools()` 收到 `FinishReason::ToolCalls` 后自动执行工具并回传结果 11. `LlmCycle::submit_with_tools()` 达到 `max_turns` 上限时终止并返回错误 12. `LlmCycle::submit_stream_with_tools()` 在流式模式下发射 `ToolExecutionCompleted` 事件 13. 自动 tool 循环产生的 Tool 消息正确追加到 `cycle.messages()` 14. `McpClient::connect()` 能完成 MCP 协议握手(initialize) 15. `McpClient::list_tools()` 能获取 MCP Server 暴露的工具列表 16. `McpClient::call_tool()` 能调用 MCP Server 的工具 17. `McpClient::into_tools()` 能生成可供 `ToolRegistry` 注册的适配器 18. 所有新公开 API 有文档注释 19. 测试覆盖率:`cargo test` 全部通过 20. `BaseTool::execute()` 签名通过 `ToolContext` 参数预留了扩展点(session_id、cancellation_token),未来 Skill/Agent 层可在不修改 trait 签名的情况下注入上下文