Callbacks

Callbacks in adk-rust bieten Hooks, um das Agentenverhalten an wichtigen Ausführungspunkten zu beobachten, anzupassen und zu steuern. Sie ermöglichen Protokollierung, Schutzmechanismen, Caching, Antwortmodifikation und mehr.

Übersicht

adk-rust unterstützt sechs Callback-Typen, die verschiedene Phasen der Agentenausführung abfangen:

Callback-TypWann ausgeführtAnwendungsfälle
before_agentBevor der Agent mit der Verarbeitung beginntEingabevalidierung, Protokollierung, vorzeitige Beendigung
after_agentNachdem der Agent abgeschlossen hatAntwortmodifikation, Protokollierung, Bereinigung
before_modelVor dem LLM-AufrufAnforderungsmodifikation, Caching, Ratenbegrenzung
after_modelNach der LLM-AntwortAntwortfilterung, Protokollierung, Caching
before_toolVor der Tool-AusführungBerechtigungsprüfungen, Parametervalidierung
after_toolNach der Tool-AusführungErgebnismodifikation, Protokollierung

Callback-Typen

Agenten-Callbacks

Agenten-Callbacks umfassen den gesamten Agenten-Ausführungszyklus.

use adk_rust::prelude::*;
use std::sync::Arc;

// Typensignatur für BeforeAgentCallback
type BeforeAgentCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

// Typensignatur für AfterAgentCallback  
type AfterAgentCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

Modell-Callbacks

Modell-Callbacks fangen LLM-Anfragen und -Antworten ab.

use adk_rust::prelude::*;
use std::sync::Arc;

// BeforeModelResult - steuert, was nach dem Callback geschieht
pub enum BeforeModelResult {
    Continue(LlmRequest),  // Mit (möglicherweise modifizierter) Anfrage fortfahren
    Skip(LlmResponse),     // Modellaufruf überspringen, stattdessen diese Antwort verwenden
}

// BeforeModelCallback - kann Anfrage modifizieren oder Modellaufruf überspringen
type BeforeModelCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>, LlmRequest)
        -> Pin<Box<dyn Future<Output = Result<BeforeModelResult>> + Send>>
    + Send + Sync
>;

// AfterModelCallback - kann die Antwort modifizieren
type AfterModelCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>, LlmResponse)
        -> Pin<Box<dyn Future<Output = Result<Option<LlmResponse>>> + Send>>
    + Send + Sync
>;

Tool-Callbacks

Tool-Callbacks fangen die Tool-Ausführung ab.

use adk_rust::prelude::*;
use std::sync::Arc;

// BeforeToolCallback - kann Tool überspringen, indem Some(Content) zurückgegeben wird
type BeforeToolCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

// AfterToolCallback - kann das Tool-Ergebnis modifizieren
type AfterToolCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

Semantik der Rückgabewerte

Callbacks verwenden unterschiedliche Rückgabewerte, um den Ausführungsfluss zu steuern:

Agent/Tool-Callbacks

RückgabewertEffekt
Ok(None)Setzt die normale Ausführung fort
Ok(Some(content))Überschreibt/überspringt mit bereitgestelltem Inhalt
Err(e)Bricht die Ausführung mit einem Fehler ab

Model-Callbacks

BeforeModelCallback verwendet BeforeModelResult:

RückgabewertEffekt
Ok(BeforeModelResult::Continue(request))Setzt mit der (möglicherweise geänderten) Anfrage fort
Ok(BeforeModelResult::Skip(response))Überspringt den Model-Aufruf, verwendet stattdessen diese Antwort
Err(e)Bricht die Ausführung mit einem Fehler ab

AfterModelCallback verwendet Option<LlmResponse>:

RückgabewertEffekt
Ok(None)Behält die ursprüngliche Antwort bei
Ok(Some(response))Ersetzt durch die modifizierte Antwort
Err(e)Bricht die Ausführung mit einem Fehler ab

Zusammenfassung

  • Vor Agent/Tool-Callbacks: Geben Sie None zurück, um fortzufahren, Some(content), um zu überspringen
  • Vor Model-Callback: Geben Sie Continue(request) zurück, um fortzufahren, Skip(response), um das Model zu umgehen
  • Nach Callbacks: Geben Sie None zurück, um das Original beizubehalten, Some(...), um es zu ersetzen

Hinzufügen von Callbacks zu Agents

Callbacks werden Agents mit dem LlmAgentBuilder hinzugefügt:

