Graph-Agenten

Erstellen Sie komplexe, zustandsbehaftete Workflows mithilfe der Orchestrierung im LangGraph-Stil mit nativer ADK-Rust-Integration.

Übersicht

GraphAgent ermΓΆglicht es Ihnen, Workflows als gerichtete Graphen mit Knoten und Kanten zu definieren, die Folgendes unterstΓΌtzen:

  • AgentNode: LLM-Agenten als Graphknoten mit benutzerdefinierten Mappern fΓΌr Eingaben und Ausgaben einbinden
  • Zyklische Workflows: Native UnterstΓΌtzung fΓΌr Schleifen und iteratives Schlussfolgern (ReAct-Muster)
  • Bedingtes Routing: Dynamisches Kanten-Routing auf Grundlage des Zustands
  • Zustandsverwaltung: Typisierter Zustand mit Reduzierern (ΓΌberschreiben, anhΓ€ngen, summieren, benutzerdefiniert)
  • Checkpointing: Persistenter Zustand fΓΌr Fehlertoleranz und Human-in-the-Loop
  • Streaming: Mehrere Streaming-Modi (Werte, Aktualisierungen, Nachrichten, Debugging)

Der adk-graph-Crate stellt eine Workflow-Orchestrierung im LangGraph-Stil zum Erstellen komplexer, zustandsbehafteter Agenten-Workflows bereit. Er erweitert das ADK-Rust-Γ–kosystem um graphbasierte Workflow-Funktionen und bewahrt dabei die vollstΓ€ndige KompatibilitΓ€t mit dem Agentensystem von ADK.

Wichtige Vorteile:

  • Visuelles Workflow-Design: Komplexe Logik als intuitive Graphen aus Knoten und Kanten definieren
  • Parallele AusfΓΌhrung: Mehrere Knoten kΓΆnnen gleichzeitig ausgefΓΌhrt werden, um die Leistung zu verbessern
  • Persistenz des Zustands: Integriertes Checkpointing fΓΌr Fehlertoleranz und Human-in-the-Loop
  • LLM-Integration: Native UnterstΓΌtzung fΓΌr das Einbinden von ADK-Agenten als Graphknoten
  • Flexibles Routing: Statische Kanten, bedingtes Routing und dynamische Entscheidungsfindung

Auswahl zwischen den Workflow-Agenten und dem Graphen

ADK-Rust unterstΓΌtzt zwei MΓΆglichkeiten zur Orchestrierung. Keine ersetzt die andere, und beide werden weiter gepflegt.

Sie benΓΆtigenVerwenden SieWarum
Eine feste Reihenfolge von SchrittenSequentialAgentDie Topologie ist die Liste. Es muss nichts deklariert werden.
Mehrere Agenten mit derselben EingabeParallelAgentFan-out ohne zu konfigurierenden Join.
Wiederholen, bis eine Bedingung erfΓΌllt istLoopAgentDie Abbruchbedingung ist ein Callback, keine Kante.
Ein zur Laufzeit ausgewΓ€hlter ZweigGraphBedingte Kanten oder ein Knoten, der seinen eigenen Nachfolger benennt.
Zyklen mit einem SchrittbudgetGraphrecursion_limit begrenzt die Super-Schritte.
Eine Pause, die eine Person spΓ€ter beantwortetGraphInterrupts erstellen einen Checkpoint fΓΌr die AusfΓΌhrung und setzen sie fort.
Überleben nach einem Neustart des ProzessesGraphSqliteCheckpointer bleibt bei jedem Super-Schritt erhalten.
ZurΓΌckspulen zu einem frΓΌheren SchrittGraphEine Zeitreise erstellt einen Fork eines Checkpoints.

Bevorzugen Sie Workflow-Agenten, wenn die Struktur der Arbeit bekannt und linear ist: Sie sind kΓΌrzer zu schreiben, und es muss kein Zustandsschema gepflegt werden. Verwenden Sie den Graphen, wenn der Kontrollfluss von Ergebnissen abhΓ€ngt oder wenn ein Lauf den Prozess ΓΌberdauern muss.

Sie lassen sich kombinieren

Die drei Workflow-Agenten implementieren Agent, und AgentNode umschließt jedes Agent, sodass ein Workflow-Agent ein Graphknoten ist:

use adk_agent::SequentialAgent;
use adk_graph::node::AgentNode;
use std::sync::Arc;

let pipeline = Arc::new(SequentialAgent::new("pipeline", vec![extract, validate]));
let node = AgentNode::new(pipeline as Arc<dyn adk_core::Agent>);
// `node` now goes into a StateGraph like any other node.

GraphAgent implementiert ebenfalls Agent, daher gilt auch die Umkehrung: Ein Graph kann ein Sub-Agent eines SequentialAgent sein. Verwenden Sie den Graphen fΓΌr den Teil, der Verzweigungen oder Dauerhaftigkeit benΓΆtigt, und die Workflow-Agenten fΓΌr die Teile, die dies nicht tun.

Was Sie erstellen werden

In diesem Leitfaden erstellen Sie eine Textverarbeitungspipeline, die Übersetzung und Zusammenfassung parallel ausführt:

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       User Input       β”‚                     β”‚
      ────────────────▢ β”‚       START         β”‚
                        β”‚                     β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚                               β”‚
                   β–Ό                               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   TRANSLATOR     β”‚            β”‚   SUMMARIZER     β”‚
        β”‚                  β”‚            β”‚                  β”‚
        β”‚  πŸ‡«πŸ‡· French       β”‚            β”‚  πŸ“ One sentence β”‚
        β”‚     Translation  β”‚            β”‚     Summary      β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚                               β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚      COMBINE        β”‚
                        β”‚                     β”‚
                        β”‚  πŸ“‹ Merge Results   β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                                   β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚        END          β”‚
                        β”‚                     β”‚
                        β”‚   βœ… Complete       β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

SchlΓΌsselkonzepte:

  • Knoten – Verarbeitungseinheiten, die Arbeit ausfΓΌhren (LLM-Agenten, Funktionen oder benutzerdefinierte Logik)
  • Kanten – Kontrollfluss zwischen Knoten (statische Verbindungen oder bedingtes Routing)
  • Zustand – Gemeinsam genutzte Daten, die durch den Graphen fließen und zwischen Knoten erhalten bleiben
  • Parallele AusfΓΌhrung – Mehrere Knoten kΓΆnnen gleichzeitig ausgefΓΌhrt werden, um die Leistung zu verbessern

Die Kernkomponenten verstehen

πŸ”§ Knoten: Die Arbeiter In den Knoten findet die eigentliche Arbeit statt. Jeder Knoten kann:

  • AgentNode: Einen LLM-Agenten umschließen, um natΓΌrliche Sprache zu verarbeiten
  • Funktionsknoten: Benutzerdefinierten Rust-Code zur Datenverarbeitung ausfΓΌhren
  • Integrierte Knoten: Vordefinierte Logik wie ZΓ€hler oder Validatoren verwenden

Stellen Sie sich Knoten als spezialisierte Arbeiter in einer Fertigungsstraße vor – jeder hat eine bestimmte Aufgabe und Fachkenntnis.

πŸ”€ Kanten: Die Ablaufsteuerung
Kanten bestimmen, wie die AusfΓΌhrung durch Ihren Graphen verlΓ€uft:

  • Statische Kanten: Direkte Verbindungen (A β†’ B β†’ C)
  • Bedingte Kanten: Dynamisches Routing basierend auf dem Zustand (if sentiment == "positive" β†’ positive_handler)
  • Parallele Kanten: Mehrere Pfade von einem Knoten (START β†’ [translator, summarizer])

Kanten sind wie Verkehrssignale und Verkehrsschilder, die den Arbeitsablauf steuern.

πŸ’Ύ Zustand: Der gemeinsame Speicher
Der Zustand ist ein SchlΓΌssel-Wert-Speicher, aus dem alle Knoten lesen und in den sie schreiben kΓΆnnen:

  • Eingabedaten: Anfangsinformationen, die dem Graphen zugefΓΌhrt werden
  • Zwischenergebnisse: Die Ausgabe eines Knotens wird zur Eingabe eines anderen
  • Endausgabe: Das abgeschlossene Ergebnis nach der gesamten Verarbeitung

Der Zustand fungiert wie ein gemeinsames Whiteboard, auf dem Knoten Informationen hinterlassen kΓΆnnen, die andere verwenden.

