Outils de fonction
Ătendez les capacitĂ©s des agents avec des fonctions Rust personnalisĂ©es.
Que sont les outils de fonction ?
Les outils de fonction vous permettent de donner aux agents des capacitĂ©s au-delĂ de la conversation : appeler APIs, effectuer des calculs, accĂ©der Ă des bases de donnĂ©es ou exĂ©cuter toute logique personnalisĂ©e. Le LLM dĂ©cide quand utiliser un outil en fonction de la demande de lâutilisateur.
Points clés :
- đ macro
#[tool]- enregistrement dâoutil sans boilerplate (recommandĂ©)- đ§
FunctionTool::new()- envelopper manuellement nâimporte quelle fonction async- đ paramĂštres JSON - entrĂ©e/sortie flexibles
- đŻ schĂ©mas typĂ©s en toute sĂ©curitĂ© - JSON Schema automatique Ă partir des types via schemars
- đ AccĂšs au contexte - Ă©tat de session, artefacts, mĂ©moire
Pipeline dâexĂ©cution des outils
Recommandé : macro #[tool]
La façon la plus rapide de créer des outils. La macro lit votre commentaire de documentation comme description et dérive le schéma JSON à partir du type de vos arguments :
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))
Si votre outil a besoin du contexte de session, ajoutez Arc<dyn ToolContext> comme premier paramĂštre :
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
}
Attributs de métadonnées des outils
Marquez les outils comme lecture seule, sûrs pour la concurrence ou de longue durée directement dans la 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"}))
}
Attributs disponibles (tous facultatifs, combinables librement) :
| Attribut | Effet |
|---|---|
read_only | is_read_only() â true â lâun des deux signaux requis pour lâenvoi concurrent de Auto |
concurrency_safe | is_concurrency_safe() â true â lâun des deux signaux requis pour lâenvoi concurrent de Auto |
long_running | is_long_running() â true â empĂȘche LLM de rappeler un outil en attente |
Plain #[tool] sans attributs conserve les valeurs par dĂ©faut (tous false), donc le code existant nâest pas à€Șà„à€°à€à€Ÿà€”à€żà€€Ă©.
Alternative : FunctionTool::new()
Pour les outils dynamiques ou lorsque vous préférez un enregistrement explicite :
Créez un outil avec FunctionTool::new() et ajoutez toujours un schéma afin que le LLM sache quels paramÚtres transmettre :
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(())
}
â ïž Important : utilisez toujours
.with_parameters_schema<T>()- sans cela, le LLM ne saura pas quels paramĂštres transmettre et pourra ne pas appeler lâoutil.
Fonctionnement :
- Lâutilisateur demande : "What's the weather in Tokyo?"
- LLM dĂ©cide dâappeler
get_weatheravec{"location": "Tokyo"} - Lâoutil renvoie
{"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} - LLM formate la réponse : "The weather in Tokyo is sunny at 22°C."
Ătape 2 : gestion des paramĂštres
Extrayez les paramĂštres de lâ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"
}))
},
);
Ătape 3 : paramĂštres typĂ©s avec schĂ©ma
Pour les outils complexes, utilisez des structs typés avec le schéma 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>();
Le schĂ©ma est gĂ©nĂ©rĂ© automatiquement Ă partir des types Rust Ă lâaide de schemars.
Ătape 4 : agent multi-outils
Ajoutez plusieurs outils Ă un seul 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()?;
Le LLM choisit automatiquement le bon outil en fonction de la demande de lâutilisateur.
Gestion des erreurs
Renvoyez les erreurs avec le composant Tool pour les Ă©checs spĂ©cifiques Ă lâoutil :
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 }))
},
);
Pour une migration rapide, la forme abrégée rétrocompatible fonctionne aussi :
Err(AdkError::tool("Parameter 'a' is required"))
Les messages dâerreur sont transmis au LLM, qui peut rĂ©essayer ou demander une entrĂ©e diffĂ©rente.
Contexte de lâoutil
Accédez aux informations de session via 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>();
Contexte disponible :
ctx.user_id()- ID utilisateur actuelctx.session_id()- ID de session actuelctx.agent_name()- Nom de lâagentctx.artifacts()- AccĂšs au stockage des artefactsctx.search_memory(query)- Service de recherche mĂ©moire
Outils de longue durée
Pour les opérations qui prennent beaucoup de temps (traitement de données, APIs externe), utilisez le modÚle non bloquant :
- DĂ©marrer lâoutil renvoie immĂ©diatement un task_id
- Le travail en arriĂšre-plan sâexĂ©cute de maniĂšre asynchrone
- Lâoutil dâĂ©tat permet aux utilisateurs de vĂ©rifier la progression
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>();
Points clés :
.with_long_running(true)indique Ă lâagent que cet outil renvoie un Ă©tat en attente- Lâoutil lance le travail avec
tokio::spawn()et renvoie immĂ©diatement - Fournissez un outil de vĂ©rification dâĂ©tat afin que les utilisateurs puissent interroger la progression
Cela ajoute une note pour empĂȘcher le LLM dâappeler lâoutil de façon rĂ©pĂ©tĂ©e.
Diffusion de la progression dâun outil
Les outils de longue durĂ©e peuvent envoyer une sortie intermĂ©diaire Ă lâinterface utilisateur tout en
sâexĂ©cutant, afin que lâutilisateur voie en direct la stdout dâune commande shell, les logs dâun build ou les octets dâun tĂ©lĂ©chargement, au lieu dâattendre le rĂ©sultat final. Appelez
ToolContext::emit_progress Ă mesure que la sortie arrive :
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 }))
}
}
La signature :
async fn emit_progress(&self, stream: &str, chunk: &str)
streamâ un libellĂ© pour le fragment :"stdout","stderr", ou tout autre canal personnalisĂ©.chunkâ le texte Ă Ă©mettre (Ă©mettre ligne par ligne pour une sortie de type terminal).
Comment cela atteint lâinterface utilisateur. Le framework relaie chaque fragment sous forme de
Event partiel sur le
EventStream de lâagent. Un consommateur le dĂ©tecte avec event.tool_progress_stream() et
lâaffiche en direct. Il nây a pas de second canal et pas de scraping des logs â la progression,
le texte du modĂšle et le rĂ©sultat final de lâoutil arrivent tous sur un seul flux ordonnĂ©.
Rétrocompatible. Le emit_progress par défaut est un no-op, donc les outils
et exécuteurs existants qui ne diffusent pas de flux ne sont pas affectés. Seuls les outils qui activent cette fonctionnalité émettent de la progression, et seuls les consommateurs qui vérifient tool_progress_stream() la voient.
La progression est bornĂ©e et avec pertes. Un outil peut produire des donnĂ©es plus vite quâun client ne les consomme â un journal de compilateur, une commande shell, une boucle incontrĂŽlĂ©e â donc le framework limite ce quâil conserve et relaie au lieu de croĂźtre sans limite :
| Limite | Valeur | En le dépassant |
|---|---|---|
| Profondeur de file par lot dâoutil | 256 Ă©vĂ©nements | Lâoutil attend jusquâĂ 100 ms quâil y ait de la place, puis le bloc est supprimĂ© |
| Octets par bloc | 8 KiB | Le bloc est tronqué à une limite de caractÚre |
| Octets par appel d'outil | 1 MiB | La progression restante n'est pas transmise |
Lorsque la sortie est supprimĂ©e pour lâune de ces raisons, exactement un Ă©vĂ©nement de progression portant le texte [adk: tool progress truncated] est Ă©mis pour cet appel, de sorte quâun Ă©cart est toujours visible plutĂŽt que silencieux. Un consommateur lent ralentit donc briĂšvement lâoutil, mais ne peut jamais le bloquer indĂ©finiment ni Ă©puiser la mĂ©moire.
Ces limites sâappliquent uniquement Ă la progression. Le rĂ©sultat final dâun outil nâest pas affectĂ© ; tronquez donc les grands rĂ©sultats Ă lâintĂ©rieur de lâoutil si cela compte pour vous.
Voir lâexemple
streaming_bashpour une interface web complĂšte qui affiche une sortiebashen direct et des rĂ©sultats dâoutil en une seule fois (read_file,grep,glob) Ă partir dâun flux dâĂ©vĂ©nements unique. Lâoutil de streamingbashlui-mĂȘme se trouve dansadk-devtools.
Exemples dâexĂ©cution
cargo adk new tool_agent --template tools
cd tool_agent
cargo run
Meilleures pratiques
- Descriptions claires - Aidez le LLM Ă comprendre quand utiliser lâoutil
- Valider les entrĂ©es - Retournez des messages dâerreur utiles pour les paramĂštres manquants
- Retourner des JSON structurés - Utilisez des noms de champs clairs
- Garder les outils ciblés - Chaque outil doit faire une seule chose, et bien la faire
- Utiliser des schémas - Pour les outils complexes, définissez des schémas de paramÚtres
- Marquer les outils de lecture seule sûrs - Définissez à la fois
.with_read_only(true)et.with_concurrency_safe(true)afin que le dispatchAutopuisse les inclure dans son sous-ensemble concurrent
MĂ©tadonnĂ©es dâoutil : lecture seule et concurrence
Marquez les outils comme lecture seule ou sĂ»rs pour la concurrence afin dâactiver un dispatch plus intelligent :
// 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}))
});
Lorsque ToolExecutionStrategy::Auto est actif, la boucle de dispatch exĂ©cute dâabord les appels en parallĂšle lorsque leurs outils sĂ©lectionnĂ©s renvoient true Ă la fois depuis is_read_only() et is_concurrency_safe(). Elle exĂ©cute ensuite sĂ©quentiellement tous les appels restants. ToolExecutionStrategy::Parallel est une surcharge explicite qui contourne ces signaux, de sorte que son appelant assume la responsabilitĂ© de la sĂ©curitĂ© de la concurrence.
SimpleToolContext : Utiliser des outils en dehors de la boucle dâagent
Lorsque vous devez appeler un outil en dehors de la boucle dâagent (tests, mode serveur MCP, dĂ©lĂ©gation Ă un sous-agent), utilisez SimpleToolContext au lieu dâimplĂ©menter toute la hiĂ©rarchie 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?;
Valeurs par dĂ©faut : user_id() â "anonymous", session_id() / branch() â "", artifacts() â None, search_memory() â vecteur vide. invocation_id et function_call_id sont tous deux gĂ©nĂ©rĂ©s automatiquement UUIDs. DĂ©finissez une vĂ©ritable session avec with_session_id(...) lorsquâun outil route ou persiste lâĂ©tat par session.
StatefulTool : Ătat partagĂ© entre les invocations
Pour les outils qui doivent conserver un état entre les appels (compteurs, caches, pools de connexions), utilisez 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 clone le Arc<S> Ă chaque invocation (augmentation peu coĂ»teuse du compteur de rĂ©fĂ©rences), de sorte que toutes les exĂ©cutions partagent le mĂȘme Ă©tat sous-jacent. Il prend en charge les mĂȘmes mĂ©thodes de builder que FunctionTool : with_long_running, with_parameters_schema, with_response_schema, with_scopes, with_read_only, et with_concurrency_safe.
Connexes
- Outils intégrés - Outils préconstruits (GoogleSearch, ExitLoop)
- Outils MCP - Intégration Model Context Protocol
- LlmAgent - Ajout dâoutils aux agents
Réponses de fonction multimodales
Les modĂšles Gemini 3 prennent en charge la rĂ©ception dâimages, dâaudio, de PDFs et de rĂ©fĂ©rences de fichiers dans les rĂ©ponses de fonction â pas seulement de JSON. Les outils peuvent renvoyer des donnĂ©es multimodales en incluant des tableaux inline_data et/ou file_data dans leur valeur de retour 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
}]
}))
}
Le framework détecte automatiquement :
- les
inline_data/file_dataviaFunctionResponseData::from_tool_result() - les données binaires inline encodées en Base64
- les parties imbriquĂ©es Ă lâintĂ©rieur de lâobjet filaire
functionResponse(correspondant au format Gemini 3 API)
Références de fichiers
Pour les grands fichiers stockĂ©s Ă lâextĂ©rieur, utilisez file_data avec un URI au lieu dâintĂ©grer les octets :
Ok(json!({
"response": { "document_id": "report-2024", "pages": 12 },
"file_data": [{
"mime_type": "application/pdf",
"file_uri": "gs://my-bucket/reports/report-2024.pdf"
}]
}))
Construction directe
Pour le code au niveau du framework (agents personnalisés, couches de conversion), construisez directement 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);
Remarque : les réponses de fonction multimodales nécessitent des modÚles de la série Gemini 3 (
gemini-3-flash-preview,gemini-3-pro-preview). Les modÚles antérieurs renvoient une erreur 400.
Voir examples/multimodal_function_response/ pour un exemple complet et fonctionnel.
PrĂ©cĂ©dent : â mistral.rs | Suivant : Outils intĂ©grĂ©s â