10.1 `#[tool]` 属性宏详解

`#[tool]` 是 RAF 框架提供的 proc-macro 属性宏,用于自动生成 `ITool` trait 的实现代码,让开发者可以专注于业务逻辑而无需手动编写 JSON Schema、参数反序列化器或样板 trait 实现。

架构概览

`#[tool]` 宏支持三种使用模式:

flowchart TD
    A["#[tool] 输入"] --> B{语法分析}
    B -->|ItemFn| C["异步函数模式<br/>(无状态工具)"]
    B -->|ItemImpl| D["impl 块模式<br/>(有状态工具,推荐)"]
    B -->|DeriveInput| E["结构体模式<br/>(兼容旧写法)"]
    C --> F[生成 PascalCase 结构体]
    C --> G[生成 Args 反序列化器]
    C --> H[生成 ITool impl]
    C --> I[生成完整 JSON Schema]
    D --> J[保留原始 impl 块]
    D --> K[生成 CallArgs 反序列化器]
    D --> L[生成 ITool impl]
    D --> M[生成完整 JSON Schema]
    E --> N[原样保留结构体]
    E --> O[生成委托 ITool impl]
    E --> P[parameters() 返回空 schema]

异步函数模式

将 `#[tool]` 应用到一个 `async fn` 上,宏会自动:

1. 将函数名转换为 PascalCase 作为工具结构体名 2. 为每个参数生成 `#[derive(Deserialize)]` 的反序列化结构体 3. 从参数类型和 `#[param]` 属性中提取 JSON Schema 4. 生成完整的 `ITool` trait 实现(含 `kind()`)

基础示例

use rust_agent_macros::tool;

#[tool(description = "将两个数字相加", kind = "function")]
async fn add(
    #[param(desc = "第一个加数")] a: i64,
    #[param(desc = "第二个加数")] b: i64,
) -> rust_agent_core::ToolResult {
    rust_agent_core::ToolResult::success(serde_json::json!({
        "result": a + b
    }))
}

上述代码经过宏展开后生成:

// ── 参数反序列化结构体 ──
#[derive(serde::Deserialize)]
#[allow(non_snake_case)]
#[doc(hidden)]
struct AddArgs {
    pub a: i64,
    pub b: i64,
}

// ── PascalCase 工具结构体 ──
pub struct Add;

impl Add {
    pub async fn call(&self, a: i64, b: i64) -> rust_agent_core::ToolResult {
        // ... 用户代码体 ...
    }
}

// ── ITool 实现 ──
#[async_trait::async_trait]
impl rust_agent_core::ITool for Add {
    fn name(&self) -> &str {
        "add"
    }

    fn description(&self) -> &str {
        "将两个数字相加"
    }

    fn parameters(&self) -> serde_json::Value {
        let mut props = serde_json::Map::new();
        // 自动生成包含 a、b 的 properties,含 description、
        // type("integer")、required: ["a", "b"]
        // ...
    }

    async fn execute(
        &self,
        arguments: serde_json::Value,
    ) -> rust_agent_core::Result<rust_agent_core::ToolResult> {
        let args: AddArgs = serde_json::from_value(arguments)
            .map_err(|e| rust_agent_core::AgentError::ToolError(
                format!("Argument deserialization failed: {}", e)
            ))?;
        Ok(self.call(args.a, args.b).await)
    }

    fn kind(&self) -> &str {
        "function"
    }
}

生成规则

| 原始元素 | 生成结果 | 说明 | |----------|---------|------| | 函数名 `snake_case` | 结构体名 `PascalCase` | 例如 `web_search` → `WebSearch` | | 参数 `name: Type` | `Args` 结构体字段 | 自动 `#[derive(Deserialize)]` | | `#[param(desc = "...")]` | Schema `description` 字段 | 注入到 JSON Schema 属性中 | | `Option<T>` 参数 | 可选参数(非 required) | `is_option_type` 检测 | | `#[tool(kind = "...")]` | `kind()` 返回值 | 默认 `"function"` | | 返回 `ToolResult` | `ITool::execute` 返回值 | 自动包装 |

带可选参数的工具

#[tool(description = "搜索文件系统中的文件", kind = "file")]
async fn search_file(
    #[param(desc = "搜索模式(glob)")] pattern: String,
    #[param(desc = "搜索起始目录")] directory: Option<String>,
    #[param(desc = "是否递归搜索")] recursive: Option<bool>,
) -> rust_agent_core::ToolResult {
    let dir = directory.unwrap_or_else(|| ".".to_string());
    let rec = recursive.unwrap_or(true);
    // ... 搜索逻辑 ...
    rust_agent_core::ToolResult::success(serde_json::json!({"matches": []}))
}