⚑ Parallele Ausführung: Der Geschwindigkeitsboost
Wenn mehrere Kanten von einem Knoten ausgehen, werden die Zielknoten gleichzeitig ausgefΓΌhrt:

  • Schnellere Verarbeitung: UnabhΓ€ngige Aufgaben werden gleichzeitig ausgefΓΌhrt
  • Ressourceneffizienz: Bessere Auslastung von CPU und E/A
  • Skalierbarkeit: Komplexere Workflows ohne lineare Verlangsamung bewΓ€ltigen

Das ist so, als wΓΌrden mehrere Mitarbeiter gleichzeitig verschiedene Teile einer Aufgabe bearbeiten, anstatt in einer Warteschlange zu warten.


Schnellstart

1. Erstellen Sie Ihr Projekt

cargo new graph_demo
cd graph_demo

FΓΌgen Sie AbhΓ€ngigkeiten zu Cargo.toml hinzu:

[dependencies]
adk-graph = { version = "2.1.0", features = ["sqlite"] }
adk-agent = "2.1.0"
adk-model = "2.1.0"
adk-core = "2.1.0"
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"
serde_json = "1.0"

Erstellen Sie .env mit Ihrem API-SchlΓΌssel:

echo 'GOOGLE_API_KEY=your-api-key' > .env

2. Beispiel fΓΌr parallele Verarbeitung

Hier ist ein vollstΓ€ndiges, funktionsfΓ€higes Beispiel, das Text parallel verarbeitet:

use adk_agent::LlmAgentBuilder;
use adk_graph::{
    agent::GraphAgent,
    edge::{END, START},
    node::{AgentNode, ExecutionConfig, NodeOutput},
    state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = Arc::new(GeminiModel::new(&api_key, "gemini-3.7-flash")?);

    // Create specialized LLM agents
    let translator_agent = Arc::new(
        LlmAgentBuilder::new("translator")
            .description("Translates text to French")
            .model(model.clone())
            .instruction("Translate the input text to French. Only output the translation.")
            .build()?,
    );

    let summarizer_agent = Arc::new(
        LlmAgentBuilder::new("summarizer")
            .description("Summarizes text")
            .model(model.clone())
            .instruction("Summarize the input text in one sentence.")
            .build()?,
    );

    // Wrap agents as graph nodes with input/output mappers
    let translator_node = AgentNode::new(translator_agent)
        .with_input_mapper(|state| {
            let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
            adk_core::Content::new("user").with_text(text)
        })
        .with_output_mapper(|events| {
            let mut updates = std::collections::HashMap::new();
            for event in events {
                if let Some(content) = event.content() {
                    let text: String = content.parts.iter()
                        .filter_map(|p| p.text())
                        .collect::<Vec<_>>()
                        .join("");
                    if !text.is_empty() {
                        updates.insert("translation".to_string(), json!(text));
                    }
                }
            }
            updates
        });

    let summarizer_node = AgentNode::new(summarizer_agent)
        .with_input_mapper(|state| {
            let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
            adk_core::Content::new("user").with_text(text)
        })
        .with_output_mapper(|events| {
            let mut updates = std::collections::HashMap::new();
            for event in events {
                if let Some(content) = event.content() {
                    let text: String = content.parts.iter()
                        .filter_map(|p| p.text())
                        .collect::<Vec<_>>()
                        .join("");
                    if !text.is_empty() {
                        updates.insert("summary".to_string(), json!(text));
                    }
                }
            }
            updates
        });

    // Build the graph with parallel execution
    let agent = GraphAgent::builder("text_processor")
        .description("Processes text with translation and summarization in parallel")
        .channels(&["input", "translation", "summary", "result"])
        .node(translator_node)
        .node(summarizer_node)
        .node_fn("combine", |ctx| async move {
            let translation = ctx.get("translation").and_then(|v| v.as_str()).unwrap_or("N/A");
            let summary = ctx.get("summary").and_then(|v| v.as_str()).unwrap_or("N/A");

            let result = format!(
                "=== Processing Complete ===\n\n\
                French Translation:\n{}\n\n\
                Summary:\n{}",
                translation, summary
            );

            Ok(NodeOutput::new().with_update("result", json!(result)))
        })
        // Parallel execution: both nodes start simultaneously
        .edge(START, "translator")
        .edge(START, "summarizer")
        .edge("translator", "combine")
        .edge("summarizer", "combine")
        .edge("combine", END)
        .build()?;

    // Execute the graph
    let mut input = State::new();
    input.insert("input".to_string(), json!("AI is transforming how we work and live."));

    let result = agent.invoke(input, ExecutionConfig::new("thread-1")).await?;
    println!("{}", result.get("result").and_then(|v| v.as_str()).unwrap_or(""));

    Ok(())
}

Beispielausgabe:

=== Processing Complete ===

French Translation:
L'IA transforme notre faΓ§on de travailler et de vivre.

Summary:
AI is revolutionizing work and daily life through technological transformation.

Funktionsweise der GraphausfΓΌhrung

Das große Ganze

Graphagenten werden in Super-Schritten ausgefΓΌhrt – alle bereiten Knoten laufen parallel, anschließend wartet der Graph, bis alle abgeschlossen sind, bevor der nΓ€chste Schritt beginnt:

Step 1: START ──┬──▢ translator (running)
                └──▢ summarizer (running)
                
                ⏳ Wait for both to complete...
                
Step 2: translator ──┬──▢ combine (running)
        summarizer β”€β”€β”˜
        
                ⏳ Wait for combine to complete...
                
Step 3: combine ──▢ END βœ…

Zustandsfluss durch Knoten

Jeder Knoten kann den gemeinsamen Zustand lesen und in ihn schreiben:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ STEP 1: Initial state                                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                     β”‚
β”‚   State: { "input": "AI is transforming how we work" }             β”‚
β”‚                                                                     β”‚
β”‚                              ↓                                      β”‚
β”‚                                                                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚   β”‚   translator     β”‚              β”‚   summarizer     β”‚           β”‚
β”‚   β”‚  reads "input"   β”‚              β”‚  reads "input"   β”‚           β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ STEP 2: After parallel execution                                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                     β”‚
β”‚   State: {                                                          β”‚
β”‚     "input": "AI is transforming how we work",                     β”‚
β”‚     "translation": "L'IA transforme notre faΓ§on de travailler",    β”‚
β”‚     "summary": "AI is revolutionizing work through technology"     β”‚
β”‚   }                                                                 β”‚
β”‚                                                                     β”‚
β”‚                              ↓                                      β”‚
β”‚                                                                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                         β”‚
β”‚   β”‚           combine                    β”‚                         β”‚
β”‚   β”‚  reads "translation" + "summary"     β”‚                         β”‚
β”‚   β”‚  writes "result"                     β”‚                         β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β”‚
β”‚                                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ STEP 3: Final state                                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                     β”‚
β”‚   State: {                                                          β”‚
β”‚     "input": "AI is transforming how we work",                     β”‚
β”‚     "translation": "L'IA transforme notre faΓ§on de travailler",    β”‚
β”‚     "summary": "AI is revolutionizing work through technology",    β”‚
β”‚     "result": "=== Processing Complete ===\n\nFrench..."          β”‚
β”‚   }                                                                 β”‚
β”‚                                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Was es funktionieren lΓ€sst

KomponenteRolle
AgentNodeUmschließt LLM-Agenten mit Eingabe-/Ausgabezuordnungen
input_mapperTransformiert den Zustand β†’ Agenteneingabe Content
output_mapperTransformiert Agentenereignisse β†’ Zustandsaktualisierungen
channelsDeklariert Zustandsfelder, die der Graph verwenden wird
edge()Definiert den AusfΓΌhrungsfluss zwischen Knoten
ExecutionConfigStellt die Thread-ID fΓΌr das Checkpointing bereit

Bedingtes Routing mit LLM-Klassifizierung

Erstellen Sie intelligente Routing-Systeme, in denen LLMs den AusfΓΌhrungspfad bestimmen:

Visualisierung: Sentiment-basiertes Routing

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       User Feedback    β”‚                     β”‚
      ────────────────▢ β”‚    CLASSIFIER       β”‚
                        β”‚  🧠 Analyze tone    β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚               β”‚               β”‚
                   β–Ό               β–Ό               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   POSITIVE       β”‚ β”‚    NEGATIVE      β”‚ β”‚    NEUTRAL       β”‚
        β”‚                  β”‚ β”‚                  β”‚ β”‚                  β”‚
        β”‚  😊 Thank you!   β”‚ β”‚  πŸ˜” Apologize    β”‚ β”‚  😐 Ask more     β”‚
        β”‚     Celebrate    β”‚ β”‚     Help fix     β”‚ β”‚     questions    β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