use adk_rust::prelude::*;
use std::sync::Arc;

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);

    let agent = LlmAgentBuilder::new("my_agent")
        .model(model)
        .instruction("You are a helpful assistant.")
        // Callback vor Agent hinzufügen
        .before_callback(Box::new(|ctx| {
            Box::pin(async move {
                println!("Agent starting: {}", ctx.agent_name());
                Ok(None) // Ausführung fortsetzen
            })
        }))
        // Callback nach Agent hinzufügen
        .after_callback(Box::new(|ctx| {
            Box::pin(async move {
                println!("Agent completed: {}", ctx.agent_name());
                Ok(None) // Ursprüngliches Ergebnis beibehalten
            })
        }))
        .build()?;

    Ok(())
}

CallbackContext-Schnittstelle

Der CallbackContext-Trait ermöglicht den Zugriff auf den Ausführungskontext:

use adk_rust::prelude::*;

#[async_trait]
pub trait CallbackContext: ReadonlyContext {
    /// Zugriff auf Artefakt-Speicher (falls konfiguriert)
    fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}

// CallbackContext erweitert ReadonlyContext
#[async_trait]
pub trait ReadonlyContext: Send + Sync {
    /// Aktuelle Invocation ID
    fn invocation_id(&self) -> &str;
    
    /// Name des aktuellen Agents
    fn agent_name(&self) -> &str;
    
    /// Benutzer-ID aus der Session
    fn user_id(&self) -> &str;
    
    /// Anwendungsname
    fn app_name(&self) -> &str;
    
    /// Session ID
    fn session_id(&self) -> &str;
    
    /// Aktueller Branch (für Multi-Agent)
    fn branch(&self) -> &str;
    
    /// Der Eingabeinhalt des Benutzers
    fn user_content(&self) -> &Content;
}

Häufige Muster

Logging-Callback

Protokolliert alle Agent-Interaktionen:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("logged_agent")
    .model(model)
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[LOG] Agent '{}' starting", ctx.agent_name());
            println!("[LOG] Session: {}", ctx.session_id());
            println!("[LOG] User: {}", ctx.user_id());
            Ok(None)
        })
    }))
    .after_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[LOG] Agent '{}' completed", ctx.agent_name());
            Ok(None)
        })
    }))
    .build()?;

Eingabe-Guardrails

Blockiert unangemessenen Inhalt vor der Verarbeitung:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("guarded_agent")
    .model(model)
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            // Überprüft die Benutzereingabe auf blockierten Inhalt
            let user_content = ctx.user_content();
            for part in &user_content.parts {
                if let Part::Text { text } = part {
                    if text.to_lowercase().contains("blocked_word") {
                        // Frühzeitig mit einer Ablehnungsnachricht zurückkehren
                        return Ok(Some(Content {
                            role: "model".to_string(),
                            parts: vec![Part::Text {
                                text: "I cannot process that request.".to_string(),
                            }],
                        }));
                    }
                }
            }
            Ok(None) // Normale Ausführung fortsetzen
        })
    }))
    .build()?;

Antwort-Caching (Vor dem Modell)

Cacht LLM-Antworten, um API-Aufrufe zu reduzieren:

use adk_rust::prelude::*;
use std::sync::Arc;
use std::collections::HashMap;
use std::sync::Mutex;

// Einfacher In-Memory-Cache
let cache: Arc<Mutex<HashMap<String, LlmResponse>>> = Arc::new(Mutex::new(HashMap::new()));
let cache_clone = cache.clone();

let agent = LlmAgentBuilder::new("cached_agent")
    .model(model)
    .before_model_callback(Box::new(move |ctx, request| {
        let cache = cache_clone.clone();
        Box::pin(async move {
            // Erstellt einen Cache-Schlüssel aus dem Anfrageinhalt
            let key = format!("{:?}", request.contents);

            // Cache überprüfen
            if let Some(cached) = cache.lock().unwrap().get(&key) {
                println!("[CACHE] Hit for request");
                return Ok(BeforeModelResult::Skip(cached.clone()));
            }

            println!("[CACHE] Miss, calling model");
            Ok(BeforeModelResult::Continue(request)) // Weiter zum Modell
        })
    }))
    .build()?;

Injizieren von multimodalen Inhalten (Vor dem Modell)

Injiziert Bilder oder andere binäre Inhalte in LLM-Anfragen für multimodale Analysen:

use adk_rust::prelude::*;
use adk_rust::artifact::{ArtifactService, LoadRequest};
use std::sync::Arc;

// Artifact-Service mit vorab geladenem Bild
let artifact_service: Arc<dyn ArtifactService> = /* ... */;
let callback_service = artifact_service.clone();