生成的 JSON Schema 中 `directory` 和 `recursive` 因为类型是 `Option<T>` 而不会出现在 `required` 数组中。

impl 块模式(推荐用于有状态工具)

当工具需要持有内部状态(如 `WorkspaceScope`、`Arc<Vec<AgentSkill>>` 等),而又希望获得自动生成的 JSON Schema 时,使用 impl 块模式。

**优势:** - 自动生成完整 JSON Schema(含参数名、类型、描述、required) - 消除手写 `Args` 结构体和 `serde_json::from_value` 样板代码 - `call` 方法直接使用 typed 参数,无需手动反序列化 - 结构体可以持有状态并实现 `IScopeTool` 等其他 trait

完整示例

use std::sync::Arc;
use rust_agent_core::{IScopeTool, ITool, ScopePolicy, 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> {
        // 直接使用 path、offset、limit 参数 —— 无需手动反序列化!
        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": "..."})))
    }
}

**宏展开后生成:**

// ── 参数反序列化结构体 ──
#[derive(serde::Deserialize)]
#[allow(non_snake_case)]
#[doc(hidden)]
struct ReadFileCallArgs {
    pub path: String,
    pub offset: Option<i64>,
    pub limit: Option<i64>,
}

// ── ITool 实现 ──
#[async_trait::async_trait]
impl rust_agent_core::ITool for ReadFile {
    fn name(&self) -> &str {
        "read_file"   // CamelCase → snake_case
    }

    fn description(&self) -> &str {
        "Reads a file from the local filesystem..."
    }

    fn parameters(&self) -> serde_json::Value {
        // 自动生成的 JSON Schema:
        // {
        //   "type": "object",
        //   "properties": {
        //     "path": { "type": "string", "description": "Absolute path to the file" },
        //     "offset": { "type": "integer", "description": "Starting line number..." },
        //     "limit": { "type": "integer", "description": "Maximum number of lines..." }
        //   },
        //   "required": ["path"]
        // }
    }

    async fn execute(&self, arguments: serde_json::Value) -> Result<ToolResult> {
        let args: ReadFileCallArgs = serde_json::from_value(arguments)?;
        self.call(args.path, args.offset, args.limit).await
    }

    fn kind(&self) -> &str {
        "file"
    }
}

// 原始 impl 块被保留(#[param] 属性被自动剥离)
impl ReadFile {
    async fn call(&self, path: String, offset: Option<i64>, limit: Option<i64>) -> Result<ToolResult> {
        // ... 用户代码 ...
    }
}

impl 块模式的约束

| 约束 | 说明 | |------|------| | 必须是 inherent impl | 不能是 `impl Trait for Struct` | | 不能是泛型 impl | 不支持 `impl<T> MyTool<T>` | | 必须包含 `async fn call` | 方法名必须是 `call`,第一个参数必须是 `&self` | | `#[tool]` 放在 impl 块上 | 而不是放在 struct 定义上 |

结构体模式(兼容旧写法,不推荐)

当 `#[tool]` 应用在结构体上时,宏假设结构体已经手动实现了 `call(&self, arguments: serde_json::Value) -> Result<ToolResult>` 方法,宏只负责生成委托的 `ITool` 实现。

**注意**:struct 模式下 `parameters()` 返回空的 JSON Schema,因为宏无法从结构体字段推断参数信息。**新代码推荐使用 impl 块模式。**

示例

use std::sync::Arc;
use rust_agent_core::WorkspaceScope;

#[tool(description = "读取本地文件系统中的文件内容", kind = "file")]
pub struct ReadFile {
    pub scope: Option<Arc<WorkspaceScope>>,
}

impl ReadFile {
    pub async fn call(
        &self,
        arguments: serde_json::Value,
    ) -> rust_agent_core::Result<rust_agent_core::ToolResult> {
        // 手动实现参数解析和执行逻辑
        let path = arguments["path"].as_str()
            .ok_or_else(|| rust_agent_core::AgentError::ToolError("Missing path".into()))?;
        // ...
        Ok(rust_agent_core::ToolResult::success(
            serde_json::json!({"content": "..."})
        ))
    }
}

宏展开生成:

#[async_trait::async_trait]
impl rust_agent_core::ITool for ReadFile {
    fn name(&self) -> &str {
        "read_file"        // CamelCase → snake_case
    }

    fn description(&self) -> &str {
        "读取本地文件系统中的文件内容"
    }

    fn parameters(&self) -> serde_json::Value {
        serde_json::json!({"type": "object", "properties": {}})
        // ⚠️ 空 schema —— 建议迁移到 impl 块模式
    }