VollstΓ€ndiger Beispielcode

use adk_agent::LlmAgentBuilder;
use adk_graph::{
    edge::{END, Router, START},
    graph::StateGraph,
    node::{AgentNode, ExecutionConfig},
    state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = Arc::new(GeminiModel::new(&api_key, "gemini-3.7-flash")?);

    // Create classifier agent
    let classifier_agent = Arc::new(
        LlmAgentBuilder::new("classifier")
            .description("Classifies text sentiment")
            .model(model.clone())
            .instruction(
                "You are a sentiment classifier. Analyze the input text and respond with \
                ONLY one word: 'positive', 'negative', or 'neutral'. Nothing else.",
            )
            .build()?,
    );

    // Create response agents for each sentiment
    let positive_agent = Arc::new(
        LlmAgentBuilder::new("positive")
            .description("Handles positive feedback")
            .model(model.clone())
            .instruction(
                "You are a customer success specialist. The customer has positive feedback. \
                Express gratitude, reinforce the positive experience, and suggest ways to \
                share their experience. Be warm and appreciative. Keep response under 3 sentences.",
            )
            .build()?,
    );

    let negative_agent = Arc::new(
        LlmAgentBuilder::new("negative")
            .description("Handles negative feedback")
            .model(model.clone())
            .instruction(
                "You are a customer support specialist. The customer has a complaint. \
                Acknowledge their frustration, apologize sincerely, and offer help. \
                Be empathetic. Keep response under 3 sentences.",
            )
            .build()?,
    );

    let neutral_agent = Arc::new(
        LlmAgentBuilder::new("neutral")
            .description("Handles neutral feedback")
            .model(model.clone())
            .instruction(
                "You are a customer service representative. The customer has neutral feedback. \
                Ask clarifying questions to better understand their needs. Be helpful and curious. \
                Keep response under 3 sentences.",
            )
            .build()?,
    );

    // Create AgentNodes with mappers
    let classifier_node = AgentNode::new(classifier_agent)
        .with_input_mapper(|state| {
            let text = state.get("feedback").and_then(|v| v.as_str()).unwrap_or("");
            adk_core::Content::new("user").with_text(text)
        })
        .with_output_mapper(|events| {
            let mut updates = std::collections::HashMap::new();
            for event in events {
                if let Some(content) = event.content() {
                    let text: String = content.parts.iter()
                        .filter_map(|p| p.text())
                        .collect::<Vec<_>>()
                        .join("")
                        .to_lowercase()
                        .trim()
                        .to_string();
                    
                    let sentiment = if text.contains("positive") { "positive" }
                        else if text.contains("negative") { "negative" }
                        else { "neutral" };
                    
                    updates.insert("sentiment".to_string(), json!(sentiment));
                }
            }
            updates
        });

    // Response nodes (similar pattern for each)
    let positive_node = AgentNode::new(positive_agent)
        .with_input_mapper(|state| {
            let text = state.get("feedback").and_then(|v| v.as_str()).unwrap_or("");
            adk_core::Content::new("user").with_text(text)
        })
        .with_output_mapper(|events| {
            let mut updates = std::collections::HashMap::new();
            for event in events {
                if let Some(content) = event.content() {
                    let text: String = content.parts.iter()
                        .filter_map(|p| p.text())
                        .collect::<Vec<_>>()
                        .join("");
                    updates.insert("response".to_string(), json!(text));
                }
            }
            updates
        });

    // Build graph with conditional routing
    let graph = StateGraph::with_channels(&["feedback", "sentiment", "response"])
        .add_node(classifier_node)
        .add_node(positive_node)
        // ... add negative_node and neutral_node similarly
        .add_edge(START, "classifier")
        .add_conditional_edges(
            "classifier",
            Router::by_field("sentiment"),  // Route based on sentiment field
            [
                ("positive", "positive"),
                ("negative", "negative"),
                ("neutral", "neutral"),
            ],
        )
        .add_edge("positive", END)
        .add_edge("negative", END)
        .add_edge("neutral", END)
        .compile()?;

    // Test with different feedback
    let mut input = State::new();
    input.insert("feedback".to_string(), json!("Your product is amazing! I love it!"));

    let result = graph.invoke(input, ExecutionConfig::new("feedback-1")).await?;
    println!("Sentiment: {}", result.get("sentiment").and_then(|v| v.as_str()).unwrap_or(""));
    println!("Response: {}", result.get("response").and_then(|v| v.as_str()).unwrap_or(""));

    Ok(())
}

Beispielablauf:

Input: "Your product is amazing! I love it!"
       ↓
Classifier: "positive"
       ↓
Positive Agent: "Thank you so much for the wonderful feedback! 
                We're thrilled you love our product. 
                Would you consider leaving a review to help others?"

ReAct-Muster: Schlussfolgern + Handeln

Erstellen Sie Agents, die Tools iterativ verwenden kΓΆnnen, um komplexe Probleme zu lΓΆsen:

Visualisierung: ReAct-Zyklus

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       User Question    β”‚                     β”‚
      ────────────────▢ β”‚      REASONER       β”‚
                        β”‚  🧠 Think + Act     β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                                   β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚   Has tool calls?   β”‚
                        β”‚                     β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚                               β”‚
                   β–Ό                               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚       YES        β”‚            β”‚        NO        β”‚
        β”‚                  β”‚            β”‚                  β”‚
        β”‚  πŸ”„ Loop back    β”‚            β”‚  βœ… Final answer β”‚
        β”‚     to reasoner  β”‚            β”‚      END         β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  └─────────────────┐
                                    β”‚
                                    β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚      REASONER       β”‚
                        β”‚  🧠 Think + Act     β”‚
                        β”‚   (next iteration)  β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

VollstΓ€ndiges ReAct-Beispiel

use adk_agent::LlmAgentBuilder;
use adk_core::{Part, Tool};
use adk_graph::{
    edge::{END, START},
    graph::StateGraph,
    node::{AgentNode, ExecutionConfig, NodeOutput},
    state::State,
};
use adk_model::GeminiModel;
use adk_tool::FunctionTool;
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = Arc::new(GeminiModel::new(&api_key, "gemini-3.7-flash")?);

    // Create tools
    let weather_tool = Arc::new(FunctionTool::new(
        "get_weather",
        "Get the current weather for a location. Takes a 'location' parameter (city name).",
        |_ctx, args| async move {
            let location = args.get("location").and_then(|v| v.as_str()).unwrap_or("unknown");
            Ok(json!({
                "location": location,
                "temperature": "72Β°F",
                "condition": "Sunny",
                "humidity": "45%"
            }))
        },
    )) as Arc<dyn Tool>;

    let calculator_tool = Arc::new(FunctionTool::new(
        "calculator",
        "Perform mathematical calculations. Takes an 'expression' parameter (string).",
        |_ctx, args| async move {
            let expr = args.get("expression").and_then(|v| v.as_str()).unwrap_or("0");
            let result = match expr {
                "2 + 2" => "4",
                "10 * 5" => "50",
                "100 / 4" => "25",
                "15 - 7" => "8",
                _ => "Unable to evaluate",
            };
            Ok(json!({ "result": result, "expression": expr }))
        },
    )) as Arc<dyn Tool>;

    // Create reasoner agent with tools
    let reasoner_agent = Arc::new(
        LlmAgentBuilder::new("reasoner")
            .description("Reasoning agent with tools")
            .model(model.clone())
            .instruction(
                "You are a helpful assistant with access to tools. Use tools when needed to answer questions. \
                When you have enough information, provide a final answer without using more tools.",
            )
            .tool(weather_tool)
            .tool(calculator_tool)
            .build()?,
    );

    // Create reasoner node that detects tool usage
    let reasoner_node = AgentNode::new(reasoner_agent)
        .with_input_mapper(|state| {
            let question = state.get("question").and_then(|v| v.as_str()).unwrap_or("");
            adk_core::Content::new("user").with_text(question)
        })
        .with_output_mapper(|events| {
            let mut updates = std::collections::HashMap::new();
            let mut has_tool_calls = false;
            let mut response = String::new();

            for event in events {
                if let Some(content) = event.content() {
                    for part in &content.parts {
                        match part {
                            Part::FunctionCall { .. } => {
                                has_tool_calls = true;
                            }
                            Part::Text { text } => {
                                response.push_str(text);
                            }
                            _ => {}
                        }
                    }
                }
            }

            updates.insert("has_tool_calls".to_string(), json!(has_tool_calls));
            updates.insert("response".to_string(), json!(response));
            updates
        });

    // Build ReAct graph with cycle
    let graph = StateGraph::with_channels(&["question", "has_tool_calls", "response", "iteration"])
        .add_node(reasoner_node)
        .add_node_fn("counter", |ctx| async move {
            let i = ctx.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
            Ok(NodeOutput::new().with_update("iteration", json!(i + 1)))
        })
        .add_edge(START, "counter")
        .add_edge("counter", "reasoner")
        .add_conditional_edges(
            "reasoner",
            |state| {
                let has_tools = state.get("has_tool_calls").and_then(|v| v.as_bool()).unwrap_or(false);
                let iteration = state.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);

                // Safety limit
                if iteration >= 5 { return END.to_string(); }

                if has_tools {
                    "counter".to_string()  // Loop back for more reasoning
                } else {
                    END.to_string()  // Done - final answer
                }
            },
            [("counter", "counter"), (END, END)],
        )
        .compile()?
        .with_recursion_limit(10);

    // Test the ReAct agent
    let mut input = State::new();
    input.insert("question".to_string(), json!("What's the weather in Paris and what's 15 + 25?"));

    let result = graph.invoke(input, ExecutionConfig::new("react-1")).await?;
    println!("Final answer: {}", result.get("response").and_then(|v| v.as_str()).unwrap_or(""));
    println!("Iterations: {}", result.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0));

    Ok(())
}

