Construyendo Sistemas Multiagente con ADK-Rust
Aprende cuándo y cómo usar diferentes patrones multiagente —desde la coordinación simple hasta la orquestación compleja basada en grafos— con la seguridad de tipos y el rendimiento de Rust.
1. Introducción
El Problema: IA Que Choca Contra un Muro
Has construido tu primer agente de IA. Es impresionante: puede responder preguntas, resumir documentos, quizás incluso escribir código. Pero luego la realidad golpea:
¿Puedes revisar mi última factura y también ayudarme a configurar el API?
Tu agente tiene dificultades. No fue entrenado en sistemas de facturación Y documentación para desarrolladores Y flujos de trabajo de resolución de problemas. Su prompt de instrucción ya tiene 2000 tokens intentando cubrirlo todo. La calidad de la respuesta se degrada.
Este es el límite del agente único. A medida que tu aplicación crece, te enfrentas a dolorosas compensaciones:
- Prompts inflados: Cada nueva capacidad significa instrucciones más largas, mayor latencia y más confusión para el modelo
- Generalista: Un agente que maneja facturación, soporte Y ventas se vuelve mediocre en los tres
- Mantenimiento imposible: Cambiar la lógica de facturación no debería arriesgarse a romper tus flujos de soporte
- Sin especialización: Tu agente de matemáticas no puede tener una herramienta de calculadora mientras tu agente de investigación tiene búsqueda web; comparten todo
La Solución: Agentes Especializados Trabajando Juntos
Los sistemas multiagente resuelven esto descomponiendo tareas complejas en roles especializados. En lugar de un generalista abrumado, creas especialistas enfocados:
- Servicio al cliente: Un coordinator dirige a los usuarios a especialistas de facturación, soporte técnico o ventas, cada uno con capacitación y herramientas enfocadas
- Creación de contenido: Un agente de investigación recopila hechos, un writer elabora la narrativa, un editor pule; cada agente domina una habilidad
- Generación de código: Un planificador diseña la arquitectura, un coder implementa, un revisor detecta errores: diferentes perspectivas mejoran la calidad
¿El resultado? Cada agente se mantiene enfocado, las indicaciones son manejables y puedes actualizar la lógica de facturación sin tocar el soporte. Son microservicios para IA.
Lo que aprenderás
ADK-Rust proporciona tres patrones progresivamente potentes para la orquestación multiagente. Este tutorial te enseñará:
- Cuándo usar cada patrón según tus requisitos
- Cómo implementarlos con código Rust listo para producción
- Por qué las compensaciones arquitectónicas importan para tu caso de uso específico
2. Eligiendo el patrón correcto
Antes de sumergirnos en el código, entendamos qué ofrece cada patrón:
| Patrón | Mejor para | Nivel de control | Complejidad |
|---|---|---|---|
| Coordinador | Traspasos de conversación | LLM decide | Baja |
| AgentTool | Procesamiento de respuestas | El coordinador procesa | Media |
| Grafo Supervisor | Flujos de trabajo complejos | Gestión completa del estado | Alta |
3. Patrón 1: El Coordinador (Subagentes)
🎯 Caso de Uso: Enrutamiento de Servicio al Cliente
Estás construyendo un bot de servicio al cliente. Los usuarios pueden preguntar sobre facturación, solicitar ayuda técnica o consultar sobre nuevas funciones. Cada dominio requiere conocimientos especializados, pero los usuarios no deberían necesitar saber con qué departamento contactar.
El patrón Coordinador utiliza la transferencia automática de agentes. Cuando añades subagentes a través de .sub_agent(), ADK-Rust inyecta una herramienta transfer_to_agent. El LLM decide cuándo transferir basándose en la conversación.
Características Clave
- Transferencia fluida: El usuario continúa de forma natural con el especialista
- Enrutamiento impulsado por LLM: El coordinator decide basándose en el contexto de la conversación
- Continuidad de la conversación: El historial de la sesión se mantiene a través de las transferencias
- Sin procesamiento de respuesta: El especialista habla directamente con el usuario después de la transferencia
Implementación
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(())
}Ejemplo de Conversación
Usuario: Hola, tengo una pregunta sobre mi factura
[coordinator]: ¡Hola! Estaré encantado de ayudarte con tu pregunta de facturación. Permíteme conectarte con nuestro especialista en facturación que puede asistirte.
Sistema: 🔄 Transferir a: billing_agent
[billing_agent]: ¡Hola! Soy el especialista en facturación. Puedo ayudarte con facturas, pagos y preguntas sobre suscripciones. ¿Qué te gustaría saber sobre tu factura?
Usuario: ¿Por qué se me cobró dos veces este mes?
[billing_agent]: Investigaré ese cargo duplicado por ti...
✅ Cuándo Usar el Coordinador
- El usuario debe interactuar directamente con los especialistas
- Las decisiones de enrutamiento son sencillas
- No es necesario procesar las respuestas de los especialistas
- El flujo de conversación es lineal (un especialista a la vez)
4. Patrón 2: Agentes como Herramientas (AgentTool)
🎯 Caso de Uso: Agregación de Conocimiento
Estás construyendo un asistente inteligente que responde preguntas que abarcan múltiples dominios. Un usuario pregunta "¿Qué es el 15% de 250, y por qué es significativo ese número en la historia?" Necesitas llamar a un experto en matemáticas, luego a un experto en trivia, y combinar sus respuestas.
El patrón AgentTool envuelve a los agentes como herramientas invocables. A diferencia de los sub-agentes, el coordinator invoca a los especialistas programáticamente y recibe sus respuestas para procesarlas o combinarlas antes de responder al usuario.
Diferencias Clave con el Coordinador
Coordinador (Sub-Agentes)
- • El especialista habla directamente con el usuario
- • Un especialista a la vez
- • Sin procesamiento de respuestas
AgentTool
- • El coordinador recibe las respuestas
- • Puede llamar a múltiples especialistas
- • Agrega y resume
Implementación
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(())
}Ejemplo: Pregunta Multidominio
Usuario: ¿Qué es el 15% de 250, y es ese número significativo en la historia?
Sistema: // El coordinador llama a la herramienta math_expert
[math_expert responde]: El 15% de 250 es 37.5
Sistema: // El Coordinador llama a la herramienta trivia_expert
[trivia_expert responde]: 37 y 38 son menos notables históricamente, pero 37.5°C es la temperatura del cuerpo humano...
Sistema: // El Coordinador sintetiza
[coordinator]: El 15% de 250 es igual a 37.5. Curiosamente, 37.5°C (99.5°F) está cerca de la temperatura corporal humana promedio de 37°C, ¡lo que lo convierte en un número médicamente significativo!
✅ Cuándo usar AgentTool
- Necesitas combinar respuestas de múltiples expertos
- El Coordinador debe resumir o filtrar la salida del especialista
- Los especialistas tienen sus propias herramientas (capacidades anidadas)
- Quieres control programático sobre la invocación del agente
5. Patrón 3: El Grafo Supervisor
🎯 Caso de Uso: Pipeline de Creación de Contenido
Estás construyendo un sistema de creación de contenido. Dado un tema, necesitas: (1) investigarlo, (2) escribir un artículo, (3) añadir ejemplos de código. El supervisor decide dinámicamente el orden basándose en la tarea, y los trabajadores pueden volver para revisiones.
El patrón de Grafo Supervisor utiliza el sistema de flujo de trabajo basado en grafos de ADK-Rust. Un agente supervisor enruta dinámicamente a los trabajadores, con gestión completa del estado y soporte para ejecución cíclica.
¿Por qué usar un Grafo?
- Enrutamiento dinámico: El Supervisor decide el siguiente trabajador basándose en el estado actual
- Ejecución cíclica: Los trabajadores pueden volver a iterar
- Estado compartido: Todos los nodos leen/escriben en un objeto de estado común
- Aristas condicionales: Diferentes rutas basadas en las decisiones de LLM
- Límites de recursión: Evitar bucles infinitos
Implementación
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(())
}✅ Cuándo usar el Grafo Supervisor
- El orden del flujo de trabajo es dinámico y está determinado por LLM
- Los trabajadores pueden necesitar iterar o volver atrás
- El estado complejo necesita ser compartido entre agentes
- Necesitas puntos de control o flujos de trabajo reanudables
- La descomposición de tareas requiere múltiples pasos secuenciales
6. Comparación de Patrones
| Característica | Coordinador | AgentTool | Grafo Supervisor |
|---|---|---|---|
| El usuario habla con | Especialista directamente | Solo el coordinador | Salida final |
| Llamadas multi-agente | ❌ Uno a la vez | ✅ Paralelo posible | ✅ Orquestado |
| Procesamiento de respuestas | ❌ | ✅ | ✅ |
| Flujos de trabajo cíclicos | ❌ | ❌ | ✅ |
| Estado compartido | Solo sesión | Solo sesión | Estado completo del grafo |
| Complejidad de configuración | 🟢 Baja | 🟡 Media | 🔴 Alta |
7. Conclusión
Los sistemas multiagente le permiten construir aplicaciones de IA sofisticadas combinando agentes especializados. Elija su patrón según sus necesidades:
- Coordinador: Rápido de configurar, ideal para el enrutamiento de servicio al cliente
- AgentTool: Cuando necesite procesar o combinar respuestas
- Grafo Supervisor: Flujos de trabajo complejos, dinámicos y de múltiples pasos