728 lines
25 KiB
Markdown
728 lines
25 KiB
Markdown
# 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 透明
|
||
- 权限检查在工具执行之前,阻断后返回错误而非静默跳过
|
||
|
||
---
|
||
|
||
## 方案设计
|
||
|
||
### 模块结构
|
||
|
||
```
|
||
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;
|
||
pub use error::ToolError;
|
||
pub use mcp::McpClient;
|
||
pub use permission::{Permission, PermissionChecker, PermissionConfig};
|
||
pub use registry::{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<Permission> {
|
||
Vec::new()
|
||
}
|
||
|
||
/// 执行工具调用。
|
||
async fn execute(&self, args: Value) -> Result<Value, ToolError>;
|
||
}
|
||
```
|
||
|
||
**设计说明**:
|
||
- `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<Value, ToolError>,
|
||
}
|
||
|
||
/// 工具注册表 —— 管理工具注册、发现、调用。
|
||
pub struct ToolRegistry {
|
||
tools: HashMap<String, Arc<dyn BaseTool>>,
|
||
permission_checker: Option<Arc<PermissionChecker>>,
|
||
}
|
||
|
||
impl ToolRegistry {
|
||
pub fn new() -> Self;
|
||
|
||
/// 设置权限检查器(可选,不设置则不检查权限)。
|
||
pub fn with_permission_checker(mut self, checker: PermissionChecker) -> Self;
|
||
|
||
/// 注册一个工具。
|
||
pub fn register(&mut self, tool: Arc<dyn BaseTool>) -> Result<(), ToolError>;
|
||
|
||
/// 批量注册工具。
|
||
pub fn register_all(&mut self, tools: Vec<Arc<dyn BaseTool>>) -> Result<(), ToolError>;
|
||
|
||
/// 注销一个工具。
|
||
pub fn unregister(&mut self, name: &str) -> Option<Arc<dyn BaseTool>>;
|
||
|
||
/// 按名称查找工具。
|
||
pub fn get(&self, name: &str) -> Option<Arc<dyn BaseTool>>;
|
||
|
||
/// 获取所有已注册工具的名称列表。
|
||
pub fn list_tools(&self) -> Vec<String>;
|
||
|
||
/// 获取所有工具的 ToolDefinition 列表(用于传递给 LLM)。
|
||
pub fn definitions(&self) -> Vec<ToolDefinition>;
|
||
|
||
/// 调用一个工具(含权限检查)。
|
||
pub async fn invoke(&self, name: &str, args: Value) -> Result<ToolInvocation, ToolError>;
|
||
|
||
/// 批量执行工具调用(并行执行互不依赖的工具)。
|
||
pub async fn invoke_all(
|
||
&self,
|
||
calls: Vec<(String, Value)>,
|
||
) -> Vec<ToolInvocation>;
|
||
}
|
||
```
|
||
|
||
**核心逻辑**:
|
||
- `invoke()`:查找工具 → 权限检查 → 执行 → 返回 `ToolInvocation`
|
||
- `invoke_all()`:对多个工具调用并行执行(使用 `tokio::join!` 或 `futures::join_all`),适用于 LLM 同时发出多个 tool_calls 的场景
|
||
- `definitions()`:将注册的工具批量转换为 `Vec<ToolDefinition>`,供 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<Permission>,
|
||
/// 拒绝的权限列表(优先级高于 allowed)。
|
||
pub denied: Vec<Permission>,
|
||
/// 是否允许未声明权限的工具执行(默认为 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。
|
||
|
||
```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<String>,
|
||
},
|
||
// /// SSE(Server-Sent Events)传输(未来支持)。
|
||
// Sse { url: String },
|
||
}
|
||
|
||
/// MCP 客户端 —— 与 MCP 服务器通信。
|
||
pub struct McpClient {
|
||
transport: McpTransport,
|
||
server_name: String,
|
||
/// 已初始化的工具列表(缓存)。
|
||
tools: Vec<McpTool>,
|
||
/// 是否已初始化。
|
||
initialized: AtomicBool,
|
||
/// 超时时间(秒)。
|
||
timeout_secs: u64,
|
||
}
|
||
|
||
/// MCP 服务器暴露的工具(缓存结构)。
|
||
struct McpTool {
|
||
name: String,
|
||
description: Option<String>,
|
||
input_schema: Value,
|
||
}
|
||
|
||
impl McpClient {
|
||
/// 创建一个 MCP 客户端。
|
||
pub fn new(server_name: impl Into<String>, 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<Vec<ToolDefinition>, ToolError>;
|
||
|
||
/// 调用一个工具(调用 tools/call)。
|
||
pub async fn call_tool(&self, name: &str, args: Value) -> Result<Value, ToolError>;
|
||
|
||
/// 关闭连接(终止子进程)。
|
||
pub async fn close(&mut self) -> Result<(), ToolError>;
|
||
|
||
/// 将 MCP 客户端转换为 BaseTool 适配器列表(用于注册到 ToolRegistry)。
|
||
pub fn into_tools(self) -> Vec<Arc<dyn BaseTool>>;
|
||
}
|
||
```
|
||
|
||
**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 的启动命令和参数
|
||
|
||
### 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<ChatResponse, LlmError> {
|
||
let tools = registry.definitions();
|
||
let max_turns = self.config.max_turns.unwrap_or(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<ChatResponse, LlmError> {
|
||
// ... 提取 submit() 中的 request → response 逻辑(不含 user prompt push)
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键设计决策**:
|
||
|
||
| 决策 | 选择 | 理由 |
|
||
|------|------|------|
|
||
| 循环方式 | 同步循环(单线程串行) | 工具执行依赖前一轮结果,串行更安全 |
|
||
| 最大轮次 | `CycleConfig.max_turns`,默认 10 | 防止无限循环(LLM 反复调用工具) |
|
||
| 工具并行 | `invoke_all()` 互不依赖的工具并行 | LLM 可能一次发出多个 tool_calls(parallel_tool_calls) |
|
||
| 错误处理 | 工具执行错误以文本回传 LLM,而非终止循环 | LLM 可自行从错误中恢复 |
|
||
| 消息追踪 | 所有工具交互通过 `self.messages` 持久化 | 调用方能通过 `cycle.messages()` 查看完整轨迹 |
|
||
|
||
**流式模式支持**:
|
||
|
||
`submit_stream()` 的增强方案:新增 `submit_stream_with_tools()`,在流式事件层面支持自动 tool 循环。
|
||
|
||
```rust
|
||
impl LlmCycle {
|
||
pub async fn submit_stream_with_tools(
|
||
&mut self,
|
||
prompt: String,
|
||
registry: &ToolRegistry,
|
||
) -> Result<Pin<Box<dyn Stream<Item = StreamEvent> + Send>>, LlmError> {
|
||
// 1. 使用 submit_stream() 获取初始事件流
|
||
// 2. 监听 TurnComplete { reason: ToolCalls }
|
||
// 3. 触发时:通过 ToolRegistry 执行工具
|
||
// 4. 发射 ToolExecutionCompleted 事件
|
||
// 5. 将工具结果注入 messages
|
||
// 6. 自动发起下一轮请求(递归)
|
||
// 7. 直到 finish_reason 为 Stop
|
||
}
|
||
}
|
||
```
|
||
|
||
**事件发射时序**:
|
||
```
|
||
submit_stream_with_tools("查天气")
|
||
│
|
||
├─ AssistantTextDelta "我来查一下北京的天气..."
|
||
├─ ToolExecutionStarted { tool_name: "get_weather", input: {city:"北京"}, id:"call_1" }
|
||
├─ TurnComplete { reason: ToolCalls }
|
||
│
|
||
├── [自动] 执行工具 get_weather({city:"北京"})
|
||
│
|
||
├─ ToolExecutionCompleted { tool_name: "get_weather", output: {temp:22}, is_error:false }
|
||
│
|
||
├─ AssistantTextDelta "北京今天 22°C"
|
||
├─ TurnComplete { reason: Stop }
|
||
│
|
||
└─ (流结束)
|
||
```
|
||
|
||
### 7. 自定义工具示例
|
||
|
||
```rust
|
||
use agcore::tools::prelude::*;
|
||
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<Value, ToolError> {
|
||
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 转换)
|
||
```
|
||
|
||
---
|
||
|
||
## 实现计划
|
||
|
||
### 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)
|
||
- 创建 `src/tools.rs` 模块根,声明子模块,重导出公共 API
|
||
- `lib.rs` 添加 `pub mod tools;`
|
||
- 编写 1 个 MockTool 测试工具并验证 trait 实现
|
||
- 运行 `cargo check` 验证
|
||
|
||
### Step 5: ToolRegistry
|
||
|
||
- 创建 `src/tools/registry.rs`
|
||
- 定义 `ToolInvocation` 结构体 + `ToolRegistry`
|
||
- 实现核心方法:register / get / list / definitions / invoke / invoke_all
|
||
- `invoke_all()` 使用 `futures::future::join_all` 并行执行互不依赖的工具
|
||
- `definitions()` 将 `HashMap` 中的工具转换为 `Vec<ToolDefinition>`
|
||
- 编写 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` 文档注释
|
||
- 编写 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 请求,终止子进程
|
||
- 实现 `into_tools()`:将 MCP 工具转换为 `Vec<Arc<dyn BaseTool>>` 适配器
|
||
- 设置 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` 保护写操作 |
|
||
| 权限配置过于复杂 | 低 | 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<ToolDefinition>`
|
||
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` 全部通过
|