Beispielablauf:

Question: "What's the weather in Paris and what's 15 + 25?"

Iteration 1:
  Reasoner: "I need to get weather info and do math"
  β†’ Calls get_weather(location="Paris") and calculator(expression="15 + 25")
  β†’ has_tool_calls = true β†’ Loop back

Iteration 2:
  Reasoner: "Based on the results: Paris is 72Β°F and sunny, 15 + 25 = 40"
  β†’ No tool calls β†’ has_tool_calls = false β†’ END

Final Answer: "The weather in Paris is 72Β°F and sunny with 45% humidity. 
              And 15 + 25 equals 40."

AgentNode

Verpackt jedes ADK Agent (typischerweise LlmAgent) als Graphknoten:

Was der Agent sieht

Ein Agent innerhalb eines Graphen wird unter einem Kontext ausgeführt, der aus dem Aufruf abgeleitet ist, durch den der Graph gestartet wurde, und verhÀlt sich daher genauso wie außerhalb eines Graphen. Wenn ein Runner ein GraphAgent aufruft, werden die IdentitÀt und Services des Aufrufers automatisch weitergegeben:

WeitergegebenHinweis
app_name, user_id, session_idDie des Aufrufers, keine synthetische
Bereiche und Request-MetadatenDamit BereichsprΓΌfungen die Berechtigungen des Aufrufers sehen
Geheimnisdienst, Speicher, Artefakte, gemeinsamer ZustandGenau wie außerhalb des Graphen verfügbar
AbbruchRunner::interrupt erreicht einen als Knoten ausgefΓΌhrten Agenten
RunConfigVom Aufrufer geerbt
branchAbgeleitet, als {caller_branch}.{agent_name}, sodass die Ereignisse eines Knotens zugeordnet werden kΓΆnnen

Ein direkt aufgerufener Graph – graph.invoke(state, ExecutionConfig::new("thread")) –
hat keine Invocation, die er erben kann. Das ist der Standalone-Modus: Der Knoten erhÀlt user_id = "graph_user", app_name = "graph_app", den Branch main, keine Secrets und keinen Speicher. Dieser Modus ist dafür gedacht, einen Graphen außerhalb einer Runner auszuführen, und kein Fallback, auf den man in der Produktion zurückgreifen sollte.

Um manuell eine Verbindung herzustellen – beispielsweise wenn ein Graph ΓΌber einen eigenen Executor gesteuert wird –, ΓΌbergeben Sie die Invocation explizit:

let config = ExecutionConfig::new(ctx.session_id()).with_parent_context(ctx.clone());

Hinweis: Der Knoten lΓ€uft weiterhin in seiner eigenen In-Memory-Graph-Session, daher ist der GesprΓ€chsverlauf des Agenten innerhalb eines Knotens auf diesen Knoten beschrΓ€nkt, statt an die Session des Aufrufers angehΓ€ngt zu werden.

let node = AgentNode::new(llm_agent)
    .with_input_mapper(|state| {
        // Transform graph state to agent input Content
        let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
        adk_core::Content::new("user").with_text(text)
    })
    .with_output_mapper(|events| {
        // Transform agent events to state updates
        let mut updates = std::collections::HashMap::new();
        for event in events {
            if let Some(content) = event.content() {
                let text: String = content.parts.iter()
                    .filter_map(|p| p.text())
                    .collect::<Vec<_>>()
                    .join("");
                updates.insert("output".to_string(), json!(text));
            }
        }
        updates
    });

Funktionsknoten

Einfache asynchrone Funktionen, die den Zustand verarbeiten:

.node_fn("process", |ctx| async move {
    let input = ctx.state.get("input").unwrap();
    let output = process_data(input).await?;
    Ok(NodeOutput::new().with_update("output", output))
})

Kantentypen

Statische Kanten

Direkte Verbindungen zwischen Knoten:

.edge(START, "first_node")
.edge("first_node", "second_node")
.edge("second_node", END)

Bedingte Kanten

Dynamisches Routing basierend auf dem Zustand:

.conditional_edge(
    "router",
    |state| {
        match state.get("next").and_then(|v| v.as_str()) {
            Some("research") => "research_node".to_string(),
            Some("write") => "write_node".to_string(),
            _ => END.to_string(),
        }
    },
    [
        ("research_node", "research_node"),
        ("write_node", "write_node"),
        (END, END),
    ],
)

Routing innerhalb eines Knotens

Eine bedingte Kante legt ihre Ziele beim Erstellen des Graphen fest. NodeOutput::with_goto tut dies nicht: Ein Knoten schreibt den Zustand und benennt seine Nachfolger im selben Schritt, und er kann jeden Knoten im Graphen benennen, einschließlich eines Knotens, zu dem keine Kante von ihm führt.

use adk_graph::node::NodeOutput;
use serde_json::json;

