4.6 自定义工具开发指南
RAF 提供四种定义工具的方式,从声明式的 `#[tool]` 宏到手动完全控制的 `impl ITool`。本章逐一剖析每种方式的适用场景、完整代码示例和最佳实践。
方式对比
| 方式 | 代码量 | JSON Schema | 参数反序列化 | 适用场景 | |------|--------|-------------|--------------|----------| | `#[tool]` async fn | 最少 | 自动生成(完整) | 自动 | 简单函数式工具;快速原型 | | `#[tool]` impl block | 适中 | 自动生成(完整) | 自动 | **有状态工具(推荐)**;需 scope 注入 | | `#[tool]` struct | 适中 | 返回空 schema | 手动在 call() 中 | 兼容旧代码(不推荐新项目使用) | | 手动实现 ITool | 最多 | 手动构建 | 手动 | 需完全控制;动态 schema(如 `cfg!()`) |
方式一:`#[tool]` async fn(无状态工具)
`#[tool]` 应用到异步函数时,自动生成一个同名的帕斯卡命名结构体,该结构体自动实现 `ITool`。这是最简单的方式,适用于无状态的纯函数工具。
use rust_agent_macros::tool;
use rust_agent_core::ToolResult;
#[tool(description = "Adds two numbers together", kind = "function")]
async fn add(
#[param(desc = "First number")] a: f64,
#[param(desc = "Second number")] b: f64,
) -> ToolResult {
ToolResult::success(serde_json::json!({"result": a + b}))
}
#[tool(description = "Returns the current date and time", kind = "function")]
async fn get_current_time() -> ToolResult {
let now = chrono::Utc::now();
ToolResult::success(serde_json::json!({
"iso8601": now.to_rfc3339(),
"timestamp": now.timestamp(),
}))
}
宏自动生成的 JSON Schema:
{
"type": "object",
"properties": {
"a": { "type": "number", "description": "First number" },
"b": { "type": "number", "description": "Second number" }
},
"required": ["a", "b"]
}
函数模式的局限性
- **不能有状态**:生成的结构体是无字段 unit struct(`pub struct Echo;`) - **不能实现其他 trait**:生成的结构体不可扩展 - **不支持 scope 注入**:无法实现 `IScopeTool`
方式二:`#[tool]` impl block(有状态工具,推荐)
当工具需要持有内部状态(如 `WorkspaceScope`、`Arc<Vec<AgentSkill>>` 等),但又希望获得自动生成的完整 JSON Schema,使用 impl 块模式。
use std::sync::Arc;
use rust_agent_core::{IScopeTool, ITool, ToolResult, WorkspaceScope};
use rust_agent_macros::tool;
pub struct ReadFile {
pub scope: Option<Arc<WorkspaceScope>>,
}
// IScopeTool 在独立的 impl 块中手动实现
impl IScopeTool for ReadFile {
fn create_scoped(&self, scope: Arc<WorkspaceScope>) -> Arc<dyn ITool> {
Arc::new(ReadFile { scope: Some(scope) })
}
}
#[tool(
description = "Reads a file from the local filesystem. Supports line range via offset/limit.",
kind = "file"
)]
impl ReadFile {
async fn call(
&self,
#[param(desc = "Absolute path to the file")] path: String,
#[param(desc = "Starting line number (1-based, optional)")] offset: Option<i64>,
#[param(desc = "Maximum number of lines to read (optional)")] limit: Option<i64>,
) -> rust_agent_core::Result<ToolResult> {
// 直接使用 typed 参数 —— 无需手动反序列化!
let base_dir = self.scope.as_ref()
.map(|s| s.root.clone())
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
// ... 业务逻辑 ...
Ok(ToolResult::success(serde_json::json!({"content": "..."})))
}
}
**关键优势:**
1. **完整 Schema 自动生成** — `parameters()` 返回包含 `path`、`offset`、`limit` 的完整 JSON Schema,含类型和描述 2. **typed 参数** — `call` 方法直接接收 `path: String`,无需 `#[derive(Deserialize)] struct Args` 和 `serde_json::from_value` 3. **可与 IScopeTool 共存** — 宏生成 `ITool` impl,你需要手动实现 `IScopeTool` impl,两个独立 inherent impl 块互不干扰 4. **消除样板代码** — 每个工具约节省 10 行反序列化样板
Rust 类型到 JSON Schema 映射
宏自动进行类型映射:
| Rust 类型 | JSON Schema type | |-----------|------------------| | `String`, `&str` | `"string"` | | `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `usize` | `"integer"` | | `f32`, `f64` | `"number"` | | `bool` | `"boolean"` | | `Option<T>` | `T` 的 schema(字段从 required 中移除) | | `Vec<T>` | `{ "type": "array", "items": T_schema }` | | 其他 | `"string"`(默认回退) |
`kind` 配置
`kind = "..."` 属性控制 `ITool::kind()` 输出,对应 `ToolDecl` 分类:
| kind | 适用场景 | |------|---------| | `"function"` | 用户注册的函数工具(默认) | | `"web"` | 网络搜索/抓取工具 | | `"file"` | 文件系统工具 | | `"shell"` | Shell 命令执行 | | `"skills"` | 技能加载和资源工具 | | `"code"` | 代码解释器/沙箱 | | `"custom"` | 工厂注册的自定义工具 |
impl 块模式的约束
| 约束 | 说明 | |------|------| | 必须是 inherent impl | 不能是 `impl Trait for Struct` | | 不能是泛型 impl | 不支持 `impl<T> MyTool<T>` | | 必须包含 `async fn call` | 方法名必须是 `call`,第一个参数必须是 `&self` | | `#[tool]` 放在 impl 块上 | 而不是放在 struct 定义上 |
方式三:`#[tool]` struct(兼容旧写法,不推荐)
将 `#[tool]` 应用在结构体上时,宏生成委托给 `self.call(arguments)` 的 `ITool` impl,但 `parameters()` 返回空 schema。新代码推荐使用方式二(impl block 模式)。
方式四:手动实现 ITool
最灵活的方式,需要完整实现 5 个方法。适用于需要精确控制 JSON Schema 格式、自定义错误处理、或需要动态 schema(如 `cfg!(windows)` 平台感知)的高级场景。
完整示例:HTTP 请求工具
use async_trait::async_trait;
use rust_agent_core::{ITool, ToolResult, AgentError, Result};
use serde::Deserialize;
pub struct HttpGetTool {
pub base_url: Option<String>,
}
#[async_trait]
impl ITool for HttpGetTool {
fn name(&self) -> &str {
"http_get"
}
fn description(&self) -> &str {
"Sends an HTTP GET request to the specified URL and returns the response body as text."
}
fn kind(&self) -> &str {
"function"
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to send the GET request to."
},
"headers": {
"type": "object",
"description": "Optional HTTP headers as key-value pairs.",
"additionalProperties": { "type": "string" }
},
"timeout_secs": {
"type": "integer",
"description": "Request timeout in seconds (default: 30)."
}
},
"required": ["url"]
})
}
async fn execute(&self, arguments: serde_json::Value) -> Result<ToolResult> {
#[derive(Deserialize)]
struct Args {
url: String,
headers: Option<std::collections::HashMap<String, String>>,
timeout_secs: Option<u64>,
}
let args: Args = serde_json::from_value(arguments)
.map_err(|e| AgentError::ToolError(
format!("Argument deserialization failed: {}", e)
))?;
// ... 业务逻辑 ...
Ok(ToolResult::success(serde_json::json!({"status": 200})))
}
}
手动方式的优缺点
**优点:** - JSON Schema 完全可控 - 支持动态 schema(`cfg!(windows)` 等运行时分支) - 支持复杂的泛型参数、自定义生命周期
**缺点:** - 代码量大(40–60 行样板) - JSON Schema 与参数定义分离,需手动保持同步 - 参数反序列化结构体与 Schema 之间没有编译期约束
注册和使用
四种方式定义的工具注册方式相同:
use rust_agent_core::ToolRegistry;
let mut registry = ToolRegistry::new();
// 方式一:宏生成的函数工具
registry.register(Add);
registry.register(GetCurrentTime);
// 方式二:impl block 模式
registry.register(ReadFile { scope: None });
// 方式三:struct 模式(兼容旧写法)
registry.register(MyLegacyTool);
// 方式四:手动实现
registry.register(HttpGetTool { base_url: Some("https://api.example.com".into()) });
选择指南
flowchart TD
START{"工具需要什么?"} --> STATE{"需要内部状态?"}
STATE -->|是| IMPL["使用 #[tool] impl block<br/>(推荐)"]
STATE -->|否| FN["使用 #[tool] async fn"]
IMPL --> NEED{"需要动态 schema?"}
NEED -->|是| MANUAL["手动实现 ITool"]
NEED -->|否| IMPL
FN --> COMPLEX{"参数结构很复杂?<br/>或需要动态 schema?"}
COMPLEX -->|是| MANUAL
COMPLEX -->|否| FN
**推荐的决策路径:**
1. 如果工具只是一个纯函数(无状态、无外部依赖)→ 使用 `#[tool] async fn` 2. 如果工具需要持有状态或外部引用(如 scope)→ 使用 `#[tool] impl block` 3. 如果需要动态 schema(如平台感知描述)→ 手动实现 `ITool`
关键要点
1. **`#[tool] async fn` 是最简方式** — 一行宏即可获得完整的 `ITool` 实现 + JSON Schema 2. **`#[tool] impl block` 是有状态工具的标准模式** — 保留扩展性(如 `IScopeTool`),同时享受完整 JSON Schema 自动生成 3. **`#[tool] struct` 已不推荐** — `parameters()` 返回空 schema,新项目应使用 impl block 模式 4. **手动实现是最终手段** — 仅在需要动态 schema 或极致灵活性的场景使用 5. **四种方式的工具注册完全相同** — `registry.register(tool)` 对任何实现了 `ITool` 的类型都适用