函数工具

使用自定义 Rust 函数扩展代理能力。


什么是函数工具?

函数工具让你赋予代理超越对话的能力——调用 APIs、执行计算、访问数据库,或实现任何自定义逻辑。LLM 会根据用户请求决定何时使用工具。

要点

  • 🚀 #[tool] - 零样板工具注册(推荐)
  • 🔧 FunctionTool::new() - 手动包装任意异步函数
  • 📝 JSON 参数 - 灵活的输入/输出
  • 🎯 类型安全的模式 - 通过 schemars 从类型自动生成 JSON Schema
  • 🔗 上下文访问 - 会话状态、工件、记忆

工具执行流水线

Rendering architecture…

创建工具最快的方式。该宏会读取你的文档注释作为描述,并根据你的参数类型派生 JSON schema:

use adk_tool::tool;
use adk_core::AdkError;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{json, Value};

#[derive(Deserialize, JsonSchema)]
struct WeatherArgs {
    /// The city to look up
    city: String,
    /// Temperature unit (celsius or fahrenheit)
    unit: Option<String>,
}

/// Get the current weather for a city.
#[tool]
async fn get_weather(args: WeatherArgs) -> Result<Value, AdkError> {
    Ok(json!({ "temp": 22, "city": args.city }))
}

// Generated: pub struct GetWeather; — implements adk_core::Tool
// Use it: agent_builder.tool(Arc::new(GetWeather))

如果你的工具需要会话上下文,请将 Arc<dyn ToolContext> 添加为第一个参数:

use adk_core::ToolContext;
use std::sync::Arc;

/// Search the user's saved documents.
#[tool]
async fn search_docs(
    ctx: Arc<dyn ToolContext>,
    args: SearchArgs,
) -> Result<Value, AdkError> {
    let user_id = ctx.user_id();
    // ... use context for scoped access
}

工具元数据属性

在宏中直接将工具标记为只读、并发安全或长时间运行:

/// Look up cached data — no side effects, safe for parallel dispatch.
#[tool(read_only, concurrency_safe)]
async fn cache_lookup(args: LookupArgs) -> Result<Value, AdkError> {
    Ok(json!({"result": "cached"}))
}

/// Start a long-running background report.
#[tool(long_running)]
async fn generate_report(args: ReportArgs) -> Result<Value, AdkError> {
    Ok(json!({"task_id": "abc123", "status": "processing"}))
}

可用属性(全部可选,可自由组合):

属性作用
read_onlyis_read_only() → true — 并发 Auto 分发所需的两个信号之一
concurrency_safeis_concurrency_safe() → true — 并发 Auto 分发所需的两个信号之一
long_runningis_long_running() → true — 防止 LLM 重新调用一个待处理的工具

#[tool] 不带属性会保留默认值(全部 false),因此现有代码不受影响。


另一种方式:FunctionTool::new()

对于动态工具,或者当你更喜欢显式注册时:

使用 FunctionTool::new() 创建一个工具,并且始终添加一个 schema,这样 LLM 就知道要传递哪些参数:

use adk_rust::prelude::*;
use adk_rust::Launcher;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;

#[derive(JsonSchema, Serialize, Deserialize)]
struct WeatherParams {
    /// The city or location to get weather for
    location: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;

    // Weather tool with proper schema
    let weather_tool = FunctionTool::new(
        "get_weather",
        "Get current weather for a location",
        |_ctx, args| async move {
            let location = args.get("location")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            Ok(json!({
                "location": location,
                "temperature": "22°C",
                "conditions": "sunny"
            }))
        },
    )
    .with_parameters_schema::<WeatherParams>(); // Required for LLM to call correctly!

    let agent = LlmAgentBuilder::new("weather_agent")
        .instruction("You help users check the weather. Always use the get_weather tool.")
        .model(Arc::new(model))
        .tool(Arc::new(weather_tool))
        .build()?;

    Launcher::new(Arc::new(agent)).run().await?;
    Ok(())
}

⚠️ 重要:始终使用 .with_parameters_schema<T>() - 如果没有它,LLM 就不知道要传递哪些参数,可能不会调用该工具。