// The node decides where control goes, from what it just computed.
async fn triage(ctx: &adk_graph::node::NodeContext) -> adk_graph::error::Result<NodeOutput> {
    let amount = ctx.get("amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
    let next = if amount > 10_000.0 { "escalate" } else { "auto_approve" };
    Ok(NodeOutput::new().with_update("risk", json!(next)).with_goto([next]))
}
VerhaltenRegel
Deklarierte KantenEin Knoten, der einen goto festlegt, folgt nicht zusΓ€tzlich seinen ausgehenden Kanten. Der goto ersetzt sie.
Mehrere ZieleAlle benannten Knoten werden in sortierter Reihenfolge ausgefΓΌhrt.
ENDDas Benennen von END stoppt diesen Zweig.
Ein unbekannter NameDie AusfΓΌhrung schlΓ€gt mit GraphError::UnknownRouteTarget fehl.
Kein gotoDie deklarierten Kanten bestimmen dies; das ist die Standardeinstellung.

Die von einem goto erzeugte Frontier wird wie jede andere als Checkpoint gespeichert, sodass ein pausierter Lauf in dem vom goto ausgewΓ€hlten Knoten fortgesetzt wird.

Hinweis: Verwenden Sie add_conditional_edges, wenn die mΓΆglichen Ziele beim Erstellen des Graphen bekannt sind β€” die Kanten erscheinen dann in einem gerenderten Diagramm. Verwenden Sie ein goto, wenn die Auswahl zum Knoten gehΓΆrt.

Router-Hilfsfunktionen

Verwenden Sie integrierte Router fΓΌr gΓ€ngige Muster:

use adk_graph::edge::Router;

// Route based on a state field value
.conditional_edge("classifier", Router::by_field("sentiment"), [
    ("positive", "positive_handler"),
    ("negative", "negative_handler"),
    ("neutral", "neutral_handler"),
])

// Route based on boolean field
.conditional_edge("check", Router::by_bool("approved"), [
    ("true", "execute"),
    ("false", "reject"),
])

// Limit iterations
.conditional_edge("loop", Router::max_iterations("count", 5), [
    ("continue", "process"),
    ("done", END),
])

Parallele AusfΓΌhrung

Mehrere Kanten von einem einzelnen Knoten werden parallel ausgefΓΌhrt:

let agent = GraphAgent::builder("parallel_processor")
    .channels(&["input", "translation", "summary", "analysis"])
    .node(translator_node)
    .node(summarizer_node)
    .node(analyzer_node)
    .node(combiner_node)
    // All three start simultaneously
    .edge(START, "translator")
    .edge(START, "summarizer")
    .edge(START, "analyzer")
    // Wait for all to complete before combining
    .edge("translator", "combiner")
    .edge("summarizer", "combiner")
    .edge("analyzer", "combiner")
    .edge("combiner", END)
    .build()?;

combiner wird einmal ausgefΓΌhrt, nachdem alle drei Zweige eingetroffen sind. Nichts im obigen Code fordert dies ausdrΓΌcklich an: Ein Knoten mit mehr als einer eingehenden direkten Kante wird beim Kompilieren automatisch zurΓΌckgestellt. Zweige unterschiedlicher LΓ€nge werden daher ohne Konfiguration korrekt zusammengefΓΌhrt.

Aus der Art und Weise, wie die Anzahl ermittelt wird, ergeben sich zwei Details:

FallVerhalten
Bedingte VorgΓ€ngerWerden nicht gezΓ€hlt. Ein bedingter Zweig wird mΓΆglicherweise nie ausgefΓΌhrt, sodass das Warten darauf den Join blockieren kΓΆnnte.
Ein Quorum statt allerSetze min_predecessors auf DeferredNodeConfig und markiere den Knoten mit mark_deferred, um ihn freizugeben, sobald n von m eintreffen.

Zyklische Graphen (ReAct-Muster)

Erstelle iterative Reasoning-Agenten mit Zyklen:

use adk_core::Part;

// Create agent with tools
let reasoner = Arc::new(
    LlmAgentBuilder::new("reasoner")
        .model(model)
        .instruction("Use tools to answer questions. Provide final answer when done.")
        .tool(search_tool)
        .tool(calculator_tool)
        .build()?
);

let reasoner_node = AgentNode::new(reasoner)
    .with_input_mapper(|state| {
        let question = state.get("question").and_then(|v| v.as_str()).unwrap_or("");
        adk_core::Content::new("user").with_text(question)
    })
    .with_output_mapper(|events| {
        let mut updates = std::collections::HashMap::new();
        let mut has_tool_calls = false;
        let mut response = String::new();

        for event in events {
            if let Some(content) = event.content() {
                for part in &content.parts {
                    match part {
                        Part::FunctionCall { name, .. } => {
                            has_tool_calls = true;
                        }
                        Part::Text { text } => {
                            response.push_str(text);
                        }
                        _ => {}
                    }
                }
            }
        }

        updates.insert("has_tool_calls".to_string(), json!(has_tool_calls));
        updates.insert("response".to_string(), json!(response));
        updates
    });

// Build graph with cycle
let react_agent = StateGraph::with_channels(&["question", "has_tool_calls", "response", "iteration"])
    .add_node(reasoner_node)
    .add_node_fn("counter", |ctx| async move {
        let i = ctx.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
        Ok(NodeOutput::new().with_update("iteration", json!(i + 1)))
    })
    .add_edge(START, "counter")
    .add_edge("counter", "reasoner")
    .add_conditional_edges(
        "reasoner",
        |state| {
            let has_tools = state.get("has_tool_calls").and_then(|v| v.as_bool()).unwrap_or(false);
            let iteration = state.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);

            // Safety limit
            if iteration >= 5 { return END.to_string(); }

            if has_tools {
                "counter".to_string()  // Loop back
            } else {
                END.to_string()  // Done
            }
        },
        [("counter", "counter"), (END, END)],
    )
    .compile()?
    .with_recursion_limit(10);

Supervisor fΓΌr mehrere Agenten

Leite Aufgaben an spezialisierte Agenten weiter:

// Create supervisor agent
let supervisor = Arc::new(
    LlmAgentBuilder::new("supervisor")
        .model(model.clone())
        .instruction("Route tasks to: researcher, writer, or coder. Reply with agent name only.")
        .build()?
);

let supervisor_node = AgentNode::new(supervisor)
    .with_output_mapper(|events| {
        let mut updates = std::collections::HashMap::new();
        for event in events {
            if let Some(content) = event.content() {
                let text: String = content.parts.iter()
                    .filter_map(|p| p.text())
                    .collect::<Vec<_>>()
                    .join("")
                    .to_lowercase();

                let next = if text.contains("researcher") { "researcher" }
                    else if text.contains("writer") { "writer" }
                    else if text.contains("coder") { "coder" }
                    else { "done" };

                updates.insert("next_agent".to_string(), json!(next));
            }
        }
        updates
    });

// Build supervisor graph
let graph = StateGraph::with_channels(&["task", "next_agent", "research", "content", "code"])
    .add_node(supervisor_node)
    .add_node(researcher_node)
    .add_node(writer_node)
    .add_node(coder_node)
    .add_edge(START, "supervisor")
    .add_conditional_edges(
        "supervisor",
        Router::by_field("next_agent"),
        [
            ("researcher", "researcher"),
            ("writer", "writer"),
            ("coder", "coder"),
            ("done", END),
        ],
    )
    // Agents report back to supervisor
    .add_edge("researcher", "supervisor")
    .add_edge("writer", "supervisor")
    .add_edge("coder", "supervisor")
    .compile()?;

Zustandsverwaltung

Zustandsschema mit Reducern

Steuere, wie Zustandsaktualisierungen zusammengefΓΌhrt werden:

let schema = StateSchema::builder()
    .channel("current_step")                    // Overwrite (default)
    .list_channel("messages")                   // Append to list
    .channel_with_reducer("count", Reducer::Sum) // Sum values
    .channel_with_reducer("data", Reducer::Custom(Arc::new(|old, new| {
        // Custom merge logic
        merge_json(old, new)
    })))
    .build();

let agent = GraphAgent::builder("stateful")
    .state_schema(schema)
    // ... nodes and edges
    .build()?;

Reducer-Typen

ReducerVerhalten
OverwriteAlten Wert durch neuen ersetzen (Standard)
AppendAn Liste anhΓ€ngen
SumNumerische Werte hinzufΓΌgen
CustomBenutzerdefinierte ZusammenfΓΌhrungsfunktion

Aktualisierungsreihenfolge

Knoten in einem Super-Schritt werden gleichzeitig ausgeführt und schließen ihre Arbeit in der jeweils benâtigten Zeit ab. Ihre Zustandsaktualisierungen werden in Knoten-Namensreihenfolge angewendet, nicht in der Reihenfolge, in der die Knoten fertig werden.

Die Reihenfolge ist relevant, sobald ein Reducer nicht kommutativ ist. Append erstellt ein Array, daher ist die Reihenfolge das Ergebnis; auch ein Custom-Reducer kann von der Reihenfolge abhΓ€ngen. Die Sortierung nach Knotennamen macht eine AusfΓΌhrung reproduzierbar: Derselbe Graph und dieselbe Eingabe liefern unabhΓ€ngig vom Timing einer langsamen AbhΓ€ngigkeit denselben Zustand.

Wenn mehrere KanΓ€le von einem Knoten beschrieben werden, werden sie in Kanal-Namensreihenfolge angewendet. KanΓ€le interagieren nicht miteinander, daher ist dies nur fΓΌr das Lesen eines Traces relevant.

Checkpointing

Aktiviere persistenten Zustand fΓΌr Fehlertoleranz und Human-in-the-Loop:

Im Speicher (Entwicklung)

use adk_graph::checkpoint::MemoryCheckpointer;

let checkpointer = Arc::new(MemoryCheckpointer::new());

let graph = StateGraph::with_channels(&["task", "result"])
    // ... nodes and edges
    .compile()?
    .with_checkpointer_arc(checkpointer.clone());

SQLite (Produktion)

use adk_graph::checkpoint::SqliteCheckpointer;

let checkpointer = SqliteCheckpointer::new("checkpoints.db").await?;

let graph = StateGraph::with_channels(&["task", "result"])
    // ... nodes and edges
    .compile()?
    .with_checkpointer(checkpointer);

Was ein Checkpoint aufzeichnet

Ein Checkpoint speichert den akkumulierten Zustand, die Schrittanzahl und die Frontier β€” die Knoten, die noch ausgefΓΌhrt werden mΓΌssen. Er wird geschrieben, nachdem die Frontier fortgeschritten ist. Dadurch wird beim Fortsetzen niemals ein Knoten erneut ausgefΓΌhrt, der bereits abgeschlossen wurde, und seine Aktualisierungen werden niemals doppelt angewendet. Bei einer abgeschlossenen AusfΓΌhrung wird eine leere Frontier als Checkpoint gespeichert. Dadurch gibt das Fortsetzen eines abgeschlossenen Threads den endgΓΌltigen Zustand zurΓΌck, anstatt den Graphen neu zu starten.

