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 permettent de donner aux agents des capacités allant au-delà de la conversation : appeler APIs, effectuer des calculs, accéder à des bases de données ou exécuter toute logique personnalisée. L’LLM décide quand utiliser un outil en fonction de la demande de l’utilisateur.

Points clés :

  • 🚀 Macro #[tool] - enregistrement des outils sans code répétitif (recommandé)
  • 🔧 FunctionTool::new() - encapsuler manuellement toute fonction asynchrone
  • 📝 Paramètres JSON - entrées et sorties flexibles
  • 🎯 Schémas garantissant la sécurité des types - JSON Schema automatique à partir des types via schemars
  • 🔗 Accès au contexte - état de session, artefacts, mémoire

Pipeline d’exécution des outils

Rendering architecture…

La manière la plus rapide de créer des outils. La macro utilise 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 directement les outils comme étant en lecture seule, compatibles avec la concurrence ou de longue durée 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) :

AttributEffet
read_onlyis_read_only() → true — l’un des deux signaux requis pour la distribution concurrente de Auto
concurrency_safeis_concurrency_safe() → true — l’un des deux signaux requis pour la distribution concurrente de Auto
long_runningis_long_running() → true — empêche LLM de rappeler un outil en attente

Le #[tool] simple sans attributs conserve les valeurs par défaut (tous les false), de sorte que le code existant n’est pas affecté.


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 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-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(())
}

⚠️ Important : Utilisez toujours .with_parameters_schema<T>() ; sans lui, LLM ne saura pas quels paramètres transmettre et pourrait ne pas appeler l’outil.

Fonctionnement :

  1. L’utilisateur demande : « Quelle est la météo à Tokyo ? »
  2. LLM décide d’appeler get_weather avec {"location": "Tokyo"}
  3. L’outil renvoie {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"}
  4. LLM formate la réponse : « La météo à Tokyo est ensoleillée, avec une température de 22 °C. »

Étape 2 : Gestion des paramètres

Extrayez les paramètres 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"
        }))
    },
);

Étape 3 : Paramètres typés avec un schéma

Pour les outils complexes, utilisez des structures typées 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 même 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 choisit automatiquement l’outil approprié en fonction de la demande de l’utilisateur.


Gestion des erreurs

Renvoyez les erreurs avec le composant Tool pour les échecs spécifiques aux outils :

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 également :

Err(AdkError::tool("Parameter 'a' is required"))

Les messages d’erreur sont transmis à LLM, qui peut réessayer ou demander une autre saisie.


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 de l’utilisateur actuel
  • ctx.session_id() - ID de la session actuelle
  • ctx.agent_name() - Nom de l’agent
  • ctx.artifacts() - Accès au stockage des artefacts
  • ctx.search_memory(query) - Service de recherche dans la mémoire

Outils de longue durée

Pour les opérations qui prennent un temps important (traitement de données, APIs externe), utilisez le modèle non bloquant :

  1. L’outil de démarrage renvoie immédiatement un task_id
  2. Le travail en arrière-plan s’exécute de manière asynchrone
  3. 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 le résultat
  • Fournissez un outil de vérification de l’état afin que les utilisateurs puissent interroger la progression

Cela ajoute une note pour empêcher LLM d’appeler l’outil de manière répétée.


Diffuser la progression d’un outil

Les outils de longue durée peuvent envoyer une sortie intermédiaire à l’interface pendant leur exécution, afin que l’utilisateur voie en direct la sortie standard d’une commande shell, les journaux d’une compilation 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 — une étiquette pour le fragment : "stdout", "stderr" ou tout canal personnalisé.
  • chunk — le texte à émettre (émettez une ligne à la fois pour une sortie de type terminal).

Comment cela parvient à l’interface. Le framework transmet 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’existe pas de second canal ni d’extraction des journaux — la progression, le texte du modèle et le résultat final de l’outil arrivent tous dans un flux ordonné unique.

