❓ O Problema de Roteamento
Imagine um pool de agentes especializados: um agente financeiro, um jurídico, um técnico, um de redação, um de pesquisa. O planejador gerou 15 tarefas. Qual agente executa qual tarefa? Esse é o problema de roteamento — e a solução errada custa caro.
Por que roteamento errado é caro
Custo extra
Usar agente com modelo GPT-4o para tarefa que GPT-4o-mini faria igual = 30x mais caro sem ganho de qualidade.
Qualidade ruim
Tarefa jurídica enviada para agente técnico: resultado genérico sem conhecimento de compliance e legislação aplicável.
Gargalo
Todas as tarefas enviadas para o mesmo agente "polivalente": queue de espera cresce enquanto agentes especializados ficam ociosos.
As dimensões do roteamento
Dimensão 1: Capacidade
O agente tem as ferramentas e conhecimento de domínio necessários para esta tarefa específica?
Dimensão 2: Disponibilidade
O agente está livre? Ou está com sua fila cheia? Load balancing resolve quando há múltiplas instâncias.
Dimensão 3: Custo
Para tarefas simples, usar o modelo mais barato que resolve. Reservar modelos caros para tarefas que realmente exigem.
Dimensão 4: SLA
Se a tarefa é urgente, escolher o agente mais rápido disponível, mesmo que seja um pouco mais caro.
📊 Roteamento Estático
A abordagem mais simples: uma tabela fixa mapeando tipo de tarefa para agente responsável. Funciona bem até o pool de agentes crescer além de 5-6 tipos.
Implementação de roteamento estático
class StaticRouter:
"""Roteador estático: tabela fixa tipo_tarefa → agente."""
ROUTING_TABLE = {
"pesquisa": "research_agent",
"analise": "analysis_agent",
"redacao": "writing_agent",
"juridico": "legal_agent",
"financeiro": "finance_agent",
"tecnico": "tech_agent",
"formatacao": "formatting_agent",
}
FALLBACK_AGENT = "general_agent"
def route(self, task: dict) -> str:
task_type = task.get("agente_tipo", "").lower()
agent = self.ROUTING_TABLE.get(task_type, self.FALLBACK_AGENT)
print(f"[StaticRouter] Task '{task['id']}' → Agent '{agent}'")
return agent
def add_route(self, task_type: str, agent_id: str):
"""Adicionar rota dinamicamente (ainda estático, mas configurável)."""
self.ROUTING_TABLE[task_type] = agent_id
Quando usar roteamento estático
- ✓Pool de <6 agentes com tipos de tarefa bem definidos
- ✓Requisito de previsibilidade total no roteamento
- ✓Ambiente regulado onde cada decisão deve ser auditável
- ✓Time que prefere transparência a flexibilidade
Limitações do roteamento estático
- ✗Não escala: 20 tipos de tarefa × 10 agentes = 200 entradas manuais
- ✗Sem capacidade de aprender com o histórico de execuções
- ✗Tarefas com tipo ambíguo caem sempre no fallback, sem tentativa de especialização
- ✗Manutenção custosa quando novos agentes são adicionados
🤖 Roteamento Dinâmico com LLM Classifier
Em vez de uma tabela fixa, um agente roteador usa LLM para ler a descrição da tarefa e selecionar o agente mais adequado do pool. Adicionar um novo agente requer apenas atualizar a descrição de capacidades — sem alterar código de roteamento.
Implementação: LLMRouter
import anthropic
import json
client = anthropic.Anthropic()
class LLMRouter:
"""Roteador dinâmico: LLM seleciona agente por semântica da tarefa."""
def __init__(self, agent_registry: dict):
"""
agent_registry: dict com agent_id → descrição de capacidades
Exemplo:
{
"research_agent": "Especializado em pesquisa web, coleta de dados públicos...",
"legal_agent": "Análise jurídica, contratos, compliance LGPD, litígios...",
"finance_agent": "Análise financeira, modelagem, valuation, DCF...",
}
"""
self.registry = agent_registry
def route(self, task: dict) -> tuple[str, float]:
"""Retorna (agent_id, confidence_score)."""
agents_desc = "\n".join([
f"- {agent_id}: {desc}"
for agent_id, desc in self.registry.items()
])
response = client.messages.create(
model="claude-haiku-4-5", # Modelo barato para classificação
max_tokens=256,
messages=[{
"role": "user",
"content": f"""Selecione o agente mais adequado para esta tarefa.
TAREFA:
Título: {task['titulo']}
Descrição: {task['descricao']}
AGENTES DISPONÍVEIS:
{agents_desc}
Retorne JSON com:
{{"agent_id": "id_do_agente", "confidence": 0.0-1.0, "reasoning": "justificativa curta"}}"""
}]
)
result = json.loads(response.content[0].text)
# Fallback se confiança baixa
if result["confidence"] < 0.6:
result["agent_id"] = "general_agent"
return result["agent_id"], result["confidence"]
def add_agent(self, agent_id: str, capabilities: str):
"""Adicionar agente ao pool sem alterar código de roteamento."""
self.registry[agent_id] = capabilities
Por que usar Claude Haiku para classificação?
Claude Haiku custa ~$0.0001 por chamada. Classificar 100 tarefas = $0.01 total. Usar Claude Opus para o mesmo: ~$1.50. O classificador é chamado para cada tarefa — o custo acumula. Use o modelo mais barato que classifica corretamente — geralmente Haiku ou GPT-4o-mini são suficientes para classificação semântica de tarefa.
⚡ Load Balancing
Quando há múltiplas instâncias do mesmo tipo de agente, o load balancer distribui a carga para evitar gargalos. Sem ele, uma instância fica sobrecarregada enquanto outras ficam ociosas.
Estratégias de load balancing
from collections import deque
import time
class AgentPool:
"""Pool de instâncias de um tipo de agente com load balancing."""
def __init__(self, agent_type: str, instances: list[str]):
self.agent_type = agent_type
self.instances = instances
self.queue_depth = {inst: 0 for inst in instances}
self.last_used = {inst: 0.0 for inst in instances}
self._round_robin = deque(instances)
def get_instance_round_robin(self) -> str:
"""Rotação simples — ignora estado."""
instance = self._round_robin[0]
self._round_robin.rotate(-1)
return instance
def get_instance_least_loaded(self) -> str:
"""Escolhe instância com menor fila — ótimo para tarefas longas."""
return min(self.queue_depth, key=self.queue_depth.get)
def get_instance_health_aware(self, healthy_instances: set) -> str:
"""Escolhe instância saudável com menor carga."""
healthy = {k: v for k, v in self.queue_depth.items()
if k in healthy_instances}
if not healthy:
raise RuntimeError(f"Nenhuma instância saudável de {self.agent_type}")
return min(healthy, key=healthy.get)
def mark_busy(self, instance_id: str):
self.queue_depth[instance_id] += 1
self.last_used[instance_id] = time.time()
def mark_free(self, instance_id: str):
self.queue_depth[instance_id] = max(0, self.queue_depth[instance_id] - 1)
def status(self) -> dict:
return {
"type": self.agent_type,
"instances": len(self.instances),
"queue_depths": dict(self.queue_depth),
"total_load": sum(self.queue_depth.values())
}
Quando usar cada estratégia
💰 Roteamento por Custo
Para tarefas simples, use o agente com o modelo mais barato. Para tarefas que exigem raciocínio profundo, use o modelo mais capaz. A classificação de complexidade é automática e acontece antes do roteamento principal.
Tabela de decisão: tarefa → modelo
| Complexidade | Exemplos | Modelo | Custo relativo |
|---|---|---|---|
| Simples | Formatação, extração, classificação, resumo curto | Claude Haiku / GPT-4o-mini | 1x |
| Médio | Análise com múltiplas variáveis, síntese de dados, raciocínio moderado | Claude Sonnet / GPT-4o | 10x |
| Complexo | Planejamento estratégico, análise jurídica profunda, código complexo | Claude Opus / o1 | 50x |
Implementação: CostAwareRouter
class CostAwareRouter:
"""Roteador que seleciona o modelo mais barato capaz para a tarefa."""
MODEL_TIERS = {
"simple": {"model": "claude-haiku-4-5", "cost_per_1k": 0.00025},
"medium": {"model": "claude-sonnet-4-6", "cost_per_1k": 0.003},
"complex": {"model": "claude-opus-4-6", "cost_per_1k": 0.015},
}
def classify_complexity(self, task: dict) -> str:
"""Classifica complexidade da tarefa usando modelo barato."""
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=64,
messages=[{
"role": "user",
"content": f"""Classifique a complexidade desta tarefa.
Tarefa: {task['descricao']}
Responda com APENAS uma palavra: "simple", "medium" ou "complex"."""
}]
)
complexity = response.content[0].text.strip().lower()
return complexity if complexity in self.MODEL_TIERS else "medium"
def route(self, task: dict) -> dict:
complexity = self.classify_complexity(task)
tier = self.MODEL_TIERS[complexity]
return {
"task_id": task["id"],
"complexity": complexity,
"model": tier["model"],
"estimated_cost_per_1k": tier["cost_per_1k"]
}
# Uso:
router = CostAwareRouter()
result = router.route({"id": "T01", "descricao": "Formatar os dados da tabela em JSON"})
# → {"complexity": "simple", "model": "claude-haiku-4-5", ...}
🔧 Implementação Completa: AgentRouter
Juntando tudo: a classe AgentRouter completa com pool de agentes, roteamento dinâmico, load balancing, logging de decisões e métricas de acerto.
import time
import json
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class RoutingDecision:
task_id: str
agent_id: str
model: str
complexity: str
confidence: float
timestamp: float = field(default_factory=time.time)
duration_ms: float = 0.0
class AgentRouter:
"""Router completo com pool, load balancing, custo e logging."""
def __init__(self, agent_registry: dict, pools: dict[str, AgentPool]):
self.registry = agent_registry # agent_type → capabilities
self.pools = pools # agent_type → AgentPool
self.llm_router = LLMRouter(agent_registry)
self.cost_router = CostAwareRouter()
self.decisions_log: list[RoutingDecision] = []
def route(self, task: dict) -> RoutingDecision:
start = time.time()
# 1. Classificar por complexidade (custo)
cost_result = self.cost_router.route(task)
# 2. Selecionar tipo de agente (capacidade)
agent_type, confidence = self.llm_router.route(task)
# 3. Selecionar instância (disponibilidade)
pool = self.pools.get(agent_type)
if pool:
instance_id = pool.get_instance_least_loaded()
pool.mark_busy(instance_id)
else:
instance_id = f"{agent_type}_default"
decision = RoutingDecision(
task_id=task["id"],
agent_id=instance_id,
model=cost_result["model"],
complexity=cost_result["complexity"],
confidence=confidence,
duration_ms=(time.time() - start) * 1000
)
self.decisions_log.append(decision)
print(f"[Router] {task['id']} → {instance_id} ({cost_result['model']}, "
f"conf={confidence:.2f}, {decision.duration_ms:.0f}ms)")
return decision
def get_routing_metrics(self) -> dict:
"""Métricas de acerto do roteador."""
if not self.decisions_log:
return {}
avg_confidence = sum(d.confidence for d in self.decisions_log) / len(self.decisions_log)
avg_latency = sum(d.duration_ms for d in self.decisions_log) / len(self.decisions_log)
complexity_dist = {}
for d in self.decisions_log:
complexity_dist[d.complexity] = complexity_dist.get(d.complexity, 0) + 1
return {
"total_decisions": len(self.decisions_log),
"avg_confidence": round(avg_confidence, 3),
"avg_routing_latency_ms": round(avg_latency, 1),
"complexity_distribution": complexity_dist,
}