Aufbau von Multi-Agenten-Systemen mit ADK-Rust
Erfahren Sie, wann und wie Sie verschiedene Multi-Agenten-Muster einsetzen – von einfacher Koordination bis hin zu komplexer graphenbasierter Orchestrierung – mit der Typsicherheit und Leistung von Rust.
1. Einführung
Das Problem: KI, die an ihre Grenzen stößt
Sie haben Ihren ersten KI-Agenten gebaut. Er ist beeindruckend – er kann Fragen beantworten, Dokumente zusammenfassen, vielleicht sogar Code schreiben. Doch dann kommt die Realität:
"Können Sie meine letzte Rechnung überprüfen und mir auch bei der Einrichtung des API helfen?"
Ihr Agent hat Schwierigkeiten. Er wurde nicht auf Abrechnungssysteme UND Entwicklerdokumente UND Fehlerbehebungsworkflows trainiert. Sein Anweisungsprompt ist bereits 2000 Tokens lang und versucht, alles abzudecken. Die Antwortqualität verschlechtert sich.
Dies ist die Grenze des Einzelagenten. Wenn Ihre Anwendung wächst, stehen Sie vor schmerzhaften Kompromissen:
- Aufgeblähte Prompts: Jede neue Funktion bedeutet längere Anweisungen, höhere Latenz und mehr Verwirrung für das Modell
- Tausendsassa: Ein Agent, der Abrechnung, Support UND Vertrieb abwickelt, wird in allen dreien mittelmäßig
- Unmögliche Wartung: Das Ändern der Abrechnungslogik sollte nicht das Risiko bergen, Ihre Support-Workflows zu unterbrechen
- Keine Spezialisierung: Ihr Mathematik-Agent kann kein Taschenrechner-Tool haben, während Ihr Recherche-Agent eine Websuche hat – sie teilen alles
Die Lösung: Spezialisierte Agenten arbeiten zusammen
Multi-Agenten-Systeme lösen dies, indem sie komplexe Aufgaben in spezialisierte Rollen zerlegen. Anstatt eines überforderten Generalisten erstellen Sie fokussierte Spezialisten:
- Kundenservice: Ein coordinator leitet Benutzer an Abrechnungs-, technischen Support- oder Vertriebsspezialisten weiter – jeder mit fokussiertem Training und Tools
- Inhaltserstellung: Ein Recherche-Agent sammelt Fakten, ein writer erstellt die Erzählung, ein Redakteur poliert – jeder Agent beherrscht eine Fähigkeit
- Codegenerierung: Ein Planer entwirft die Architektur, ein coder implementiert, ein Prüfer fängt Fehler ab – unterschiedliche Perspektiven verbessern die Qualität
Das Ergebnis? Jeder Agent bleibt fokussiert, Prompts bleiben überschaubar, und Sie können die Abrechnungslogik aktualisieren, ohne den Support zu berühren. Es sind Microservices für KI.
Was Sie lernen werden
ADK-Rust bietet drei schrittweise leistungsfähigere Muster für die Multi-Agenten-Orchestrierung. Dieses Tutorial wird Ihnen beibringen:
- Wann Sie jedes Muster basierend auf Ihren Anforderungen verwenden sollten
- Wie Sie diese mit produktionsreifem Rust-Code implementieren
- Warum die architektonischen Kompromisse für Ihren spezifischen Anwendungsfall wichtig sind
2. Das richtige Muster wählen
Bevor wir uns in den Code stürzen, lassen Sie uns verstehen, was jedes Muster bietet:
| Muster | Am besten geeignet für | Kontrollebene | Komplexität |
|---|---|---|---|
| Koordinator | Gesprächsübergaben | LLM entscheidet | Niedrig |
| AgentTool | Antwortverarbeitung | Koordinator verarbeitet | Mittel |
| Supervisor-Graph | Komplexe Workflows | Vollständiges Zustandsmanagement | Hoch |
3. Muster 1: Der Koordinator (Unter-Agenten)
🎯 Anwendungsfall: Kundenservice-Routing
Sie entwickeln einen Kundenservice-Bot. Benutzer könnten Fragen zur Abrechnung stellen, technische Hilfe anfordern oder sich nach neuen Funktionen erkundigen. Jede Domäne erfordert spezialisiertes Wissen, aber Benutzer sollten nicht wissen müssen, welche Abteilung sie kontaktieren sollen.
Das Koordinator-Muster verwendet die automatische Agentenübertragung. Wenn Sie Unter-Agenten über .sub_agent() hinzufügen, injiziert ADK-Rust ein transfer_to_agent-Tool. Der LLM entscheidet basierend auf der Konversation, wann die Übergabe erfolgen soll.
Hauptmerkmale
- Nahtlose Übergabe: Benutzer setzt das Gespräch natürlich mit dem Spezialisten fort
- LLM-gesteuertes Routing: Der coordinator entscheidet basierend auf dem Konversationskontext
- Konversationskontinuität: Der Sitzungsverlauf wird über Übertragungen hinweg beibehalten
- Keine Antwortverarbeitung: Spezialist spricht nach der Übertragung direkt mit dem Benutzer
Implementierung
use adk_rust::prelude::*;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);
// Specialist: Billing Agent
// Clear description helps the coordinator know when to transfer
let billing_agent = LlmAgentBuilder::new("billing_agent")
.description(
"Handles all billing questions: invoices, payments, refunds, subscription plans, and account charges. Transfer here for any money-related questions."
)
.instruction(
"You are a billing specialist. Answer questions about invoices, payments, and subscription plans. Be concise and accurate. If asked about technical issues, suggest transferring to support."
)
.model(model.clone())
.build()?;
// Specialist: Technical Support Agent
let support_agent = LlmAgentBuilder::new("support_agent")
.description(
"Provides technical support: troubleshooting, bug reports, feature questions, and integration help. Transfer here for any technical problems."
)
.instruction(
"You are a technical support specialist. Help users troubleshoot issues step by step. Ask clarifying questions when needed. Be patient and thorough."
)
.model(model.clone())
.build()?;
// Coordinator: Routes to specialists
let coordinator = LlmAgentBuilder::new("coordinator")
.description("Customer service coordinator")
.instruction(
"You are a friendly customer service coordinator. Your job is to:\n 1. Greet users warmly\n 2. Understand their needs\n 3. Route to the right specialist:\n - Billing questions → transfer to billing_agent\n - Technical issues → transfer to support_agent\n 4. Handle general questions yourself\n\n Always explain who you're connecting them with."
)
.model(model.clone())
.sub_agent(Arc::new(billing_agent))
.sub_agent(Arc::new(support_agent))
.build()?;
// Run with the built-in launcher
Launcher::new(Arc::new(coordinator))
.run()
.await?;
Ok(())
}Beispielkonversation
Benutzer: Hallo, ich habe eine Frage zu meiner Rechnung
[coordinator]: Hallo! Gerne helfe ich Ihnen bei Ihrer Abrechnungsfrage. Lassen Sie mich Sie mit unserem Abrechnungsspezialisten verbinden, der Ihnen helfen kann.
System: 🔄 Übertragung an: billing_agent
[billing_agent]: Hallo! Ich bin der Abrechnungsspezialist. Ich kann Ihnen bei Rechnungen, Zahlungen und Abonnementfragen helfen. Was möchten Sie über Ihre Rechnung wissen?
Benutzer: Warum wurde mir diesen Monat zweimal etwas berechnet?
[billing_agent]: Ich werde diese doppelte Abbuchung für Sie prüfen...
✅ Wann den Koordinator verwenden
- Benutzer sollten direkt mit Spezialisten interagieren
- Routing-Entscheidungen sind unkompliziert
- Sie müssen Spezialistenantworten nicht verarbeiten
- Der Gesprächsfluss ist linear (ein Spezialist nach dem anderen)
4. Muster 2: Agenten als Werkzeuge (AgentTool)
🎯 Anwendungsfall: Wissensaggregation
Sie entwickeln einen intelligenten Assistenten, der Fragen aus mehreren Domänen beantwortet. Ein Benutzer fragt: „Was sind 15 % von 250, und warum ist diese Zahl in der Geschichte bedeutsam?“ Sie müssen einen Mathematikexperten und dann einen Trivia-Experten hinzuziehen und deren Antworten kombinieren.
Das AgentTool-Muster verpackt Agenten als aufrufbare Werkzeuge. Im Gegensatz zu Sub-Agenten ruft der coordinator Spezialisten programmatisch auf und empfängt deren Antworten, um sie zu verarbeiten oder zu kombinieren, bevor er dem Benutzer antwortet.
Wesentliche Unterschiede zum Koordinator
Koordinator (Sub-Agenten)
- • Spezialist spricht direkt mit dem Benutzer
- • Ein Spezialist nach dem anderen
- • Keine Antwortverarbeitung
AgentTool
- • Koordinator empfängt Antworten
- • Kann mehrere Spezialisten aufrufen
- • Aggregiert und fasst zusammen
Implementierung
use adk_agent::LlmAgentBuilder;
use adk_tool::{AgentTool, FunctionTool};
use adk_core::ToolContext;
use serde_json::{json, Value};
use std::sync::Arc;
// Calculator tool for the math agent
async fn calculator(
_ctx: Arc<dyn ToolContext>,
args: Value
) -> Result<Value, adk_core::AdkError> {
let operation = args["operation"].as_str().unwrap_or("add");
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(0.0);
let result = match operation {
"add" => a + b,
"multiply" => a * b,
"percent" => a * (b / 100.0),
_ => return Err(adk_core::AdkError::Tool(
format!("Unknown operation: {}", operation)
)),
};
Ok(json!({ "result": result }))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create calculator tool
let calc_tool = FunctionTool::new(
"calculator",
"Performs arithmetic: add, multiply, percent. Args: operation (string), a (number), b (number)",
calculator,
);
// Math Expert agent - has its own tools
let math_agent = LlmAgentBuilder::new("math_expert")
.description(
"A math expert that performs calculations. Use for any math-related questions, percentages, or numerical analysis."
)
.instruction(
"You are a math expert. Use the calculator tool for calculations. Show your work step by step. Be precise."
)
.model(model.clone())
.tool(Arc::new(calc_tool))
.build()?;
// Trivia Expert agent - uses LLM knowledge
let trivia_agent = LlmAgentBuilder::new("trivia_expert")
.description(
"A trivia and history expert. Use for questions about historical facts, pop culture, science facts, and trivia."
)
.instruction(
"You are a trivia expert with vast knowledge across domains. Answer questions accurately and include interesting related facts."
)
.model(model.clone())
.build()?;
// Wrap agents as tools with configuration
let math_tool = AgentTool::new(Arc::new(math_agent))
.skip_summarization(false) // Summarize lengthy responses
.forward_artifacts(true); // Pass through any generated files
let trivia_tool = AgentTool::new(Arc::new(trivia_agent))
.skip_summarization(false);
// Coordinator uses agents as tools
let coordinator = LlmAgentBuilder::new("coordinator")
.description("Smart assistant that combines expert knowledge")
.instruction(
"You are a helpful assistant with access to expert agents:\n - math_expert: For calculations and math problems\n - trivia_expert: For facts, history, and trivia\n\n When questions span multiple domains, call multiple experts and synthesize their responses into a cohesive answer."
)
.model(model)
.tool(Arc::new(math_tool))
.tool(Arc::new(trivia_tool))
.build()?;
Launcher::new(Arc::new(coordinator)).run().await?;
Ok(())
}Beispiel: Frage aus mehreren Domänen
Benutzer: Was sind 15 % von 250, und ist diese Zahl in der Geschichte bedeutsam?
System: // Koordinator ruft math_expert Werkzeug auf
[math_expert antwortet]: 15 % von 250 sind 37,5
System: // Koordinator ruft trivia_expert Tool auf
[trivia_expert antwortet]: 37 und 38 sind historisch weniger bemerkenswert, aber 37,5 °C ist die menschliche Körpertemperatur...
System: // Koordinator synthetisiert
[coordinator]: 15 % von 250 ergeben 37,5. Interessanterweise liegt 37,5 °C (99,5 °F) nahe an der durchschnittlichen menschlichen Körpertemperatur von 37 °C, was sie zu einer medizinisch bedeutsamen Zahl macht!
✅ Wann AgentTool verwenden
- Sie müssen Antworten von mehreren Experten kombinieren
- Der Koordinator sollte die Ausgabe des Spezialisten zusammenfassen oder filtern
- Spezialisten haben ihre eigenen Tools (verschachtelte Fähigkeiten)
- Sie möchten die Agentenaufrufe programmatisch steuern
5. Muster 3: Der Supervisor-Graph
🎯 Anwendungsfall: Content-Erstellungspipeline
Sie bauen ein Content-Erstellungssystem auf. Zu einem bestimmten Thema müssen Sie: (1) recherchieren, (2) einen Artikel schreiben, (3) Codebeispiele hinzufügen. Der supervisor entscheidet dynamisch die Reihenfolge basierend auf der Aufgabe, und die Worker können für Überarbeitungen zurückkehren.
Das Supervisor-Graph-Muster verwendet das graphenbasierte Workflow-System von ADK-Rust. Ein supervisor-Agent leitet dynamisch an Worker weiter, mit vollständiger Zustandsverwaltung und Unterstützung für zyklische Ausführung.
Warum einen Graphen verwenden?
- Dynamisches Routing: Der Supervisor entscheidet den nächsten Worker basierend auf dem aktuellen Zustand
- Zyklische Ausführung: Worker können für Iterationen zurückkehren
- Geteilter Zustand: Alle Knoten lesen/schreiben in ein gemeinsames Zustandsobjekt
- Bedingte Kanten: Unterschiedliche Pfade basierend auf LLM-Entscheidungen
- Rekursionsgrenzen: Verhindern unendliche Schleifen
Implementierung
use adk_agent::LlmAgentBuilder;
use adk_graph::{
StateGraph,
edge::{START, END},
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<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);
// Supervisor: Decides which worker should act next
let supervisor = LlmAgentBuilder::new("supervisor")
.description("Routes tasks to specialized workers")
.instruction(
"You are a task supervisor. Based on the task and work done so far, decide who should work next.\n\n Workers available:\n - researcher: Gathers information and facts\n - writer: Writes content based on research\n - coder: Creates code examples\n\n Respond with ONLY one word: 'researcher', 'writer', 'coder', or 'done'."
)
.model(model.clone())
.build()?;
// Workers with specialized roles
let researcher = LlmAgentBuilder::new("researcher")
.instruction("Research the topic. Provide key facts as bullet points.")
.model(model.clone())
.build()?;
let writer = LlmAgentBuilder::new("writer")
.instruction("Write engaging content based on the research provided.")
.model(model.clone())
.build()?;
let coder = LlmAgentBuilder::new("coder")
.instruction("Write clean, documented code examples for the topic.")
.model(model.clone())
.build()?;
// Create AgentNodes with input/output mappers
let supervisor_node = AgentNode::new(Arc::new(supervisor))
.with_input_mapper(|state| {
let task = state.get("task").and_then(|v| v.as_str()).unwrap_or("");
let history = state.get("history")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|h| h.get("agent").and_then(|a| a.as_str()))
.map(|s| format!("- {} completed", s))
.collect::<Vec<_>>()
.join("\n"))
.unwrap_or_default();
adk_core::Content::new("user").with_text(format!(
"Task: {}\n\nWork completed:\n{}\n\nWho next?",
task,
if history.is_empty() { "None yet" } else { &history }
))
})
.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();
let next = if text.to_lowercase().contains("researcher") {
"researcher"
} else if text.to_lowercase().contains("writer") {
"writer"
} else if text.to_lowercase().contains("coder") {
"coder"
} else {
"done"
};
updates.insert("next_agent".to_string(), json!(next));
}
}
updates
});
// Build the graph
let graph = StateGraph::with_channels(&[
"task", "next_agent", "history",
"research_output", "written_content", "code_output"
])
.add_node(supervisor_node)
.add_node(AgentNode::new(Arc::new(researcher)))
.add_node(AgentNode::new(Arc::new(writer)))
.add_node(AgentNode::new(Arc::new(coder)))
// Finalize node compiles all outputs
.add_node_fn("finalize", |ctx| async move {
let research = ctx.get("research_output").and_then(|v| v.as_str());
let content = ctx.get("written_content").and_then(|v| v.as_str());
let code = ctx.get("code_output").and_then(|v| v.as_str());
let result = format!(
"=== FINAL OUTPUT ===\n\n{}\n\n{}\n\n{}",
research.unwrap_or("No research"),
content.unwrap_or("No content"),
code.unwrap_or("No code")
);
Ok(NodeOutput::new().with_update("final_result", json!(result)))
})
// Graph structure
.add_edge(START, "supervisor")
.add_conditional_edges(
"supervisor",
|state| state.get("next_agent")
.and_then(|v| v.as_str())
.unwrap_or("done")
.to_string(),
[
("researcher", "researcher"),
("writer", "writer"),
("coder", "coder"),
("done", "finalize"),
],
)
// Workers cycle back to supervisor
.add_edge("researcher", "supervisor")
.add_edge("writer", "supervisor")
.add_edge("coder", "supervisor")
.add_edge("finalize", END)
.compile()?
.with_recursion_limit(15); // Prevent infinite loops
// Execute
let mut input = State::new();
input.insert("task".to_string(), json!("Create a guide about Rust error handling"));
input.insert("history".to_string(), json!([]));
let result = graph.invoke(input, ExecutionConfig::new("content-thread")).await?;
println!("{}", result.get("final_result").and_then(|v| v.as_str()).unwrap_or(""));
Ok(())
}✅ Wann den Supervisor-Graphen verwenden
- Die Workflow-Reihenfolge ist dynamisch und LLM-bestimmt
- Mitarbeiter müssen möglicherweise iterieren oder zurückkehren
- Komplexer Zustand muss zwischen Agenten geteilt werden
- Sie benötigen Checkpointing oder fortsetzbare Workflows
- Die Aufgabenzerlegung erfordert mehrere sequentielle Schritte
6. Mustervergleich
| Merkmal | Koordinator | AgentTool | Supervisor-Graph |
|---|---|---|---|
| Benutzer spricht mit | Spezialist direkt | Nur Koordinator | Endgültige Ausgabe |
| Multi-Agenten-Aufrufe | ❌ Einzeln | ✅ Parallel möglich | ✅ Orchestriert |
| Antwortverarbeitung | ❌ | ✅ | ✅ |
| Zyklische Workflows | ❌ | ❌ | ✅ |
| Gemeinsamer Zustand | Nur Sitzung | Nur Sitzung | Vollständiger Graphzustand |
| Einrichtungskomplexität | 🟢 Niedrig | 🟡 Mittel | 🔴 Hoch |
7. Fazit
Multi-Agenten-Systeme ermöglichen es Ihnen, anspruchsvolle KI-Anwendungen durch die Kombination spezialisierter Agenten zu erstellen. Wählen Sie Ihr Muster basierend auf Ihren Bedürfnissen:
- Koordinator: Schnell einzurichten, ideal für die Weiterleitung im Kundenservice
- AgentTool: Wenn Sie Antworten verarbeiten oder kombinieren müssen
- Supervisor-Graph: Komplexe, dynamische, mehrstufige Workflows
🦀 Los geht's
Bereit, Ihr eigenes Multi-Agenten-System zu erstellen?