In zwei FΓ€llen wird absichtlich die Frontier gespeichert, die gerade ausgefΓΌhrt wurde, und nicht die nΓ€chste, da der unterbrochene Knoten seine Aktualisierungen noch nicht erzeugt hat und beim Fortsetzen erneut ausgefΓΌhrt werden muss:

SituationGespeicherte Frontier
Super-Schritt abgeschlossenDie als NΓ€chstes auszufΓΌhrenden Knoten
AusfΓΌhrung abgeschlossenLeer
AusgelΓΆste Unterbrechung (blockierend oder Streaming)Die Knoten, die ausgefΓΌhrt wurden

Gestreamte AusfΓΌhrungen werden nach demselben Zeitplan wie blockierende AusfΓΌhrungen mit PrΓΌfpunkten versehen, auch wenn eine Unterbrechung den Stream beendet, sodass eine Pause mit Beteiligung eines Menschen in beiden AusfΓΌhrungsmodi fortgesetzt werden kann.

PrΓΌfpunktverlauf (Zeitreise)

Nur Lesezugriffe. TimeTravelHandle::state_history(from, to) gibt den Zustand zurΓΌck, der bei jedem Schritt mit PrΓΌfpunkt gespeichert wurde. Es wird nichts ausgefΓΌhrt – kein Knoten wird ausgefΓΌhrt, kein Ereignis wird neu erzeugt und kein Seiteneffekt wiederholt. Um die AusfΓΌhrung ab einem Punkt im Verlauf erneut zu starten, verwenden Sie fork_at, um von diesem PrΓΌfpunkt einen Zweig zu erstellen, und rufen Sie den Graphen auf dem abgezweigten Thread auf. Die Methode hieß zuvor replay und wurde als erneute AusfΓΌhrung des Graphen dokumentiert, was sie nie getan hat.

PrΓΌfpunkte ermΓΆglichen außerdem eine dauerhafte Wiederaufnahme – wenn eine GraphausfΓΌhrung abstΓΌrzt oder der Prozess neu gestartet wird, wird die AusfΓΌhrung am letzten persistent gespeicherten PrΓΌfpunkt fortgesetzt, anstatt von vorne zu beginnen. Verwenden Sie SqliteCheckpointer (die Funktion sqlite) fΓΌr absturzsichere Persistenz. MemoryCheckpointer hΓ€lt PrΓΌfpunkte im Prozess, sodass sie einen Neustart nicht ΓΌberleben. Dies sind die beiden Backends, die dieses Crate bereitstellt; implementieren Sie das Trait Checkpointer fΓΌr alle anderen.

// List all checkpoints for a thread
let checkpoints = checkpointer.list("thread-id").await?;
for cp in checkpoints {
    println!("Step {}: {:?}", cp.step, cp.state.get("status"));
}

// Load a specific checkpoint
if let Some(checkpoint) = checkpointer.load_by_id(&checkpoint_id).await? {
    println!("State at step {}: {:?}", checkpoint.step, checkpoint.state);
}

Mensch-in-der-Schleife

Pausieren Sie die AusfΓΌhrung zur menschlichen Genehmigung mithilfe dynamischer Unterbrechungen:

use adk_graph::{error::GraphError, node::NodeOutput};

// Planner agent assesses risk
let planner_node = AgentNode::new(planner_agent)
    .with_output_mapper(|events| {
        let mut updates = std::collections::HashMap::new();
        for event in events {
            if let Some(content) = event.content() {
                let text: String = content.parts.iter()
                    .filter_map(|p| p.text())
                    .collect::<Vec<_>>()
                    .join("");

                // Extract risk level from LLM response
                let risk = if text.to_lowercase().contains("risk: high") { "high" }
                    else if text.to_lowercase().contains("risk: medium") { "medium" }
                    else { "low" };

                updates.insert("plan".to_string(), json!(text));
                updates.insert("risk_level".to_string(), json!(risk));
            }
        }
        updates
    });

// Review node with dynamic interrupt
let graph = StateGraph::with_channels(&["task", "plan", "risk_level", "approved", "result"])
    .add_node(planner_node)
    .add_node(executor_node)
    .add_node_fn("review", |ctx| async move {
        let risk = ctx.get("risk_level").and_then(|v| v.as_str()).unwrap_or("low");
        let approved = ctx.get("approved").and_then(|v| v.as_bool());

        // Already approved - continue
        if approved == Some(true) {
            return Ok(NodeOutput::new());
        }

        // High/medium risk - interrupt for approval
        if risk == "high" || risk == "medium" {
            return Ok(NodeOutput::interrupt_with_data(
                &format!("{} RISK: Human approval required", risk.to_uppercase()),
                json!({
                    "plan": ctx.get("plan"),
                    "risk_level": risk,
                    "action": "Set 'approved' to true to continue"
                })
            ));
        }

        // Low risk - auto-approve
        Ok(NodeOutput::new().with_update("approved", json!(true)))
    })
    .add_edge(START, "planner")
    .add_edge("planner", "review")
    .add_edge("review", "executor")
    .add_edge("executor", END)
    .compile()?
    .with_checkpointer_arc(checkpointer.clone());

// Execute - may pause for approval
let thread_id = "task-001";
let result = graph.invoke(input, ExecutionConfig::new(thread_id)).await;

match result {
    Err(GraphError::Interrupted(interrupt)) => {
        println!("*** EXECUTION PAUSED ***");
        println!("Reason: {}", interrupt.interrupt);
        println!("Plan awaiting approval: {:?}", interrupt.state.get("plan"));

        // Human reviews and approves...

        // Update state with approval
        graph.update_state(thread_id, [("approved".to_string(), json!(true))]).await?;

        // Resume execution
        let final_result = graph.invoke(State::new(), ExecutionConfig::new(thread_id)).await?;
        println!("Final result: {:?}", final_result.get("result"));
    }
    Ok(result) => {
        println!("Completed without interrupt: {:?}", result);
    }
    Err(e) => {
        println!("Error: {}", e);
    }
}

Statische Unterbrechungen

Verwenden Sie interrupt_before oder interrupt_after fΓΌr obligatorische Pausenpunkte:

let graph = StateGraph::with_channels(&["task", "plan", "result"])
    .add_node(planner_node)
    .add_node(executor_node)
    .add_edge(START, "planner")
    .add_edge("planner", "executor")
    .add_edge("executor", END)
    .compile()?
    .with_interrupt_before(&["executor"]);  // Always pause before execution

Gestreamte AusfΓΌhrung

Streamen Sie Ereignisse, wΓ€hrend der Graph ausgefΓΌhrt wird:

use futures::StreamExt;
use adk_graph::stream::StreamMode;

let stream = agent.stream(input, config, StreamMode::Updates);

while let Some(event) = stream.next().await {
    match event? {
        StreamEvent::NodeStart(name) => println!("Starting: {}", name),
        StreamEvent::Updates { node, updates } => {
            println!("{} updated state: {:?}", node, updates);
        }
        StreamEvent::NodeEnd(name) => println!("Completed: {}", name),
        StreamEvent::Done(state) => println!("Final state: {:?}", state),
        _ => {}
    }
}

Streaming-Modi

ModusBeschreibung
ValuesNach jedem Knoten den vollstΓ€ndigen Zustand streamen
UpdatesNur ZustandsΓ€nderungen streamen
MessagesAktualisierungen des Nachrichtentyps streamen
DebugAlle internen Ereignisse streamen

Messages mode liest Token aus Node::execute_stream, sobald sie erzeugt werden. In diesem Modus wird jeder Knoten pro Super-Schritt einmal ausgefΓΌhrt: Der Knoten meldet seine Zustandsaktualisierungen im Stream als StreamEvent::Updates-Ereignis, und der Executor wendet diese an, anstatt den Knoten ein zweites Mal auszufΓΌhren, um sie zu sammeln. Dies ist besonders fΓΌr AgentNode relevant, da eine zweite AusfΓΌhrung einen zweiten abgerechneten Modellaufruf pro Knoten bedeuten wΓΌrde.

Wichtig: Ein benutzerdefiniertes Node, das execute_stream überschreibt, muss ein StreamEvent::Updates-Ereignis liefern, das seine Zustandsaktualisierungen enthÀlt. Andernfalls streamt der Knoten zwar Ereignisse, trÀgt aber im Messages-Modus keinen Zustand bei. Das standardmÀßige execute_stream, das execute umschließt, übernimmt dies für Sie.