其工作方式如下

  1. 用户提问:"东京的天气怎么样?"
  2. LLM 决定使用 get_weather{"location": "Tokyo"} 进行调用
  3. 工具返回 {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"}
  4. LLM 格式化响应:"东京天气晴朗,22°C。"

第 2 步:参数处理

从 JSON args 中提取参数:

let order_tool = FunctionTool::new(
    "process_order",
    "Process an order. Parameters: product_id (required), quantity (required), priority (optional)",
    |_ctx, args| async move {
        // Required parameters - return error if missing
        let product_id = args.get("product_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| adk_core::AdkError::tool("product_id is required"))?;
        
        let quantity = args.get("quantity")
            .and_then(|v| v.as_i64())
            .ok_or_else(|| adk_core::AdkError::tool("quantity is required"))?;
        
        // Optional parameter with default
        let priority = args.get("priority")
            .and_then(|v| v.as_str())
            .unwrap_or("normal");
        
        Ok(json!({
            "order_id": "ORD-12345",
            "product_id": product_id,
            "quantity": quantity,
            "priority": priority,
            "status": "confirmed"
        }))
    },
);

第 3 步:带 Schema 的类型化参数

对于复杂工具,使用带有 JSON Schema 的类型化结构体:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(JsonSchema, Serialize, Deserialize)]
struct CalculatorParams {
    /// The arithmetic operation to perform
    operation: Operation,
    /// First operand
    a: f64,
    /// Second operand
    b: f64,
}

#[derive(JsonSchema, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

let calculator = FunctionTool::new(
    "calculator",
    "Perform arithmetic operations",
    |_ctx, args| async move {
        let params: CalculatorParams = serde_json::from_value(args)?;
        let result = match params.operation {
            Operation::Add => params.a + params.b,
            Operation::Subtract => params.a - params.b,
            Operation::Multiply => params.a * params.b,
            Operation::Divide if params.b != 0.0 => params.a / params.b,
            Operation::Divide => return Err(adk_core::AdkError::tool("Cannot divide by zero")),
        };
        Ok(json!({ "result": result }))
    },
)
.with_parameters_schema::<CalculatorParams>();

该 schema 会通过 schemars 从 Rust 类型自动生成。


第 4 步:多工具 Agent

向一个 agent 添加多个工具:

let agent = LlmAgentBuilder::new("assistant")
    .instruction("Help with calculations, conversions, and weather.")
    .model(Arc::new(model))
    .tool(Arc::new(calc_tool))
    .tool(Arc::new(convert_tool))
    .tool(Arc::new(weather_tool))
    .build()?;

LLM 会根据用户的请求自动选择合适的工具。


错误处理

对于工具特定的失败,使用 Tool 组件返回错误:

use adk_core::{AdkError, ErrorComponent, ErrorCategory};

let divide_tool = FunctionTool::new(
    "divide",
    "Divide two numbers",
    |_ctx, args| async move {
        let a = args.get("a").and_then(|v| v.as_f64())
            .ok_or_else(|| AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.missing_param",
                "Parameter 'a' is required",
            ))?;
        let b = args.get("b").and_then(|v| v.as_f64())
            .ok_or_else(|| AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.missing_param",
                "Parameter 'b' is required",
            ))?;
        
        if b == 0.0 {
            return Err(AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.division_by_zero",
                "Cannot divide by zero",
            ));
        }
        
        Ok(json!({ "result": a / b }))
    },
);

为了快速迁移,向后兼容的简写方式也可以使用:

Err(AdkError::tool("Parameter 'a' is required"))

错误消息会传递给 LLM,它可以重试或请求不同的输入。


工具上下文

通过 ToolContext 访问会话信息:

#[derive(JsonSchema, Serialize, Deserialize)]
struct GreetParams {
    #[serde(default)]
    message: Option<String>,
}

let greet_tool = FunctionTool::new(
    "greet",
    "Greet the user with session info",
    |ctx, _args| async move {
        let user_id = ctx.user_id();
        let session_id = ctx.session_id();
        let agent_name = ctx.agent_name();
        Ok(json!({
            "greeting": format!("Hello, user {}!", user_id),
            "session": session_id,
            "served_by": agent_name
        }))
    },
)
.with_parameters_schema::<GreetParams>();

