函数工具
使用自定义 Rust 函数扩展 agent 的能力。
什么是函数工具?
函数工具让你能够赋予 agent 超越对话的能力——调用 APIs、执行计算、访问数据库或运行任何自定义逻辑。LLM 会根据用户的请求决定何时使用工具。
主要亮点:
- 🚀
#[tool]宏 - 零样板代码的工具注册(推荐)- 🔧
FunctionTool::new()- 手动封装任意异步函数- 📝 JSON 参数 - 灵活的输入/输出
- 🎯 类型安全的架构 - 通过 schemars 根据类型自动生成 JSON Schema
- 🔗 上下文访问 - 会话状态、制品、记忆
工具执行流程
推荐:#[tool] 宏
创建工具最快捷的方式。该宏会将你的文档注释作为描述,并根据参数类型派生 JSON 架构:
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_only | is_read_only() → true — 并发 Auto 调度所需的两个信号之一 |
concurrency_safe | is_concurrency_safe() → true — 并发 Auto 调度所需的两个信号之一 |
long_running | is_long_running() → true — 防止 LLM 再次调用待处理工具 |
不带属性的普通 #[tool] 会保留默认值(全部为 false),因此现有代码不受影响。
替代方案:FunctionTool::new()
对于动态工具,或当你更喜欢显式注册时:
使用 FunctionTool::new() 创建工具,并且始终添加架构,以便 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-3.7-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 将不知道要传递哪些参数,并且可能不会调用该工具。
工作原理:
- 用户询问:“东京的天气怎么样?”
- LLM 决定使用
get_weather和{"location": "Tokyo"}进行调用 - 工具返回
{"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} - 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 步:使用架构处理类型化参数
对于复杂工具,使用带有 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>();
该架构使用 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()- 当前用户 IDctx.session_id()- 当前会话 IDctx.agent_name()- agent 的名称ctx.artifacts()- 访问工件存储ctx.search_memory(query)- 搜索记忆服务
长时间运行的工具
对于需要较长时间的操作(数据处理、外部 APIs),请使用非阻塞模式:
- 启动工具立即返回一个 task_id
- 后台工作异步运行
- 状态工具允许用户检查进度
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 此工具会返回待处理状态- 工具使用
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 是空操作,因此现有不进行流式传输的工具和运行器不会受到影响。只有选择启用的工具才会发出进度,并且只有检查 tool_progress_stream() 的消费者才能观察到这些进度。
进度是有界且有损的。 工具生成输出的速度可能快于客户端消费输出的速度——例如编译器日志、Shell 命令或失控循环——因此,框架会限制其暂存和转发的内容量,而不是让其无限增长:
| 限制 | 值 | 超出限制时 |
|---|---|---|
| 每个工具批次的队列深度 | 256 个事件 | 工具最多等待 100 毫秒以腾出空间,随后丢弃该数据块 |
| 每个数据块的字节数 | 8 KiB | 在字符边界处截断该数据块 |
| 每次工具调用的字节数 | 1 MiB | 不会转发剩余进度 |
当输出因上述任一原因被丢弃时,该调用会恰好发出一个携带文本 [adk: tool progress truncated] 的进度事件,因此始终可以看到间隙,而不会悄无声息。这样,速度较慢的消费者会暂时拖慢工具,但绝不会无限期地阻塞工具或耗尽内存。
这些限制仅适用于进度。工具的最终结果不受影响,因此如果这对你很重要,请在工具内部截断大型结果。
请参阅
streaming_bash示例,了解如何构建完整的 Web UI,从单个事件流中呈现实时的bash输出和一次性工具结果(read_file、grep、glob)。流式bash工具本身位于adk-devtools中。
运行示例
cargo adk new tool_agent --template tools
cd tool_agent
cargo run
最佳实践
- 清晰的描述 - 帮助 LLM 理解何时使用该工具
- 验证输入 - 对缺少的参数返回有帮助的错误消息
- 返回结构化的 JSON - 使用清晰的字段名称
- 保持工具专注 - 每个工具都应做好一件事
- 使用架构 - 对于复杂工具,定义参数架构
- 标记安全的只读工具 - 同时设置
.with_read_only(true)和.with_concurrency_safe(true),使Auto调度能够将其纳入并发子集
工具元数据:只读与并发
将工具标记为只读或并发安全,以启用更智能的调度:
// 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 服务器模式、子 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() → None,search_memory() → 空 vec。invocation_id 和 function_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 相同的构建器方法:with_long_running、with_parameters_schema、with_response_schema、with_scopes、with_read_only 和 with_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
}]
}))
}
框架会自动:
- 通过
FunctionResponseData::from_tool_result()检测inline_data/file_data - 对内联二进制数据进行 Base64 编码
- 将各部分嵌套到
functionResponsewire 对象中(匹配 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.7-flash。早期模型会返回 400 错误。
请参阅 examples/multimodal_function_response/ 获取完整的可运行示例。
上一页:← mistral.rs | 下一页:内置工具 →