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ΓΆtigen | Verwenden Sie | Warum |
|---|---|---|
| Eine feste Reihenfolge von Schritten | SequentialAgent | Die Topologie ist die Liste. Es muss nichts deklariert werden. |
| Mehrere Agenten mit derselben Eingabe | ParallelAgent | Fan-out ohne zu konfigurierenden Join. |
| Wiederholen, bis eine Bedingung erfΓΌllt ist | LoopAgent | Die Abbruchbedingung ist ein Callback, keine Kante. |
| Ein zur Laufzeit ausgewΓ€hlter Zweig | Graph | Bedingte Kanten oder ein Knoten, der seinen eigenen Nachfolger benennt. |
| Zyklen mit einem Schrittbudget | Graph | recursion_limit begrenzt die Super-Schritte. |
| Eine Pause, die eine Person spΓ€ter beantwortet | Graph | Interrupts erstellen einen Checkpoint fΓΌr die AusfΓΌhrung und setzen sie fort. |
| Γberleben nach einem Neustart des Prozesses | Graph | SqliteCheckpointer bleibt bei jedem Super-Schritt erhalten. |
| ZurΓΌckspulen zu einem frΓΌheren Schritt | Graph | Eine 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
| Komponente | Rolle |
|---|---|
AgentNode | UmschlieΓt LLM-Agenten mit Eingabe-/Ausgabezuordnungen |
input_mapper | Transformiert den Zustand β Agenteneingabe Content |
output_mapper | Transformiert Agentenereignisse β Zustandsaktualisierungen |
channels | Deklariert Zustandsfelder, die der Graph verwenden wird |
edge() | Definiert den AusfΓΌhrungsfluss zwischen Knoten |
ExecutionConfig | Stellt 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:
| Weitergegeben | Hinweis |
|---|---|
app_name, user_id, session_id | Die des Aufrufers, keine synthetische |
| Bereiche und Request-Metadaten | Damit BereichsprΓΌfungen die Berechtigungen des Aufrufers sehen |
| Geheimnisdienst, Speicher, Artefakte, gemeinsamer Zustand | Genau wie auΓerhalb des Graphen verfΓΌgbar |
| Abbruch | Runner::interrupt erreicht einen als Knoten ausgefΓΌhrten Agenten |
RunConfig | Vom Aufrufer geerbt |
branch | Abgeleitet, 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]))
}
| Verhalten | Regel |
|---|---|
| Deklarierte Kanten | Ein Knoten, der einen goto festlegt, folgt nicht zusΓ€tzlich seinen ausgehenden Kanten. Der goto ersetzt sie. |
| Mehrere Ziele | Alle benannten Knoten werden in sortierter Reihenfolge ausgefΓΌhrt. |
END | Das Benennen von END stoppt diesen Zweig. |
| Ein unbekannter Name | Die AusfΓΌhrung schlΓ€gt mit GraphError::UnknownRouteTarget fehl. |
| Kein goto | Die 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:
| Fall | Verhalten |
|---|---|
| Bedingte VorgΓ€nger | Werden nicht gezΓ€hlt. Ein bedingter Zweig wird mΓΆglicherweise nie ausgefΓΌhrt, sodass das Warten darauf den Join blockieren kΓΆnnte. |
| Ein Quorum statt aller | Setze 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
| Reducer | Verhalten |
|---|---|
Overwrite | Alten Wert durch neuen ersetzen (Standard) |
Append | An Liste anhΓ€ngen |
Sum | Numerische Werte hinzufΓΌgen |
Custom | Benutzerdefinierte 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:
| Situation | Gespeicherte Frontier |
|---|---|
| Super-Schritt abgeschlossen | Die als NΓ€chstes auszufΓΌhrenden Knoten |
| AusfΓΌhrung abgeschlossen | Leer |
| 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 Siefork_at, um von diesem PrΓΌfpunkt einen Zweig zu erstellen, und rufen Sie den Graphen auf dem abgezweigten Thread auf. Die Methode hieΓ zuvorreplayund 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
| Modus | Beschreibung |
|---|---|
Values | Nach jedem Knoten den vollstΓ€ndigen Zustand streamen |
Updates | Nur ZustandsΓ€nderungen streamen |
Messages | Aktualisierungen des Nachrichtentyps streamen |
Debug | Alle 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, dasexecute_streamΓΌberschreibt, muss einStreamEvent::Updates-Ereignis liefern, das seine Zustandsaktualisierungen enthΓ€lt. Andernfalls streamt der Knoten zwar Ereignisse, trΓ€gt aber imMessages-Modus keinen Zustand bei. Das standardmΓ€Γigeexecute_stream, dasexecuteumschlieΓ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()?;
| Regel | Verhalten |
|---|---|
| Gemeinsame Namen | Ein 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 innerhalb | Pausiert das ΓΌbergeordnete Element und ΓΌbertrΓ€gt den Namen des Teilgraphen sowie die innere Nachricht. |
| Threads | Der Teilgraph lΓ€uft auf <parent thread>/<node name>, daher kΓΆnnen zwei Teilgraphen eines ΓΌbergeordneten Elements nicht kollidieren. |
| Ein falscher Kanalname | SchlΓ€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 Pause | Wie die Antwort eintrifft |
|---|---|
interrupt_before / interrupt_after innerhalb | Nichts bereitzustellen; das Fortsetzen hebt die ausgelΓΆste Sperre auf |
| Ein Knoten, der selbst entscheidet | Die 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-runnerfΓΌr die StandardausfΓΌhrung verwenden - Callbacks: VollstΓ€ndige UnterstΓΌtzung fΓΌr Vorher-/Nachher-Callbacks
- Sessions: Funktioniert mit
adk-sessionfΓΌr den GesprΓ€chsverlauf - Streaming: Gibt ADK
EventStreamzurΓΌ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
| Funktion | LangGraph | adk-graph |
|---|---|---|
| Zustandsverwaltung | TypedDict + Reducer | StateSchema + Reducer |
| AusfΓΌhrungsmodell | Pregel-Supersteps | Pregel-Supersteps |
| Checkpointing | Speicher, SQLite, Postgres | Speicher, SQLite |
| Mensch-in-der-Schleife | interrupt_before/after | interrupt_before/after + dynamisch |
| Streaming | 5 Modi | 5 Modi |
| Zyklen | Nativ | Nativ |
| Typsicherheit | Python-Typisierung | Rust-Typsystem |
| LLM-Integration | LangChain | AgentNode + ADK-Agenten |
| Routing von einem Knoten | Command(goto=...) | NodeOutput::with_goto, AgentNode::with_goto_mapper |
| Durch den Zustand bestimmte VerzweigungsgrΓΆΓe | Send("node", input) | ctx.run_node_with(name, input, options) |
| Wiederholung pro Knoten | RetryPolicy | RetryPolicy mit begrenztem Backoff und Jitter |
| ParallelitΓ€tsgrenze | max_concurrency in der Konfiguration | with_max_concurrency im Graphen |
| Knoten-Caching | cache_policy | cache_policy (node-cache-Funktion) |
| VerzΓΆgerter Join | defer=True | Automatisch fΓΌr mehrere direkte VorgΓ€nger sowie ein n-von-m-Quorum |
| Untergraph als Knoten | add_node("sub", compiled) | SubgraphNode, wobei die Kanalzuordnung beim Kompilieren des ΓΌbergeordneten Graphen ΓΌberprΓΌft wird |
| Γbergang vom Untergraphen zum ΓΌbergeordneten Graphen | Command(graph=Command.PARENT) | NodeOutput::with_goto_parent |
| Graphweite Knotenstandards | set_node_defaults (β₯1.2) | with_node_defaults sowie der bereits vorhandene default_timeout |
| Fehlerbehandler fΓΌr Knoten | error_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 β