函数工具
使用自定义 Rust 函数扩展代理能力。
什么是函数工具?
函数工具让你赋予代理超越对话的能力——调用 APIs、执行计算、访问数据库,或实现任何自定义逻辑。LLM 会根据用户请求决定何时使用工具。
要点:
- 🚀
#[tool]宏 - 零样板工具注册(推荐)- 🔧
FunctionTool::new()- 手动包装任意异步函数- 📝 JSON 参数 - 灵活的输入/输出
- 🎯 类型安全的模式 - 通过 schemars 从类型自动生成 JSON Schema
- 🔗 上下文访问 - 会话状态、工件、记忆
工具执行流水线
推荐:#[tool] 宏
创建工具最快的方式。该宏会读取你的文档注释作为描述,并根据你的参数类型派生 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_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() 创建一个工具,并且始终添加一个 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 就不知道要传递哪些参数,可能不会调用该工具。
其工作方式如下:
- 用户提问:"东京的天气怎么样?"
- 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 步:带 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()- 当前用户 IDctx.session_id()- 当前会话 IDctx.agent_name()- agent 的名称ctx.artifacts()- 对 artifact 存储的访问ctx.search_memory(query)- 搜索 memory 服务
长时间运行的工具
对于需要较长时间的操作(数据处理、外部 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 该工具会返回 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_file、grep、glob)。流式的bash工具本身位于adk-devtools。
运行示例
cargo adk new tool_agent --template tools
cd tool_agent
cargo run
最佳实践
- 清晰的描述 - 帮助 LLM 理解何时使用该工具
- 验证输入 - 对缺失参数返回有帮助的错误消息
- 返回结构化 JSON - 使用清晰的字段名
- 保持工具聚焦 - 每个工具都应把一件事做好
- 使用 schema - 对于复杂工具,定义参数 schema
- 标记安全的只读工具 - 同时设置
.with_read_only(true)和.with_concurrency_safe(true),这样Autodispatch 就能将它们纳入并发子集
工具元数据:只读与并发
将工具标记为只读或并发安全,以启用更智能的调度:
// 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() → 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 相同的 builder 方法: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 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-preview、gemini-3-pro-preview)。更早的模型会返回 400 错误。
请参见 examples/multimodal_function_response/ 以获取完整可运行示例。
上一页:← mistral.rs | 下一页:内置工具 →