Timeout-Richtlinien gelten fΓΌr die gestreamte AusfΓΌhrung selbst. Bei einem Stream bedeutet idle_timeout, dass innerhalb des Limits kein Ereignis erzeugt wurde.

Untergraphen

Ein kompiliertes Diagramm wird ΓΌber SubgraphNode als Knoten eines anderen ausgefΓΌhrt. Der innere Graph behΓ€lt seine eigenen KanΓ€le, Kanten und Unterbrechungspunkte und tauscht benannte KanΓ€le mit seinem ΓΌbergeordneten Graphen aus.

use adk_graph::subgraph::SubgraphNode;
use std::sync::Arc;

let outer = StateGraph::with_channels(&["document", "size"])
    .add_node(
        SubgraphNode::new("measure_doc", Arc::new(inner))
            .with_input("document", "text")
            .with_output("length", "size"),
    )
    .add_edge(START, "measure_doc")
    .add_edge("measure_doc", END)
    .compile()?;
RegelVerhalten
Gemeinsame NamenEin Kanal, den beide Schemata unter demselben Namen deklarieren, wird in beide Richtungen weitergeleitet.
isolated()Nichts wird implizit weitergeleitet; jeder Austausch muss benannt werden. Das lohnt sich, wenn die beiden Graphen getrennt gepflegt werden, da das HinzufΓΌgen eines Kanals zu einem Graphen dann nicht stillschweigend beginnen kann, den anderen zu speisen.
Eine Pause innerhalbPausiert das ΓΌbergeordnete Element und ΓΌbertrΓ€gt den Namen des Teilgraphen sowie die innere Nachricht.
ThreadsDer Teilgraph lΓ€uft auf <parent thread>/<node name>, daher kΓΆnnen zwei Teilgraphen eines ΓΌbergeordneten Elements nicht kollidieren.
Ein falscher KanalnameSchlΓ€gt fehl, wenn das ΓΌbergeordnete Element kompiliert, und nennt den Kanal sowie die Seite.

Die letzte Zeile ist der entscheidende Unterschied: Beide Schemata sind verfΓΌgbar, bevor irgendetwas ausgefΓΌhrt wird. Daher kann eine Zuordnung, die einen Kanal benennt, den keine der beiden Seiten deklariert, keine AusfΓΌhrung erreichen und als fehlender Wert sichtbar werden. Ein Untergraph, der ΓΌberhaupt nichts austauscht, wird aus demselben Grund abgelehnt, da er seinen ΓΌbergeordneten Graphen nicht beeinflussen kΓΆnnte.

Fortsetzen einer Pause innerhalb eines Untergraphen

Es ist nichts ZusΓ€tzliches erforderlich. Wird der ΓΌbergeordnete Graph erneut im selben Thread aufgerufen, wird der Untergraph erneut betreten. Dieser findet seinen eigenen Checkpoint auf <parent thread>/<node name> und setzt die AusfΓΌhrung an der Stelle fort, an der sie angehalten wurde. Arbeit, die der Untergraph vor der Pause abgeschlossen hat, wird nicht wiederholt, und eine Pause mehrere Ebenen tiefer wird auf dieselbe Weise fortgesetzt – die Nachricht nennt jede Ebene, die sie durchlaufen hat.

Ein Untergraph, der ein Unterbrechungstor deklariert, aber keinen Checkpointer besitzt, wird beim Kompilieren des ΓΌbergeordneten Graphen abgelehnt: Er wΓΌrde an seinem ersten Knoten erneut betreten und fΓΌr seine bereits abgeschlossene Arbeit ein zweites Mal bezahlen.

Art der PauseWie die Antwort eintrifft
interrupt_before / interrupt_after innerhalbNichts bereitzustellen; das Fortsetzen hebt die ausgelΓΆste Sperre auf
Ein Knoten, der selbst entscheidetDie Entscheidung trifft als Zustand ein, der ΓΌber die Kanalzuordnung projiziert wird

Da beide Graphen echte Checkpointer enthalten, bleibt dies auch nach einem Neustart des Prozesses erhalten: Ein neuer Satz von Graphobjekten, die sich nur die Datenbanken teilen, setzt denselben Lauf fort.

Übergabe der Kontrolle an den übergeordneten Graphen

Ein Knoten innerhalb eines Subgraphen kann seinen eigenen Graphen beenden und einen Knoten des Graphen benennen, der ihn enthΓ€lt:

Ok(NodeOutput::new()
    .with_update("reason", json!("no confident answer"))
    .with_goto_parent(["escalate"]))

Der Subgraph wird wie gewohnt beendet und projiziert seine AusgabekanÀle, anschließend setzt der übergeordnete Graph bei escalate fort, anstatt den vom Subgraph-Knoten deklarierten Kanten zu folgen. Der übergeordnete Graph validiert das Ziel, da nur er seine eigenen Knoten kennt.

ZuverlΓ€ssigkeits- und Kostenkontrollen

Alle diese Optionen sind standardmÀßig deaktiviert, sodass sich ein Graph so verhÀlt wie zuvor, solange keine davon aktiviert wurde.

Wiederholungen pro Knoten

Ein vorΓΌbergehender Fehler β€” eine Ratenbegrenzung, eine unterbrochene Verbindung β€” beendet andernfalls den Lauf.

use adk_graph::retry::{RetryOn, RetryPolicy};
use std::time::Duration;

let graph = graph.with_node_retry(
    "call_model",
    RetryPolicy::new(3)
        .with_initial_delay(Duration::from_millis(500))
        .with_max_delay(Duration::from_secs(8))
        .with_backoff_factor(2.0)
        .with_retry_on(RetryOn::Any),
);

Die VerzΓΆgerung wΓ€chst um backoff_factor, wird auf max_delay begrenzt und anschließend mit Jitter versehen. Ein Knoten ohne Richtlinie wird einmal ausgefΓΌhrt, sodass Wiederholungen ausdrΓΌcklich aktiviert werden mΓΌssen. Eine Richtlinie aus RetryPolicy::default() erlaubt zehn Versuche, deren neun Wartezeiten sich auf etwa 243 Sekunden summieren β€” senken Sie max_attempts, wenn ein Aufrufer auf die Antwort wartet.

Eine Unterbrechung wird unabhΓ€ngig davon, was retry_on besagt, niemals wiederholt: Eine Pause ist kein Fehler. Die Anzahl der Versuche wird im Checkpoint gespeichert, sodass ein fortgesetzter Lauf das verbleibende Budget verwendet, anstatt es neu zu starten.

Begrenzung der NebenlΓ€ufigkeit

Eine weit aufgefΓ€cherte Verteilung startet ihre gesamte Frontier auf einmal, wodurch ein Verbindungspool erschΓΆpft oder eine Ratenbegrenzung des Anbieters ausgelΓΆst werden kann.

let graph = graph.with_max_concurrency(4);

Knoten jenseits des Limits warten auf einen freien Platz. Die Reihenfolge der Aufnahme entspricht der nach Namen sortierten Frontier und hΓ€ngt daher nicht vom Timing ab. Imperative Aufrufe von untergeordneten Knoten fallen nicht unter dieses Budget, da ein ΓΌbergeordneter Knoten auf seine untergeordneten Knoten wartet, wΓ€hrend er seinen eigenen Platz belegt.

ZeitΓΌberschreitungen pro Knoten

Ein TimeoutPolicy begrenzt einen einzelnen Versuch und legt zusammen mit idle_timeout fest, wie lange ein Knoten ohne Fortschrittsmeldung auskommen darf. Wird einer der beiden Werte ΓΌberschritten, ergibt sich GraphError::NodeTimedOut, worauf eine Wiederholungsrichtlinie reagieren kann.

Direktes Aufrufen eines Knotens

Wenn die Anzahl der Teilaufgaben aus dem Zustand statt aus der Struktur des Graphen stammt, kann ein Knoten selbst einen anderen Knoten aufrufen:

use adk_graph::child::RunNodeOptions;

let output = ctx
    .run_node_with("reviewer", json!({ "aspect": aspect }), RunNodeOptions::with_run_id(aspect))
    .await?;

Das Ziel benΓΆtigt keine Kante. Jedes abgeschlossene Kind wird unter <parent>/<child>@<run_id> aufgezeichnet, sodass ein fortgesetzter Lauf die aufgezeichnete Antwort zurΓΌckgibt, statt das Kind erneut auszufΓΌhren β€” was wichtig ist, wenn das Kind einen Modellaufruf verursacht.

Knotencaching