let agent = LlmAgentBuilder::new("image_analyst")
    .model(model)
    .instruction("Describe the image provided by the user.")
    .before_model_callback(Box::new(move |_ctx, mut request| {
        let service = callback_service.clone();
        Box::pin(async move {
            // Bild aus dem Artifact-Speicher laden
            if let Ok(response) = service.load(LoadRequest {
                app_name: "my_app".to_string(),
                user_id: "user".to_string(),
                session_id: "session".to_string(),
                file_name: "user:photo.png".to_string(),
                version: None,
            }).await {
                // Bild in die Nachricht des Benutzers injizieren
                if let Some(last_content) = request.contents.last_mut() {
                    if last_content.role == "user" {
                        last_content.parts.push(response.part);
                    }
                }
            }

            Ok(BeforeModelResult::Continue(request))
        })
    }))
    .build()?;

Dieses Muster ist für multimodale KI unerlässlich, da Werkzeugantworten JSON-Text sind – das Modell kann von Werkzeugen zurückgegebene Bilder nicht „sehen“. Durch das direkte Injizieren des Bildes in die Anfrage empfängt das Modell tatsächliche Bilddaten.

Antwortmodifikation (Nach dem Modell)

Modifiziert oder filtert Modellantworten:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("filtered_agent")
    .model(model)
    .after_model_callback(Box::new(|ctx, mut response| {
        Box::pin(async move {
            // Den Antwortinhalt modifizieren
            if let Some(ref mut content) = response.content {
                for part in &mut content.parts {
                    if let Part::Text { text } = part {
                        // Haftungsausschluss zu allen Antworten hinzufügen
                        *text = format!("{}\n\n[AI-generated response]", text);
                    }
                }
            }
            Ok(Some(response))
        })
    }))
    .build()?;

Werkzeug-Berechtigungsprüfung (Vor dem Werkzeug)

Validiert die Ausführungsberechtigungen des Werkzeugs:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("permission_agent")
    .model(model)
    .tool(Arc::new(GoogleSearchTool::new()))
    .before_tool_callback(Box::new(|ctx| {
        Box::pin(async move {
            // Überprüfen, ob der Benutzer Berechtigungen für Werkzeuge hat
            let user_id = ctx.user_id();
            
            // Beispiel: Bestimmte Benutzer von der Nutzung von Werkzeugen blockieren
            if user_id == "restricted_user" {
                return Ok(Some(Content {
                    role: "function".to_string(),
                    parts: vec![Part::Text {
                        text: "Tool access denied for this user.".to_string(),
                    }],
                }));
            }
            
            Ok(None) // Werkzeugausführung erlauben
        })
    }))
    .build()?;

Werkzeugergebnis-Logging (Nach dem Werkzeug)

Protokolliert alle Werkzeugausführungen:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("tool_logged_agent")
    .model(model)
    .tool(Arc::new(GoogleSearchTool::new()))
    .after_tool_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[TOOL LOG] Tool executed for agent: {}", ctx.agent_name());
            println!("[TOOL LOG] Session: {}", ctx.session_id());
            Ok(None) // Ursprüngliches Ergebnis beibehalten
        })
    }))
    .build()?;

Mehrere Callbacks

Sie können mehrere Callbacks desselben Typs hinzufügen. Sie werden in der angegebenen Reihenfolge ausgeführt:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("multi_callback_agent")
    .model(model)
    // First before callback - logging
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[1] Logging callback");
            Ok(None)
        })
    }))
    // Second before callback - validation
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[2] Validation callback");
            Ok(None)
        })
    }))
    .build()?;

Wenn ein Callback Some(content) zurückgibt, werden nachfolgende Callbacks desselben Typs übersprungen.

Fehlerbehandlung

Callbacks können Fehler zurückgeben, um die Ausführung abzubrechen:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("error_handling_agent")
    .model(model)
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            // Validate something critical
            if ctx.user_id().is_empty() {
                return Err(AdkError::Agent("User ID is required".to_string()));
            }
            Ok(None)
        })
    }))
    .build()?;

Bewährte Praktiken

  1. Halten Sie Callbacks schlank: Vermeiden Sie aufwendige Berechnungen in Callbacks
  2. Behandeln Sie Fehler elegant: Geben Sie aussagekräftige Fehlermeldungen zurück
  3. Verwenden Sie Logging sparsam: Zu viel Logging kann die Leistung beeinträchtigen
  4. Cachen Sie klug: Berücksichtigen Sie Strategien zur Cache-Invalidierung
  5. Testen Sie Callbacks unabhängig: Unit-Tests der Callback-Logik separat durchführen

Verwandt


Vorheriges: ← Zustandsverwaltung | Nächstes: Artefakte →