    async fn execute(
        &self,
        arguments: serde_json::Value,
    ) -> rust_agent_core::Result<rust_agent_core::ToolResult> {
        self.call(arguments).await
    }

    fn kind(&self) -> &str {
        "file"
    }
}

`kind` 配置

`#[tool]` 宏支持 `kind = "..."` 属性,控制 `ITool::kind()` 返回值,映射到 `ToolDecl` 分类标签:

| kind | 适用场景 | |------|---------| | `"function"` | 用户注册的函数工具(默认) | | `"custom"` | 工厂注册的自定义工具 | | `"web"` | 网络搜索/抓取工具 | | `"file"` | 文件系统工具 | | `"shell"` | Shell 命令执行 | | `"skills"` | 技能加载和资源工具 | | `"code"` | 代码解释器/沙箱 | | `"mcp"` | MCP 远程工具 | | `"openapi"` | OpenAPI 规范工具 |

内部实现细节

`parse_attr` — 属性解析

从 `#[tool(description = "...", kind = "...")]` 属性中提取描述和分类字符串。也支持 `desc` 作为 `description` 的简写别名。

fn parse_attr(attr: TokenStream) -> (String, String) {
    // 解析 Punctuated<MetaNameValue, Token![,]>
    // 提取 "description"/"desc" 和 "kind" 的值
    // kind 默认值为 "function"
}

`expand_tool_fn` — 函数模式展开

核心生成逻辑分为以下步骤:

1. **命名生成**:函数名 `snake_case` → `PascalCase`(`to_pascal_case`) 2. **参数提取**:遍历 `func.sig.inputs`,提取每个参数的标识符、类型、属性和是否 Optional 3. **Schema 生成**:遍历参数生成 JSON Schema properties 代码 4. **Required 检测**:非 Option 类型的参数加入 `required` 数组 5. **Args 结构体**:为参数自动派生 `Deserialize` 6. **代码拼接**:组合以上部分成完整的 TokenStream

`expand_tool_impl` — impl 块模式展开

核心生成逻辑为:

1. **验证**:确认是 inherent impl(非 trait impl)、非泛型、包含 `async fn call` 2. **提取 struct 名**:从 `impl <Name>` 的 `self_ty` 中解析 3. **查找 call 方法**:遍历 `items`,匹配 `ImplItem::Fn` 且 `sig.ident == "call"` 4. **提取参数**:跳过 receiver(`&self`),提取后续参数的 ident、Type、`#[param(desc)]` 注解 5. **Schema 生成**:复用函数模式的 Schema 生成逻辑(`rust_type_to_schema_tokens`) 6. **`Args` 结构体**:生成 `{Name}CallArgs` 私有结构体 7. **剥离 `#[param]`**:在重新发出的 `impl` 块中自动去除 `#[param]` 属性 8. **代码拼接**:生成 `ITool` impl + 原始 impl 块

`expand_tool_struct` — 结构体模式展开

更简单:保留原始结构体定义,仅添加 `ITool` trait 实现,将 `execute` 委托给 `self.call(arguments)`。`parameters()` 返回空 schema。

`extract_param_desc` — 参数描述提取

从参数的 `#[param(desc = "...")]` 或 `#[param(description = "...")]` 属性中提取描述文本,注入到 JSON Schema 的 `description` 字段。

`is_option_type` — 可选类型检测

通过检查类型路径的最后一个段是否为 `Option` 来判断参数是否可选。

`rust_type_to_schema_tokens` — 类型到 Schema 转换

处理三种情况:

1. `Option<T>` — 提取内部类型,按 `T` 生成 Schema 2. `Vec<T>` — 生成 `{"type": "array", "items": <T schema>}` 3. 基本类型 — `String` → `"string"`、`i64` → `"integer"`、`f64` → `"number"`、`bool` → `"boolean"`

详细映射表请参阅 [10.2 类型映射](macro-type-mapping.md)。

`struct_name_to_tool_name` — 结构体名到工具名转换

将 CamelCase 结构体名转换为 snake_case 工具名: - `ReadFile` → `"read_file"` - `LoadSkillTool` → `"load_skill"`(自动剥离 `_tool` 后缀) - `RunCommand` → `"run_command"`

在 Agent 中使用

use rust_agent_framework::AgentBuilder;

let agent = AgentBuilder::new("calculator")
    .chat_client(client)
    .instructions("你是一个计算助手。")
    .with_tool(Add)      // 直接传入 PascalCase 结构体实例
    .with_tool(Multiply)
    .build()?;

每个通过 `#[tool]` 宏定义的工具都是一个实现了 `ITool` 的零大小类型(ZST),可以直接实例化并注册到 Agent 中。