可用上下文

  • ctx.user_id() - 当前用户 ID
  • ctx.session_id() - 当前会话 ID
  • ctx.agent_name() - agent 的名称
  • ctx.artifacts() - 对 artifact 存储的访问
  • ctx.search_memory(query) - 搜索 memory 服务

长时间运行的工具

对于需要较长时间的操作(数据处理、外部 APIs),请使用非阻塞模式:

  1. 启动工具 立即返回一个 task_id
  2. 后台工作 异步运行
  3. 状态工具 让用户检查进度
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(JsonSchema, Serialize, Deserialize)]
struct ReportParams {
    topic: String,
}

#[derive(JsonSchema, Serialize, Deserialize)]
struct StatusParams {
    task_id: String,
}

// Shared task store
let tasks: Arc<RwLock<HashMap<String, TaskState>>> = Arc::new(RwLock::new(HashMap::new()));
let tasks1 = tasks.clone();
let tasks2 = tasks.clone();

// Tool 1: Start (returns immediately)
let start_tool = FunctionTool::new(
    "generate_report",
    "Start generating a report. Returns task_id immediately.",
    move |_ctx, args| {
        let tasks = tasks1.clone();
        async move {
            let topic = args.get("topic").and_then(|v| v.as_str()).unwrap_or("general").to_string();
            let task_id = format!("task_{}", rand::random::<u32>());
            
            // Store initial state
            tasks.write().await.insert(task_id.clone(), TaskState {
                status: "processing".to_string(),
                progress: 0,
                result: None,
            });

            // Spawn background work (non-blocking!)
            let tasks_bg = tasks.clone();
            let tid = task_id.clone();
            tokio::spawn(async move {
                // Simulate work...
                tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
                if let Some(t) = tasks_bg.write().await.get_mut(&tid) {
                    t.status = "completed".to_string();
                    t.result = Some("Report complete".to_string());
                }
            });

            // Return immediately with task_id
            Ok(json!({"task_id": task_id, "status": "processing"}))
        }
    },
)
.with_parameters_schema::<ReportParams>()
.with_long_running(true);  // Mark as long-running

// Tool 2: Check status
let status_tool = FunctionTool::new(
    "check_report_status",
    "Check report generation status",
    move |_ctx, args| {
        let tasks = tasks2.clone();
        async move {
            let task_id = args.get("task_id").and_then(|v| v.as_str()).unwrap_or("");
            if let Some(t) = tasks.read().await.get(task_id) {
                Ok(json!({"status": t.status, "result": t.result}))
            } else {
                Ok(json!({"error": "Task not found"}))
            }
        }
    },
)
.with_parameters_schema::<StatusParams>();

要点

  • .with_long_running(true) 告诉 agent 该工具会返回 pending 状态
  • 工具通过 tokio::spawn() 启动工作并立即返回
  • 提供一个状态检查工具,以便用户轮询进度

这会添加一条说明,以防止 LLM 重复调用该工具。


从工具流式输出进度

长时间运行的工具可以在仍在执行时向 UI 推送中间输出,这样用户就能实时看到 shell 命令的 stdout、构建日志或下载字节流,而不必等待最终结果。随着输出到达,调用 ToolContext::emit_progress

use adk_core::{Result, Tool, ToolContext};
use std::sync::Arc;

#[async_trait::async_trait]
impl Tool for BuildTool {
    // ... name(), description(), parameters_schema() ...

    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: serde_json::Value) -> Result<serde_json::Value> {
        // Emit chunks as they arrive — each becomes a partial Event on the
        // agent's EventStream, the SAME stream the model's reply travels on.
        ctx.emit_progress("stdout", "Compiling project...\n").await;
        ctx.emit_progress("stdout", "Build finished in 4.2s\n").await;
        ctx.emit_progress("stderr", "warning: unused variable `x`\n").await;

        // The final return value is still the complete result the model consumes.
        Ok(serde_json::json!({ "status": "ok", "warnings": 1 }))
    }
}

签名:

async fn emit_progress(&self, stream: &str, chunk: &str)
  • stream — 分块的标签:"stdout""stderr",或任何自定义通道。
  • chunk — 要发出的文本(终端风格输出请按行发出)。

