No post anterior, eu destrinchei como agents funcionam: LLM, tools e loop. Agora a conversa muda de nível. O problema não é fazer um demo de 5 minutos. É projetar um agent que aguente produção, rode 24/7 e não precise de babá.

Continua sendo design de sistema, com as perguntas de sempre: quais são os requisitos, quais os failure modes, como escala e como monitora?

As 5 decisões de design

Projetar um agent normalmente vira 5 decisões:

  1. Qual tarefa o agent resolve?
  2. Quais tools ele precisa?
  3. Qual modelo usar?
  4. Qual estratégia de planning?
  5. Como garantir reliability?

Dá pra quebrar assim.

Decisão 1: Escopo da tarefa

O erro mais comum é escopo amplo demais. “Um agent que resolve qualquer problema de infra” não funciona. Agents funcionam melhor quando têm escopo claro e ferramentas específicas.

Bom escopo vs ruim

Escopo ruim (amplo demais)Escopo bom (focado)
“Agent de operações genérico”“Agent que diagnostica e remedia alertas de CPU alta”
“Agent que gerencia toda a infra”“Agent que faz scaling de AKS node pools baseado em métricas”
“Assistente que responde tudo”“Agent que classifica e roteia tickets de L1”

Framework pra definir escopo

1. Qual trigger inicia o agent? (alerta, request humano, schedule)
2. Quais inputs ele recebe? (métricas, logs, ticket text)
3. Quais ações ele pode tomar? (lista finita de tools)
4. Qual o resultado esperado? (ação executada, relatório, escalação)
5. Quando ele deve parar e pedir ajuda humana? (critérios claros)

Exemplo pra um “Agent de Triagem de Alertas”:

Trigger: alerta do Azure Monitor
Inputs: nome do recurso, métrica, threshold, valor atual
Ações: verificar métricas, consultar runbook, executar remediação, escalar
Resultado: alerta remediado OU escalado com contexto
Para e pede ajuda: quando remediação falha 2x, quando envolve dados de cliente

Decisão 2: Design de tools

Tools são a interface entre o agent e o mundo. Tool design ruim = agent ruim, independente do modelo.

Princípios de design

Granularidade certa: nem genérica demais, nem específica demais.

# Muito genérica (LLM vai se confundir):
{"name": "run_command", "description": "Executa qualquer comando no servidor"}

# Muito específica (precisa de milhares de tools):
{"name": "get_cpu_usage_web_prod_01", "description": "..."}

# Ideal (ação clara com parâmetros):
{"name": "get_server_metrics", 
 "description": "Retorna CPU, memória e disco de um servidor",
 "parameters": {"hostname": "string"}}

Idempotência: tools que podem ser chamadas múltiplas vezes sem efeitos colaterais indesejados.

# Perigosa: tool que cria recurso sem checar se já existe
def create_alert_rule(name, metric, threshold):
    # Pode duplicar se agent chamar 2x
    az_monitor.create_rule(name, metric, threshold)

# Segura: idempotente
def ensure_alert_rule(name, metric, threshold):
    existing = az_monitor.get_rule(name)
    if existing:
        if existing.threshold != threshold:
            az_monitor.update_rule(name, metric, threshold)
        return {"status": "already_exists", "updated": existing.threshold != threshold}
    else:
        az_monitor.create_rule(name, metric, threshold)
        return {"status": "created"}

Retornos informativos: o agent precisa do resultado pra decidir o próximo passo.

# Retorno ruim (agent não sabe o que aconteceu):
def restart_service(service_name):
    os.system(f"systemctl restart {service_name}")
    return "done"

# Retorno bom (agent tem contexto pra próxima decisão):
def restart_service(service_name):
    result = subprocess.run(
        ["systemctl", "restart", service_name],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        # Verificar se realmente subiu
        status = subprocess.run(
            ["systemctl", "is-active", service_name],
            capture_output=True, text=True
        )
        return {
            "action": "restart",
            "service": service_name,
            "success": status.stdout.strip() == "active",
            "current_status": status.stdout.strip()
        }
    else:
        return {
            "action": "restart",
            "service": service_name,
            "success": False,
            "error": result.stderr
        }

Quantas tools é demais?

Número de toolsComportamento típico
1-5Modelo costuma escolher bem
6-15Ainda funciona bem com descriptions claras
16-30Tools parecidas começam a colidir
30+Vale separar por domínio ou fazer routing antes

Se precisa de 30+ tools, considere tool routing: um primeiro passo que classifica a tarefa e seleciona um subset de tools relevantes.

Decisão 3: Escolha do modelo

CritérioModelo maior (GPT-4o, Claude 3.5)Modelo menor (GPT-4o-mini, Haiku)
Tool selectionMais margem em tarefas ambíguasBoa quando a task é simples e o catálogo é pequeno
Raciocínio complexoMelhorAdequado pra tasks simples
Custo por iteraçãoMais caroMais barato
Latência por iteraçãoMais altaMais baixa
Quando usarTasks complexas, multi-stepClassificação, routing, tasks simples

Pattern: cascata de modelos

def agent_with_cascade(task):
    """Usa modelo menor pra classificar, maior pra executar."""
    
    # Step 1: classificar complexidade (modelo barato)
    complexity = classify_task(task, model="gpt-4o-mini")
    
    if complexity == "simple":
        # Task simples: modelo menor resolve
        return run_agent(task, model="gpt-4o-mini", max_iterations=3)
    else:
        # Task complexa: modelo maior
        return run_agent(task, model="gpt-4o", max_iterations=10)

Decisão 4: Estratégia de planning

Como o agent decompõe tarefas complexas em steps?

No planning (ReAct puro)

O agent decide um step por vez. Simples, mas pode se perder em tarefas longas.

Agent: "Hmm, preciso verificar o servidor"  chama tool
Agent: "Ok, CPU alta. Preciso ver os processos"  chama tool
Agent: "Encontrei o processo. Vou reiniciar"  chama tool

Bom pra: tasks de 1-3 steps. Diagnosis simples.

Plan-then-execute

O agent primeiro cria um plano, depois executa step by step.

def agent_with_planning(task):
    # Step 1: criar plano
    plan = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": 
             "Crie um plano numerado para completar a tarefa. "
             "Cada step deve ser uma ação concreta. Max 5 steps."},
            {"role": "user", "content": task}
        ]
    )
    
    # Step 2: executar cada step do plano
    for step in parse_plan(plan):
        result = execute_step(step)
        if not result["success"]:
            # Replanejar se step falhou
            return replan(task, plan, step, result)
    
    return summarize_results()