cache_policy fΓΌr einen Knoten versieht dessen Ergebnis anhand des Knotennamens und des aktuellen Zustands mit einem SchlΓΌssel, optional mit einem TTL, sodass unverΓ€nderte Eingaben die Arbeit ΓΌberspringen. Erfordert das Feature node-cache; ein von Redis unterstΓΌtzter Speicher ist ΓΌber redis-cache verfΓΌgbar.

Delta-Checkpoints

Das Feature delta speichert die Differenz zwischen Super-Schritten statt des gesamten Zustands, was wichtig ist, wenn der Zustand groß ist und es viele Schritte gibt.

Zeitreise

Mit dem Feature time-travel gibt graph.time_travel(thread_id)? ein Handle ΓΌber den Checkpoint-Verlauf eines Threads zurΓΌck: Schritte auflisten, den Zustand zu einem bestimmten Schritt lesen oder einen Checkpoint mit fork_at abzweigen, um daraus einen neuen Thread zu erstellen. Der Aufruf gibt Result zurΓΌck, weil jede Operation Checkpoints liest; daher meldet ein Graph ohne Checkpointer GraphError::CheckpointError, statt eine Panik auszulΓΆsen.

Graphweite Standardwerte

Dieselbe Wiederholung ΓΌber zwanzig Knoten zu wiederholen, kann durch Auslassungen leicht fehleranfΓ€llig werden.

use adk_graph::graph::NodeDefaults;

let graph = graph
    .with_node_defaults(NodeDefaults::new().with_retry(RetryPolicy::new(3)))
    .with_node_retry("critical", RetryPolicy::new(10));

Ein Wert auf Knotenebene hat immer Vorrang. NodeDefaults enthÀlt außerdem ein Timeout und einen Fehler-Handler.

Wiederherstellung nach einem Knotenfehler

Sobald das Wiederholungslimit eines Knotens aufgebraucht ist, kann ein Handler aufzeichnen, was geschehen ist, und einen Wiederherstellungsknoten benennen, statt den Lauf zu beenden:

let graph = graph.with_node_error_handler("charge", |node, error, _state| {
    Ok(NodeOutput::new()
        .with_update("status", json!(format!("{node} failed: {error}")))
        .with_goto(["compensate"]))
});

Die RΓΌckgabe von Err beendet den Lauf wie zuvor. Eine Unterbrechung erreicht niemals einen Handler, weil eine Pause kein Fehler ist.

Wachstum von Checkpoints begrenzen

Ein Thread sammelt pro Super-Schritt einen Checkpoint. Ein Lauf, der tagelang besteht, wΓ€chst daher unbegrenzt, was Speicherplatz kostet und list verlangsamt.

use adk_graph::checkpoint::RetentionPolicy;
use std::time::Duration;

let graph = graph
    .with_checkpoint_retention(
        RetentionPolicy::keep_last(50).with_max_age(Duration::from_secs(7 * 24 * 3600)),
    );

Das Bereinigen erfolgt nach jedem Speichern, sodass die Kosten proportional zum Lauf bleiben und kein externer Job erforderlich ist. Der neueste Checkpoint wird niemals verworfen, unabhΓ€ngig davon, was die Richtlinie vorgibt, weil er derjenige ist, den eine Fortsetzung lΓ€dt β€” keep_last(0) wird auf eins gesetzt, und ein Thread, dessen sΓ€mtliche Checkpoints Γ€lter als das Alterslimit sind, behΓ€lt einen.

StandardmÀßig deaktiviert, sodass ein bestehender Thread seine vollstÀndige Historie behÀlt und Zeitreisen weiterhin jeden Schritt erreichen kânnen. Legen Sie eine Richtlinie fest, wenn ein Thread lange lÀuft und Sie nicht weit zurückspringen müssen.

Nicht deklarierte KanΓ€le ablehnen

Ein Kanal, den das Schema nicht deklariert, verwendet den Überschreib-Reducer, weil dieser der Fallback für einen unbekannten Namen ist. Ein Graph, der einen Listenkanal deklariert und dann in einen Àhnlich klingenden Namen schreibt, behÀlt nur den letzten Wert und meldet nichts.

let graph = graph.with_strict_channels();

Ein Knoten, der in einen nicht deklarierten Kanal schreibt, schlÀgt anschließend mit GraphError::UndeclaredChannel fehl und nennt dabei den Knoten und den Kanal. StandardmÀßig deaktiviert und wirkungslos, wenn ein Graph überhaupt keine KanÀle deklariert.

ADK-Integration

GraphAgent implementiert das ADK-Trait Agent und funktioniert daher mit:

  • Runner: Zusammen mit adk-runner fΓΌr die StandardausfΓΌhrung verwenden
  • Callbacks: VollstΓ€ndige UnterstΓΌtzung fΓΌr Vorher-/Nachher-Callbacks
  • Sessions: Funktioniert mit adk-session fΓΌr den GesprΓ€chsverlauf
  • Streaming: Gibt ADK EventStream zurΓΌck
use adk_runner::Runner;

let graph_agent = GraphAgent::builder("workflow")
    .before_agent_callback(|ctx| async {
        println!("Starting graph execution for session: {}", ctx.session_id());
        Ok(())
    })
    .after_agent_callback(|ctx, event| async {
        if let Some(content) = event.content() {
            println!("Graph completed with content");
        }
        Ok(())
    })
    // ... graph definition
    .build()?;

// GraphAgent implements Agent trait - use with Launcher or Runner
// See adk-runner README for Runner configuration

Beispiele

Validierte Graphbeispiele in diesem Repository:

cargo run --manifest-path examples/tier_examples/standard/Cargo.toml --bin 11-standard-graph
cargo run --manifest-path examples/tier_examples/standard/Cargo.toml --bin 12-standard-sequential
cargo run --manifest-path examples/competitive_graph_resume/Cargo.toml

Die vollstΓ€ndige Graphgalerie mit echter LLM-Integration befindet sich unter adk-playground.

Vergleich mit LangGraph

FunktionLangGraphadk-graph
ZustandsverwaltungTypedDict + ReducerStateSchema + Reducer
AusfΓΌhrungsmodellPregel-SuperstepsPregel-Supersteps
CheckpointingSpeicher, SQLite, PostgresSpeicher, SQLite
Mensch-in-der-Schleifeinterrupt_before/afterinterrupt_before/after + dynamisch
Streaming5 Modi5 Modi
ZyklenNativNativ
TypsicherheitPython-TypisierungRust-Typsystem
LLM-IntegrationLangChainAgentNode + ADK-Agenten
Routing von einem KnotenCommand(goto=...)NodeOutput::with_goto, AgentNode::with_goto_mapper
Durch den Zustand bestimmte VerzweigungsgrâßeSend("node", input)ctx.run_node_with(name, input, options)
Wiederholung pro KnotenRetryPolicyRetryPolicy mit begrenztem Backoff und Jitter
ParallelitΓ€tsgrenzemax_concurrency in der Konfigurationwith_max_concurrency im Graphen
Knoten-Cachingcache_policycache_policy (node-cache-Funktion)
VerzΓΆgerter Joindefer=TrueAutomatisch fΓΌr mehrere direkte VorgΓ€nger sowie ein n-von-m-Quorum
Untergraph als Knotenadd_node("sub", compiled)SubgraphNode, wobei die Kanalzuordnung beim Kompilieren des ΓΌbergeordneten Graphen ΓΌberprΓΌft wird
Übergang vom Untergraphen zum übergeordneten GraphenCommand(graph=Command.PARENT)NodeOutput::with_goto_parent
Graphweite Knotenstandardsset_node_defaults (β‰₯1.2)with_node_defaults sowie der bereits vorhandene default_timeout
Fehlerbehandler fΓΌr Knotenerror_handler (β‰₯1.2)with_node_error_handler, ausgefΓΌhrt, sobald das Wiederholungsbudget aufgebraucht ist

Zwei Unterschiede sollten klar benannt werden. run_node_with gibt das Ergebnis des untergeordneten Elements inline zurΓΌck und zeichnet es auf, sodass ein fortgesetzter Lauf nicht erneut fΓΌr ein bereits abgeschlossenes untergeordnetes Element bezahlt, wΓ€hrend Send die Arbeit an den Scheduler ΓΌbergibt und sie ΓΌber einen Reducer sammelt. Und ein Wechsel von einem Teilgraphen zu seinem ΓΌbergeordneten Graphen hat hier noch kein GegenstΓΌck.


ZurΓΌck: ← Multi-Agent Systems | Weiter: Realtime Agents β†’

Graph-Agenten - ADK-Rust Dokumentation | ADK-Rust