它如何到达 UI。 框架会将每个分块作为 agent 的 EventStream 上的一个部分 Event 进行转发。消费者通过 event.tool_progress_stream() 检测它并实时渲染。这里没有第二条通道,也不需要抓取日志——进度、模型文本和最终工具结果都会在同一条有序流上到达。

向后兼容。 默认的 emit_progress 是 no-op,因此现有不流式输出的工具和 runner 不受影响。只有选择启用的工具才会发出进度,只有检查 tool_progress_stream() 的消费者才能观察到它。

进度是有上限且可能丢失的。 工具产出输出的速度可能快于客户端消费的速度——例如编译器日志、shell 命令或失控的循环——因此框架会限制其可保留和转发的内容,而不是无限增长:

限制超出时
每个工具批次的队列深度256 个事件工具最多等待 100 毫秒以获取空间,然后该块会被丢弃
每块字节数8 KiB该块会在字符边界处被截断
每次工具调用的字节数1 MiB剩余进度不会被转发

当输出因以下任一原因被丢弃时,该调用会发出一个恰好携带文本 [adk: tool progress truncated] 的进度事件,因此始终会显示一个缺口,而不会静默消失。因此,慢速消费者只会短暂拖慢工具,但绝不会无限期阻塞它,也不会耗尽内存。

这些限制仅适用于 progress。工具的最终结果不受影响,因此如果这对你很重要,请在工具内部截断大型结果。

有关完整的 web UI 示例,请参见 streaming_bash 示例;它会从单一事件流中渲染实时 bash 输出以及一次性工具结果(read_filegrepglob)。流式的 bash 工具本身位于 adk-devtools


运行示例

cargo adk new tool_agent --template tools
cd tool_agent
cargo run

最佳实践

  1. 清晰的描述 - 帮助 LLM 理解何时使用该工具
  2. 验证输入 - 对缺失参数返回有帮助的错误消息
  3. 返回结构化 JSON - 使用清晰的字段名
  4. 保持工具聚焦 - 每个工具都应把一件事做好
  5. 使用 schema - 对于复杂工具,定义参数 schema
  6. 标记安全的只读工具 - 同时设置 .with_read_only(true).with_concurrency_safe(true),这样 Auto dispatch 就能将它们纳入并发子集

工具元数据:只读与并发

将工具标记为只读或并发安全,以启用更智能的调度:

// A lookup tool that performs no side effects
let lookup = FunctionTool::new("lookup", "Look up data", |_ctx, args| async move {
    Ok(json!({"result": "cached data"}))
})
.with_read_only(true)
.with_concurrency_safe(true); // Auto mode requires both signals

// A mutation tool (defaults: read_only=false, concurrency_safe=false)
let update = FunctionTool::new("update", "Update record", |_ctx, args| async move {
    Ok(json!({"updated": true}))
});

ToolExecutionStrategy::Auto 处于活动状态时,调度循环会先并发执行那些其选定工具同时从 is_read_only()is_concurrency_safe() 返回 true 的调用。随后再顺序执行其余所有调用。ToolExecutionStrategy::Parallel 是一个显式覆盖,会绕过这些信号,因此其调用方需要自行负责并发安全。


SimpleToolContext:在 agent 循环之外使用工具

当你需要在 agent 循环之外调用工具(测试、MCP server 模式、子 agent 委派)时,请使用 SimpleToolContext,而不是实现完整的 ToolContext trait 层次结构:

use adk_tool::SimpleToolContext;
use adk_core::ToolContext;
use std::sync::Arc;

// Construct with just a caller name — all other fields get sensible defaults
let ctx = SimpleToolContext::new("my-test-harness");

// Optionally override the function call ID
let ctx = SimpleToolContext::new("my-mcp-server")
    .with_function_call_id("custom-call-id");

// Bind a session ID so session-aware tools (and MCP servers that key state by
// session) see a stable identifier instead of the empty default.
let ctx = SimpleToolContext::new("my-mcp-server")
    .with_session_id("session-42");

