Construindo Sistemas Multiagente com ADK-Rust
Aprenda quando e como usar diferentes padrões multiagente—desde coordenação simples até orquestração complexa baseada em grafos—com a segurança de tipos e o desempenho de Rust.
1. Introdução
O Problema: IA Que Atinge um Limite
Você construiu seu primeiro agente de IA. É impressionante — ele pode responder perguntas, resumir documentos, talvez até escrever código. Mas então a realidade bate:
"Você pode verificar minha última fatura e também me ajudar a configurar o API?"
Seu agente tem dificuldades. Ele não foi treinado em sistemas de faturamento E documentação de desenvolvedor E fluxos de trabalho de solução de problemas. Seu prompt de instrução já tem 2000 tokens tentando cobrir tudo. A qualidade da resposta degrada.
Este é o limite do agente único. À medida que sua aplicação cresce, você enfrenta compensações dolorosas:
- Prompts inchados: Cada nova capacidade significa instruções mais longas, maior latência e mais confusão para o modelo
- Pau para toda obra: Um agente que lida com faturamento, suporte E vendas torna-se medíocre em todos os três
- Manutenção impossível: Alterar a lógica de faturamento não deveria arriscar quebrar seus fluxos de suporte
- Sem especialização: Seu agente de matemática não pode ter uma ferramenta de calculadora enquanto seu agente de pesquisa tem busca na web — eles compartilham tudo
A Solução: Agentes Especializados Trabalhando Juntos
Sistemas multiagentes resolvem isso decompondo tarefas complexas em papéis especializados. Em vez de um generalista sobrecarregado, você cria especialistas focados:
- Atendimento ao cliente: Um coordinator direciona os usuários para especialistas em faturamento, suporte técnico ou vendas — cada um com treinamento e ferramentas focados
- Criação de conteúdo: Um agente de pesquisa coleta fatos, um writer elabora a narrativa, um editor a aprimora — cada agente domina uma habilidade
- Geração de código: Um planejador projeta a arquitetura, um coder implementa, um revisor encontra bugs — diferentes perspectivas melhoram a qualidade
O resultado? Cada agente permanece focado, os prompts permanecem gerenciáveis, e você pode atualizar a lógica de faturamento sem tocar no suporte. São microsserviços para IA.
O Que Você Vai Aprender
ADK-Rust oferece três padrões progressivamente poderosos para orquestração multiagente. Este tutorial ensinará a você:
- Quando usar cada padrão com base em seus requisitos
- Como implementá-los com código Rust pronto para produção
- Por que as compensações arquitetônicas são importantes para o seu caso de uso específico
2. Escolhendo o Padrão Certo
Antes de mergulhar no código, vamos entender o que cada padrão oferece:
| Padrão | Melhor Para | Nível de Controle | Complexidade |
|---|---|---|---|
| Coordenador | Transferências de conversa | LLM decide | Baixa |
| AgentTool | Processamento de resposta | Coordenador processa | Média |
| Grafo Supervisor | Fluxos de trabalho complexos | Gerenciamento completo de estado | Alta |
3. Padrão 1: O Coordenador (Subagentes)
🎯 Caso de Uso: Roteamento de Atendimento ao Cliente
Você está construindo um bot de atendimento ao cliente. Os usuários podem perguntar sobre faturamento, solicitar ajuda técnica ou indagar sobre novos recursos. Cada domínio requer conhecimento especializado, mas os usuários não deveriam precisar saber qual departamento contatar.
O padrão Coordenador utiliza transferência automática de agente. Ao adicionar subagentes via .sub_agent(), ADK-Rust injeta uma ferramenta transfer_to_agent. O LLM decide quando fazer a transferência com base na conversa.
Características Principais
- Transferência contínua: O usuário continua naturalmente com o especialista
- Roteamento impulsionado por LLM: O coordinator decide com base no contexto da conversa
- Continuidade da conversa: O histórico da sessão é mantido entre as transferências
- Sem processamento de resposta: O especialista fala diretamente com o usuário após a transferência
Implementação
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(())
}Exemplo de Conversa
Usuário: Olá, tenho uma pergunta sobre minha fatura
[coordinator]: Olá! Ficarei feliz em ajudar com sua pergunta sobre faturamento. Deixe-me conectá-lo com nosso especialista em faturamento que poderá ajudá-lo.
Sistema: 🔄 Transferir para: billing_agent
[billing_agent]: Olá! Sou o especialista em faturamento. Posso ajudar com faturas, pagamentos e perguntas sobre assinaturas. O que você gostaria de saber sobre sua fatura?
Usuário: Por que fui cobrado duas vezes este mês?
[billing_agent]: Vou verificar essa cobrança duplicada para você...
✅ Quando Usar o Coordenador
- O usuário deve interagir diretamente com os especialistas
- As decisões de roteamento são diretas
- Você não precisa processar as respostas dos especialistas
- O fluxo da conversa é linear (um especialista por vez)
4. Padrão 2: Agentes como Ferramentas (AgentTool)
🎯 Caso de Uso: Agregação de Conhecimento
Você está construindo um assistente inteligente que responde a perguntas que abrangem múltiplos domínios. Um usuário pergunta "Quanto é 15% de 250, e por que esse número é significativo na história?" Você precisa chamar um especialista em matemática, depois um especialista em curiosidades, e combinar suas respostas.
O padrão AgentTool encapsula agentes como ferramentas invocáveis. Diferente dos sub-agentes, o coordinator invoca especialistas programaticamente e recebe suas respostas para processar ou combinar antes de responder ao usuário.
Principais Diferenças em relação ao Coordenador
Coordenador (Sub-Agentes)
- • O especialista fala diretamente com o usuário
- • Um especialista por vez
- • Sem processamento de resposta
AgentTool
- • O Coordenador recebe as respostas
- • Pode chamar múltiplos especialistas
- • Agrega e resume
Implementação
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(())
}Exemplo: Pergunta Multi-Domínio
Usuário: Quanto é 15% de 250, e esse número é significativo na história?
Sistema: // O Coordenador chama a ferramenta math_expert
[math_expert responde]: 15% de 250 é 37.5
Sistema: // Coordenador chama a ferramenta trivia_expert
[trivia_expert responde]: 37 e 38 são menos notáveis historicamente, mas 37,5°C é a temperatura do corpo humano...
Sistema: // Coordenador sintetiza
[coordinator]: 15% de 250 é igual a 37,5. Curiosamente, 37,5°C (99,5°F) está próximo da temperatura média do corpo humano de 37°C, tornando-o um número medicamente significativo!
✅ Quando Usar AgentTool
- Você precisa combinar respostas de múltiplos especialistas
- O Coordenador deve resumir ou filtrar a saída do especialista
- Especialistas têm suas próprias ferramentas (capacidades aninhadas)
- Você quer controle programático sobre a invocação do agente
5. Padrão 3: O Grafo Supervisor
🎯 Caso de Uso: Pipeline de Criação de Conteúdo
Você está construindo um sistema de criação de conteúdo. Dado um tópico, você precisa: (1) pesquisá-lo, (2) escrever um artigo, (3) adicionar exemplos de código. O supervisor decide dinamicamente a ordem com base na tarefa, e os trabalhadores podem retornar para revisões.
O padrão Grafo Supervisor usa o sistema de fluxo de trabalho baseado em grafo do ADK-Rust. Um agente supervisor roteia dinamicamente para os trabalhadores, com gerenciamento de estado completo e suporte à execução cíclica.
Por Que Usar um Grafo?
- Roteamento dinâmico: O Supervisor decide o próximo trabalhador com base no estado atual
- Execução cíclica: Os trabalhadores podem retornar para iterações
- Estado compartilhado: Todos os nós leem/escrevem em um objeto de estado comum
- Arestas condicionais: Caminhos diferentes com base nas decisões do LLM
- Limites de recursão: Previnem loops infinitos
Implementação
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(())
}✅ Quando Usar o Grafo Supervisor
- A ordem do fluxo de trabalho é dinâmica e LLM-determinada
- Os trabalhadores podem precisar iterar ou retornar
- O estado complexo precisa ser compartilhado entre os agentes
- Você precisa de pontos de verificação ou fluxos de trabalho retomáveis
- A decomposição de tarefas requer múltiplos passos sequenciais
6. Comparação de Padrões
| Funcionalidade | Coordenador | AgentTool | Grafo Supervisor |
|---|---|---|---|
| Usuário fala com | Especialista diretamente | Apenas Coordenador | Saída final |
| Chamadas multiagente | ❌ Um por vez | ✅ Paralelo possível | ✅ Orquestrado |
| Processamento de resposta | ❌ | ✅ | ✅ |
| Fluxos de trabalho cíclicos | ❌ | ❌ | ✅ |
| Estado compartilhado | Somente sessão | Somente sessão | Estado completo do grafo |
| Complexidade de configuração | 🟢 Baixa | 🟡 Média | 🔴 Alta |
7. Conclusão
Sistemas multiagente permitem construir aplicações de IA sofisticadas combinando agentes especializados. Escolha seu padrão com base nas suas necessidades:
- Coordenador: Rápido de configurar, ótimo para roteamento de atendimento ao cliente
- AgentTool: Quando você precisa processar ou combinar respostas
- Grafo Supervisor: Fluxos de trabalho complexos, dinâmicos e com múltiplas etapas