Funktionstools

Erweitern Sie die Agentenfähigkeiten mit benutzerdefinierten Rust-Funktionen.


Was sind Funktionstools?

Funktionstools ermöglichen es Ihnen, Agenten Fähigkeiten zu verleihen, die über die Konversation hinausgehen – das Aufrufen von APIs, das Durchführen von Berechnungen, den Zugriff auf Datenbanken oder jede benutzerdefinierte Logik. Das LLM entscheidet basierend auf der Benutzeranfrage, wann ein Tool verwendet werden soll.

Wichtige Highlights:

  • 🔧 Jede async-Funktion umwandeln in ein aufrufbares Tool
  • 📝 JSON-Parameter – flexible Eingabe/Ausgabe
  • 🎯 Typensichere Schemas – optionale JSON Schema-Validierung
  • 🔗 Kontextzugriff – Sitzungsstatus, Artefakte, Speicher

Schritt 1: Basistool

Erstellen Sie ein Tool mit FunctionTool::new() und fügen Sie immer ein Schema hinzu, damit das LLM weiß, welche Parameter übergeben werden müssen:

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

⚠️ Wichtig: Verwenden Sie immer .with_parameters_schema<T>() – ohne dies weiß das LLM nicht, welche Parameter übergeben werden müssen und ruft das Tool möglicherweise nicht auf.

Wie es funktioniert:

  1. Benutzer fragt: "Wie ist das Wetter in Tokio?"
  2. LLM entscheidet, get_weather mit {"location": "Tokyo"} aufzurufen
  3. Tool gibt {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} zurück
  4. LLM formatiert die Antwort: "Das Wetter in Tokio ist sonnig bei 22°C."

Schritt 2: Parameterbehandlung

Extrahieren Sie Parameter aus den 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".into()))?;
        
        let quantity = args.get("quantity")
            .and_then(|v| v.as_i64())
            .ok_or_else(|| adk_core::AdkError::Tool("quantity is required".into()))?;
        
        // 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"
        }))
    },
);

Schritt 3: Typisierte Parameter mit Schema

Für komplexe Tools verwenden Sie typisierte Structs mit 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>();

Das Schema wird automatisch aus Rust-Typen mithilfe von schemars generiert.


Schritt 4: Multi-Tool Agent

Fügen Sie einem Agenten mehrere Tools hinzu:

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()?;

Das LLM wählt automatisch das richtige Tool basierend auf der Anfrage des Benutzers aus.


Fehlerbehandlung

Geben Sie AdkError::Tool für tool-spezifische Fehler zurück:

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 }))
    },
);

Fehlermeldungen werden an das LLM übergeben, das es erneut versuchen oder nach einer anderen Eingabe fragen kann.


Tool-Kontext

Greifen Sie über ToolContext auf Sitzungsinformationen zu:

#[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>();

Verfügbarer Kontext:

  • ctx.user_id() - Aktuelle Benutzer-ID
  • ctx.session_id() - Aktuelle Sitzungs-ID
  • ctx.agent_name() - Name des Agenten
  • ctx.artifacts() - Zugriff auf Artefakt-Speicher
  • ctx.search_memory(query) - Speicher-Suchdienst

Langlebige Tools

Für Operationen, die erhebliche Zeit in Anspruch nehmen (Datenverarbeitung, externe APIs), verwenden Sie das nicht-blockierende Muster:

  1. Start-Tool kehrt sofort mit einer task_id zurück
  2. Hintergrundarbeit läuft asynchron
  3. Status-Tool ermöglicht es Benutzern, den Fortschritt zu überprüfen
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>();

Wichtige Punkte:

  • .with_long_running(true) teilt dem Agent mit, dass dieses Tool einen ausstehenden Status zurückgibt
  • Das Tool startet Arbeit mit tokio::spawn() und kehrt sofort zurück
  • Stellen Sie ein Statusprüfungs-Tool bereit, damit Benutzer den Fortschritt abfragen können

Dies fügt einen Hinweis hinzu, um zu verhindern, dass das LLM das Tool wiederholt aufruft.


Beispiele ausführen

cd official_docs_examples/tools/function_tools_test

# Grundlegendes Tool mit Closure
cargo run --bin basic

# Tool mit typisiertem JSON-Schema
cargo run --bin with_schema

# Multi-Tool Agent (3 Tools)
cargo run --bin multi_tool

# Tool-Kontext (Sitzungsinformationen)
cargo run --bin context

# Langlaufendes Tool
cargo run --bin long_running

Bewährte Verfahren

  1. Klare Beschreibungen - Helfen Sie dem LLM zu verstehen, wann das Tool verwendet werden soll
  2. Eingaben validieren - Geben Sie hilfreiche Fehlermeldungen für fehlende Parameter zurück
  3. Strukturiertes JSON zurückgeben - Verwenden Sie klare Feldnamen
  4. Tools fokussiert halten - Jedes Tool sollte eine Sache gut machen
  5. Schemas verwenden - Definieren Sie für komplexe Tools Parameterschemata

Verwandt


Vorheriges: ← mistral.rs | Nächstes: Integrierte Tools →