// Use it to execute any tool
let tool_ctx: Arc<dyn ToolContext> = Arc::new(ctx);
let result = my_tool.execute(tool_ctx, json!({"key": "value"})).await?;

默认值:user_id()"anonymous"session_id() / branch()""artifacts()Nonesearch_memory() → 空 vec。invocation_idfunction_call_id 都会自动生成 UUIDs。当工具按会话路由或持久化状态时,请使用 with_session_id(...) 设置真实会话。


StatefulTool:跨调用共享状态

对于需要在调用之间维护状态的工具(计数器、缓存、连接池),请使用 StatefulTool<S>

use adk_tool::StatefulTool;
use adk_core::ToolContext;
use std::sync::Arc;
use tokio::sync::RwLock;

struct AppCache {
    entries: RwLock<HashMap<String, String>>,
}

let cache = Arc::new(AppCache {
    entries: RwLock::new(HashMap::new()),
});

let cache_tool = StatefulTool::new(
    "cache_lookup",
    "Look up a value in the application cache",
    cache.clone(),
    |state, _ctx, args| async move {
        let key = args["key"].as_str().unwrap_or("");
        let entries = state.entries.read().await;
        let value = entries.get(key).cloned().unwrap_or_default();
        Ok(json!({"key": key, "value": value}))
    },
)
.with_read_only(true)
.with_concurrency_safe(true);

StatefulTool 会在每次调用时克隆 Arc<S>(廉价的引用计数增加),因此所有执行都会共享相同的底层状态。它支持与 FunctionTool 相同的 builder 方法:with_long_runningwith_parameters_schemawith_response_schemawith_scopeswith_read_onlywith_concurrency_safe



多模态函数响应

Gemini 3 模型支持在函数响应中接收图像、音频、PDFs 和文件引用,而不仅仅是 JSON。工具可以通过在其 JSON 返回值中包含 inline_data 和/或 file_data 数组来返回多模态数据:

/// Tool that returns a chart image alongside JSON metadata.
async fn generate_chart(
    _ctx: Arc<dyn ToolContext>,
    args: serde_json::Value,
) -> Result<serde_json::Value> {
    let png_bytes: Vec<u8> = render_chart(&args);

    // Include inline_data in the return value — the framework extracts it automatically
    Ok(json!({
        "response": {
            "title": "Q4 Sales",
            "chart_type": "bar"
        },
        "inline_data": [{
            "mime_type": "image/png",
            "data": png_bytes
        }]
    }))
}

框架会自动:

  1. 通过 FunctionResponseData::from_tool_result() 检测 inline_data/file_data
  2. 对内联二进制数据进行 Base64 编码
  3. 将这些部分嵌套到 functionResponse wire object 中(与 Gemini 3 API 格式一致)

文件引用

对于外部存储的大文件,请使用带有 URI 的 file_data,而不是嵌入字节:

Ok(json!({
    "response": { "document_id": "report-2024", "pages": 12 },
    "file_data": [{
        "mime_type": "application/pdf",
        "file_uri": "gs://my-bucket/reports/report-2024.pdf"
    }]
}))

直接构造

对于框架层代码(自定义 agent、转换层),请直接构造 FunctionResponseData

use adk_core::{FunctionResponseData, InlineDataPart, FileDataPart};

// JSON + inline image
let frd = FunctionResponseData::with_inline_data(
    "chart_tool",
    json!({"title": "Q4 Chart"}),
    vec![InlineDataPart { mime_type: "image/png".into(), data: png_bytes }],
);

// JSON + file reference
let frd = FunctionResponseData::with_file_data(
    "doc_tool",
    json!({"status": "ok"}),
    vec![FileDataPart { mime_type: "application/pdf".into(), file_uri: "gs://bucket/file.pdf".into() }],
);

// JSON + both
let frd = FunctionResponseData::with_multimodal("tool", json, inline_parts, file_parts);

注意:多模态函数响应需要 Gemini 3 系列模型(gemini-3-flash-previewgemini-3-pro-preview)。更早的模型会返回 400 错误。

请参见 examples/multimodal_function_response/ 以获取完整可运行示例。


上一页← mistral.rs | 下一页内置工具 →

函数工具 - ADK-Rust 文档 | ADK-Rust