Bom pra: tasks de 4-8 steps. Tasks previsíveis.

Hierarchical planning

Decomposição recursiva: tarefa grande → subtarefas → sub-subtarefas.

Planejamento hierárquico de migraçãoTarefa principal de migração de banco dividida em três subtarefas, cada uma com três passos.TarefaMigrar o banco de dados pra nova regiãoSubtarefa 1Verificar pré-requisitosSubtarefa 2Executar migraçãoSubtarefa 3ValidarChecar replicaçãoVerificar espaço em discoConfirmar janela de manutençãoCriar réplica na nova regiãoAguardar syncFazer cutoverTestar conectividadeVerificar integridadeAtualizar DNS

Bom pra: tasks complexas de 10+ steps. Requer agent mais sofisticado.

Decisão 5: Reliability

Agents falham. LLMs alucinam, tools retornam erros, tasks levam mais steps que o esperado. Design pra falha.

Retry com backoff

import time

def execute_tool_with_retry(tool_name, args, max_retries=3):
    for attempt in range(max_retries):
        try:
            result = execute_tool(tool_name, args)
            if result.get("error"):
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)  # exponential backoff
                    continue
            return result
        except Exception as e:
            if attempt == max_retries - 1:
                return {"error": str(e), "failed_after": max_retries}
            time.sleep(2 ** attempt)

Fallback humano

def run_agent_with_fallback(task, confidence_threshold=0.7):
    result = run_agent(task)
    
    # Se o agent não está confiante, escala pra humano
    confidence = evaluate_confidence(result)
    if confidence < confidence_threshold:
        notify_human(
            task=task,
            agent_result=result,
            confidence=confidence,
            reason="Agent não confiante na solução"
        )
        return {"status": "escalated", "reason": "low_confidence"}
    
    return result

Observabilidade

import logging
import time

def run_agent_with_telemetry(task):
    trace_id = generate_trace_id()
    start_time = time.time()
    iterations = 0
    total_tokens = 0
    tools_called = []
    
    # ... agent loop ...
    
    # Log structured
    logging.info("agent_task_complete", extra={
        "trace_id": trace_id,
        "task": task[:100],
        "iterations": iterations,
        "total_tokens": total_tokens,
        "tools_called": tools_called,
        "duration_ms": (time.time() - start_time) * 1000,
        "success": True
    })

Template: Agent de diagnóstico de alertas

Juntando tudo, um agent completo pra diagnosticar alertas:

SYSTEM_PROMPT = """Você é um agent de diagnóstico de infraestrutura.

Quando receber um alerta:
1. Colete métricas do recurso afetado
2. Verifique se há procedimento documentado (runbook)
3. Se houver procedimento simples e seguro, execute
4. Se não houver ou for arriscado, escale pra equipe L2

Regras:
- NUNCA execute ações destrutivas (delete, deallocate) sem aprovação
- Se após 3 ações o problema persistir, escale
- Sempre documente o que fez no ticket
"""

TOOLS = [
    tool_get_metrics,       # Read-only, safe
    tool_get_logs,          # Read-only, safe
    tool_search_runbook,    # Read-only, safe
    tool_restart_service,   # Requires approval if production
    tool_scale_resource,    # Requires approval
    tool_create_ticket,     # Safe (write, but non-destructive)
    tool_escalate,          # Safe (notification)
]

GUARDRAILS = {
    "max_iterations": 8,
    "max_cost_usd": 0.25,
    "approval_required": ["restart_service", "scale_resource"],
    "forbidden_in_production": ["delete_resource"],
    "escalation_after_failures": 2
}

O que levar pra segunda-feira

  • Comece com escopo pequeno. Um agent que faz uma coisa bem vale mais do que um agent que tenta fazer tudo e erra metade.
  • Tool design pesa mais do que parece. Tools idempotentes, com retornos informativos e descriptions claras.
  • Modelo maior não é sempre melhor. GPT-4o-mini pra tasks simples, GPT-4o pra tasks que requerem raciocínio.
  • Planning strategy depende da complexidade. ReAct pra 1-3 steps, plan-then-execute pra 4-8, hierarchical pra 10+.
  • Design pra falha desde o dia 1. Retries, fallback humano, limits de iteração, observabilidade.

O próximo post entra num ponto que separa demo de sistema útil: memória, estado e consistência.

Leitura complementar