Outils de fonction
Étendez les capacités des agents avec des fonctions Rust personnalisées.
Qu'est-ce qu'un outil de fonction ?
Les outils de fonction vous permettent de doter les agents de capacités allant au-delà de la conversation - appel d'APIs, exécution de calculs, accès à des bases de données ou toute logique personnalisée. Le LlmAgent décide quand utiliser un outil en fonction de la requête de l'utilisateur.
Points clés :
- 🔧 Encapsulez toute fonction async comme un outil appelable
- 📝 Paramètres JSON - entrée/sortie flexible
- 🎯 Schémas de types sûrs - validation optionnelle du JSON Schema
- 🔗 Accès au contexte - état de session, artefacts, mémoire
Étape 1 : Outil de base
Créez un outil avec FunctionTool::new() et ajoutez toujours un schéma afin que le LlmAgent 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")?;
// Outil météo avec un schéma approprié
let weather_tool = FunctionTool::new(
"get_weather",
"Obtenir la météo actuelle pour un lieu",
|_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>(); // Requis pour que le LlmAgent l'appelle correctement !
let agent = LlmAgentBuilder::new("weather_agent")
.instruction("Vous aidez les utilisateurs à vérifier la météo. Utilisez toujours l'outil get_weather.")
.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 LlmAgent ne saura pas quels paramètres transmettre et pourrait ne pas appeler l'outil.
Comment ça marche :
- L'utilisateur demande : "Quel temps fait-il à Tokyo ?"
- Le LlmAgent décide d'appeler
get_weatheravec{"location": "Tokyo"} - L'outil retourne
{"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} - Le LlmAgent formate la réponse : "Le temps à Tokyo est ensoleillé avec 22°C."
Étape 2 : Gestion des paramètres
Extrayez les paramètres de l'objet JSON args :
let order_tool = FunctionTool::new(
"process_order",
"Traiter une commande. Paramètres : product_id (requis), quantity (requis), priority (optionnel)",
|_ctx, args| async move {
// Paramètres requis - retourner une erreur s'ils sont manquants
let product_id = args.get("product_id")
.and_then(|v| v.as_str())
.ok_or_else(|| adk_core::AdkError::Tool("product_id is required".into()))?;
let quantity = args.get("quantity")
.and_then(|v| v.as_i64())
.ok_or_else(|| adk_core::AdkError::Tool("quantity is required".into()))?;
// Paramètre optionnel avec valeur par défaut
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 Schema
Pour les FunctionTool complexes, utilisez des structs typés avec 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".into())),
};
Ok(json!({ "result": result }))
},
)
.with_parameters_schema::<CalculatorParams>();
Le schema est auto-généré à partir des types Rust en utilisant schemars.
Étape 4 : Agent multi-outils
Ajoutez plusieurs Tool à un 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 Tool en fonction de la demande de l'utilisateur.
Gestion des erreurs
Retournez AdkError::Tool pour les erreurs spécifiques au Tool :
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(|| adk_core::AdkError::Tool("Parameter 'a' is required".into()))?;
let b = args.get("b").and_then(|v| v.as_f64())
.ok_or_else(|| adk_core::AdkError::Tool("Parameter 'b' is required".into()))?;
if b == 0.0 {
return Err(adk_core::AdkError::Tool("Cannot divide by zero".into()));
}
Ok(json!({ "result": a / b }))
},
);
Les messages d'erreur sont transmis au LLM, qui peut réessayer ou demander une entrée différente.
Contexte de Tool
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 d'artefactsctx.search_memory(query)- Service de recherche de mémoire
Outils à longue exécution
Pour les opérations qui prennent beaucoup de temps (traitement de données, APIs externes), utilisez le modèle non bloquant :
- Démarrer l'outil retourne immédiatement un task_id
- Le travail en arrière-plan s'exécute de manière asynchrone
- L'outil de statut 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 retourne un statut en attente- L'outil lance le travail avec
tokio::spawn()et retourne immédiatement - Fournissez un outil de vérification de statut afin que les utilisateurs puissent interroger la progression
Ceci ajoute une note pour empêcher le LlmAgent d'appeler l'outil de manière répétée.
---
## Exemples d'exécution
```bash
cd official_docs_examples/tools/function_tools_test
# Basic tool with closure
cargo run --bin basic
# Tool with typed JSON schema
cargo run --bin with_schema
# Multi-tool agent (3 tools)
cargo run --bin multi_tool
# Tool context (session info)
cargo run --bin context
# Long-running tool
cargo run --bin long_running
Bonnes Pratiques
- Descriptions claires - Aidez le LlmAgent à comprendre quand utiliser l'outil
- Validez les entrées - Retournez des messages d'erreur utiles pour les paramètres manquants
- Retournez du JSON structuré - Utilisez des noms de champs clairs
- Gardez les outils ciblés - Chaque outil devrait faire une chose et la faire bien
- Utilisez des schémas - Pour les outils complexes, définissez des schémas de paramètres
Liés
- Outils Intégrés - Outils pré-construits (GoogleSearch, ExitLoopTool)
- MCP Tools - Intégration du Model Context Protocol
- LlmAgent - Ajout d'outils aux agents
Précédent : ← mistral.rs | Suivant : Outils Intégrés →