MÓDULO 5.2

🔀 Camada 2 — Roteamento Multi-Agente

Dado um pool de agentes especializados, como selecionar o agente certo para cada tarefa — por capacidade, custo e disponibilidade? A Camada 2 é a inteligência de despacho do orquestrador.

6
Tópicos
50
Minutos
Avançado
Nível
Código
Tipo
1

❓ 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.

2

📊 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
3

🤖 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.

4

⚡ 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

Round-robin Tarefas com duração similar e sem estado entre execuções. Simples e eficiente.
Least loaded Tarefas com duração variável. Garante que nenhuma instância fique saturada enquanto outras ficam ociosas.
Health-aware Produção com health checks ativos. Automaticamente exclui instâncias que falharam no último heartbeat.
5

💰 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", ...}
6

🔧 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,
        }

Resumo do Módulo 5.2

Roteamento estático: simples e auditável, mas não escala além de 6 agentes
LLM classifier: roteamento semântico que escala automaticamente com novos agentes
Load balancing distribui carga entre instâncias para evitar gargalos
Roteamento por custo: modelos baratos para tarefas simples — reduz custo em até 90%
AgentRouter combina tudo: capacidade + disponibilidade + custo em uma decisão