Ferramentas de Função
Amplie os recursos do agente com funções Rust personalizadas.
O que são ferramentas de função?
As ferramentas de função permitem oferecer aos agentes capacidades além da conversação — chamar APIs, realizar cálculos, acessar bancos de dados ou executar qualquer lógica personalizada. O LLM decide quando usar uma ferramenta com base na solicitação do usuário.
Principais destaques:
- 🚀 Macro
#[tool]— registro de ferramentas sem código repetitivo (recomendado)- 🔧
FunctionTool::new()— encapsula manualmente qualquer função assíncrona- 📝 Parâmetros JSON — entrada/saída flexíveis
- 🎯 Esquemas com segurança de tipos — JSON Schema automático a partir dos tipos via schemars
- 🔗 Acesso ao contexto — estado da sessão, artefatos e memória
Pipeline de execução da ferramenta
Recomendado: macro #[tool]
A maneira mais rápida de criar ferramentas. A macro lê seu comentário de documentação como descrição e deriva o esquema JSON a partir do tipo dos argumentos:
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))
Se sua ferramenta precisar do contexto da sessão, adicione Arc<dyn ToolContext> como primeiro parâmetro:
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
}
Atributos de metadados da ferramenta
Marque as ferramentas como somente leitura, seguras para concorrência ou de execução longa diretamente na macro:
/// 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"}))
}
Atributos disponíveis (todos opcionais e combináveis livremente):
| Atributo | Efeito |
|---|---|
read_only | is_read_only() → true — um dos dois sinais necessários para o despacho concorrente de Auto |
concurrency_safe | is_concurrency_safe() → true — um dos dois sinais necessários para o despacho concorrente de Auto |
long_running | is_long_running() → true — impede que LLM chame novamente uma ferramenta pendente |
Plain #[tool] sem atributos mantém os padrões (todos false), portanto o código existente não é afetado.
Alternativa: FunctionTool::new()
Para ferramentas dinâmicas ou quando você prefere o registro explícito:
Crie uma ferramenta com FunctionTool::new() e sempre adicione um esquema para que LLM saiba quais parâmetros passar:
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(())
}
⚠️ Importante: Sempre use
.with_parameters_schema<T>()— sem ele, LLM não saberá quais parâmetros passar e poderá não chamar a ferramenta.
Como funciona:
- O usuário pergunta: "Qual é o clima em Tóquio?"
- LLM decide chamar
get_weathercom{"location": "Tokyo"} - A ferramenta retorna
{"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} - LLM formata a resposta: "O clima em Tóquio está ensolarado, a 22°C."
Etapa 2: Tratamento de parâmetros
Extraia os parâmetros de 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"
}))
},
);
Etapa 3: Parâmetros tipados com esquema
Para ferramentas complexas, use estruturas tipadas com o esquema JSON:
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>();
O esquema é gerado automaticamente a partir dos tipos Rust usando schemars.
Etapa 4: Agente com múltiplas ferramentas
Adicione várias ferramentas a um único agente:
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 escolhe automaticamente a ferramenta correta com base na solicitação do usuário.
Tratamento de erros
Retorne erros com o componente Tool para falhas específicas da ferramenta:
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 }))
},
);
Para uma migração rápida, a abreviação compatível com versões anteriores também funciona:
Err(AdkError::tool("Parameter 'a' is required"))
As mensagens de erro são passadas para LLM, que pode tentar novamente ou solicitar uma entrada diferente.
Contexto da ferramenta
Acesse as informações da sessão por meio de 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>();
Contexto disponível:
ctx.user_id()— ID do usuário atualctx.session_id()— ID da sessão atualctx.agent_name()— Nome do agentectx.artifacts()— Acesso ao armazenamento de artefatosctx.search_memory(query)— Serviço de pesquisa de memória
Ferramentas de longa duração
Para operações que levam um tempo significativo (processamento de dados, APIs externo), use o padrão não bloqueante:
- A ferramenta de início retorna imediatamente com um task_id
- O trabalho em segundo plano é executado de forma assíncrona
- A ferramenta de status permite que os usuários verifiquem o progresso
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>();
Pontos principais:
.with_long_running(true)informa ao agente que essa ferramenta retorna um status pendente- A ferramenta inicia o trabalho com
tokio::spawn()e retorna imediatamente - Forneça uma ferramenta de verificação de status para que os usuários possam consultar o progresso
Isso adiciona uma observação para evitar que o LLM chame a ferramenta repetidamente.
Transmitindo o progresso de uma ferramenta
Ferramentas de longa duração podem enviar resultados intermediários à interface enquanto ainda estão em execução, para que o usuário veja ao vivo a saída padrão de um comando shell, os logs de uma compilação ou os bytes de um download, em vez de esperar pelo resultado final. Chame ToolContext::emit_progress à medida que a saída chegar:
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 }))
}
}
A assinatura:
async fn emit_progress(&self, stream: &str, chunk: &str)
stream— um rótulo para o bloco:"stdout","stderr"ou qualquer canal personalizado.chunk— o texto a ser emitido (emita uma linha por vez para obter uma saída no estilo de terminal).
Como isso chega à interface. O framework encaminha cada bloco como um Event parcial no EventStream do agente. Um consumidor o detecta com event.tool_progress_stream() e o renderiza ao vivo. Não há um segundo canal nem análise de logs — o progresso, o texto do modelo e o resultado final da ferramenta chegam todos em um único fluxo ordenado.
Compatível com versões anteriores. O emit_progress padrão não realiza nenhuma operação, portanto as ferramentas e os executores existentes que não transmitem dados não são afetados. Somente as ferramentas que optarem por isso emitem progresso, e somente os consumidores que verificarem tool_progress_stream() o observarão.
O progresso é limitado e apresenta perdas. Uma ferramenta pode produzir resultados mais rapidamente do que um cliente consegue consumi-los — um registro de compilação, um comando de shell, um loop descontrolado —, portanto, o framework limita o que manterá e encaminhará, em vez de crescer indefinidamente:
| Limite | Valor | Ao excedê-lo |
|---|---|---|
| Profundidade da fila por lote de ferramentas | 256 eventos | A ferramenta aguarda até 100 ms por espaço; em seguida, o bloco é descartado |
| Bytes por bloco | 8 KiB | O bloco é truncado em um limite de caractere |
| Bytes por chamada de ferramenta | 1 MiB | O progresso restante não é encaminhado |
Quando a saída é descartada por qualquer um desses motivos, exatamente um evento de progresso contendo o texto [adk: tool progress truncated] é emitido para essa chamada, portanto uma lacuna sempre fica visível em vez de permanecer silenciosa. Consequentemente, um consumidor lento desacelera brevemente a ferramenta, mas nunca pode bloqueá-la indefinidamente nem esgotar a memória.
Esses limites se aplicam somente ao progresso. O resultado final de uma ferramenta não é afetado, portanto trunque resultados grandes dentro da ferramenta, se isso for importante para você.
Consulte o exemplo
streaming_bashpara ver uma interface web completa que renderiza a saídabashem tempo real e os resultados de ferramenta de execução única (read_file,grep,glob) a partir de um único fluxo de eventos. A própria ferramenta debashem fluxo está emadk-devtools.
Exemplos de execução
cargo adk new tool_agent --template tools
cd tool_agent
cargo run
Práticas recomendadas
- Descrições claras - Ajude LLM a entender quando usar a ferramenta
- Valide as entradas - Retorne mensagens de erro úteis para parâmetros ausentes
- Retorne JSON estruturado - Use nomes de campos claros
- Mantenha as ferramentas focadas - Cada ferramenta deve fazer bem uma única coisa
- Use esquemas - Para ferramentas complexas, defina esquemas de parâmetros
- Marque ferramentas seguras e somente leitura - Defina
.with_read_only(true)e.with_concurrency_safe(true)para queAutopossa incluí-las em seu subconjunto concorrente
Metadados da ferramenta: somente leitura e concorrência
Marque as ferramentas como somente leitura ou seguras para concorrência para permitir um despacho mais inteligente:
// 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}))
});
Quando ToolExecutionStrategy::Auto está ativo, o loop de despacho primeiro executa chamadas em simultâneo quando as ferramentas selecionadas por elas retornam true tanto de is_read_only() quanto de is_concurrency_safe(). Em seguida, executa todas as chamadas restantes sequencialmente. ToolExecutionStrategy::Parallel é uma substituição explícita que ignora esses sinais, portanto a segurança de concorrência fica sob responsabilidade de quem faz a chamada.
SimpleToolContext: Usando ferramentas fora do loop do agente
Quando precisar chamar uma ferramenta fora do loop do agente (testes, modo de servidor MCP, delegação de subagentes), use SimpleToolContext em vez de implementar toda a hierarquia de traits ToolContext:
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?;
Padrões: user_id() → "anonymous", session_id() / branch() → "", artifacts() → None, search_memory() → vec vazia. Tanto invocation_id quanto function_call_id são UUIDs gerados automaticamente. Defina uma sessão real com with_session_id(...) quando uma ferramenta encaminhar ou persistir estado por sessão.
StatefulTool: Estado compartilhado entre invocações
Para ferramentas que precisam manter estado entre chamadas (contadores, caches, pools de conexão), use 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 clona o Arc<S> a cada invocação (incremento barato da contagem de referências), portanto todas as execuções compartilham o mesmo estado subjacente. Ele oferece os mesmos métodos construtores que FunctionTool: with_long_running, with_parameters_schema, with_response_schema, with_scopes, with_read_only e with_concurrency_safe.
Relacionados
- Ferramentas integradas - Ferramentas pré-construídas (GoogleSearch, ExitLoop)
- Ferramentas MCP - Integração com o Model Context Protocol
- LlmAgent - Adicionando ferramentas aos agentes
Respostas multimodais de funções
Os modelos Gemini 3 são compatíveis com o recebimento de imagens, áudio, PDFs e referências de arquivos em respostas de funções — não apenas JSON. As ferramentas podem retornar dados multimodais incluindo matrizes inline_data e/ou file_data no valor de retorno JSON:
/// 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
}]
}))
}
O framework automaticamente:
- Detecta
inline_data/file_datapor meio deFunctionResponseData::from_tool_result() - Codifica dados binários embutidos em Base64
- Aninha as partes no objeto de transmissão
functionResponse(seguindo o formato API do Gemini 3)
Referências de arquivos
Para arquivos grandes armazenados externamente, use file_data com um URI em vez de incorporar os bytes:
Ok(json!({
"response": { "document_id": "report-2024", "pages": 12 },
"file_data": [{
"mime_type": "application/pdf",
"file_uri": "gs://my-bucket/reports/report-2024.pdf"
}]
}))
Construção direta
Para código no nível do framework (agentes personalizados, camadas de conversão), construa FunctionResponseData diretamente:
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);
Observação: As respostas de função multimodais exigem modelos da série Gemini 3, como
gemini-3.7-flash. Modelos anteriores retornam um erro 400.
Consulte examples/multimodal_function_response/ para ver um exemplo completo em funcionamento.
Anterior: ← mistral.rs | Próximo: Ferramentas integradas →