Rétrocompatible. La valeur emit_progress par défaut est une opération sans effet ; les outils et exécuteurs existants qui ne diffusent pas de sortie ne sont donc pas affectés. Seuls les outils qui activent cette fonctionnalité émettent de la progression, et seuls les consommateurs qui vérifient tool_progress_stream() l’observent.

La progression est limitée et avec pertes. Un outil peut produire des résultats plus rapidement qu’un client ne les consomme — un journal de compilation, une commande shell, une boucle qui s’emballe — ; le framework limite donc ce qu’il conserve et transmet au lieu de croître sans limite :

LimiteValeurEn cas de dépassement
Profondeur de file par lot d’outils256 événementsL’outil attend jusqu’à 100 ms qu’un espace se libère, puis le fragment est ignoré
Octets par fragment8 KiBLe fragment est tronqué à la limite d’un caractère
Octets par appel d’outil1 MiBLa progression restante n’est pas transmise

Lorsque la sortie est abandonnée pour l’une de ces raisons, exactement un événement de progression contenant le texte [adk: tool progress truncated] est émis pour cet appel, de sorte qu’une lacune est toujours visible plutôt que silencieuse. 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 résultats volumineux dans l’outil si cela vous importe.

Consultez l’exemple streaming_bash pour découvrir une interface web complète qui affiche en direct la sortie bash et les résultats d’outils ponctuels (read_file, grep, glob) à partir d’un flux d’événements unique. L’outil bash de diffusion en continu se trouve lui-même dans adk-devtools.


Exécuter les exemples

cargo adk new tool_agent --template tools
cd tool_agent
cargo run

Bonnes pratiques

  1. Descriptions claires - Aidez LLM à comprendre quand utiliser l’outil
  2. Valider les entrées - Renvoyez des messages d’erreur utiles lorsque des paramètres sont manquants
  3. Renvoyer des JSON structurés - Utilisez des noms de champs clairs
  4. Garder les outils ciblés - Chaque outil doit faire une seule chose correctement
  5. Utiliser des schémas - Pour les outils complexes, définissez des schémas de paramètres
  6. Marquer les outils sûrs et en lecture seule - Définissez à la fois .with_read_only(true) et .with_concurrency_safe(true) afin que Auto puisse les inclure dans son sous-ensemble concurrent

Métadonnées des outils : lecture seule et concurrence

Marquez les outils comme étant en lecture seule ou sûrs pour l’exécution concurrente afin de permettre une distribution plus intelligente :

// 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 distribution exécute d’abord les appels de manière concurrente lorsque les outils sélectionnés renvoient true à la fois depuis is_read_only() et is_concurrency_safe(). Elle exécute ensuite tous les appels restants séquentiellement. ToolExecutionStrategy::Parallel est une substitution explicite qui ignore ces signaux ; son appelant est donc responsable de la sécurité de l’exécution concurrente.


SimpleToolContext : Utiliser des outils en dehors de la boucle de l’agent

Lorsque vous devez appeler un outil en dehors de la boucle de l’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() → vec vide. invocation_id et function_call_id sont tous deux des UUIDs générés automatiquement. Définissez une session réelle avec with_session_id(...) lorsqu’un outil achemine ou conserve un é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 (simple incrémentation 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 construction que FunctionTool : with_long_running, with_parameters_schema, with_response_schema, with_scopes, with_read_only et with_concurrency_safe.



Réponses de fonctions 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 fonctions — et pas uniquement 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 :

  1. Détecte inline_data/file_data via FunctionResponseData::from_tool_result()
  2. Encode en Base64 les données binaires intégrées
  3. Imbrique les parties dans l’objet filaire functionResponse (conformément au format API de Gemini 3)

Références de fichiers

Pour les fichiers volumineux stockés en externe, 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 fonctions multimodales nécessitent des modèles de la série Gemini 3 tels que gemini-3.7-flash. Les modèles antérieurs renvoient une erreur 400.

Consultez examples/multimodal_function_response/ pour obtenir un exemple complet fonctionnel.


Précédent : ← mistral.rs | Suivant : Outils intégrés →