AI Engineer: construa um agente de IA real, módulo a módulo
Cada módulo combina teoria explicada, exemplos de código comentados e exercícios práticos. Ao concluí-los, todos os entregáveis se integram em um sistema de agentes pronto para produção.
- módulos
- 10
- entregáveis
- 10
- projeto final
- 1
- duração estimada
- ~10 sem
Projeto final: Nexus Support Agent
Sistema multi-agente de suporte ao cliente. End-to-end: RAG, memória episódica, tool calling, human-in-the-loop, model routing, observabilidade e avaliação contínua em CI.
Arquitetura
| Entrada | Core Agents | Infraestrutura |
|---|---|---|
| Chat API (FastAPI) | Orchestrator | pgvector + RAG |
| Classifier Agent | SupportAgent (ReAct) | Redis (session) |
| MemoryManager | CriticAgent | LangSmith (tracing) |
| Guardrails | EscalationRouter | Eval pipeline (CI) |
Qual componente cada módulo entrega
- M1 →
LLMClient - M2 →
PromptLoader - M3 →
SupportAgent - M4 →
Orchestrator - M5 →
RAG+Memory - M6 →
EscalationRouter - M7 →
ToolRegistry - M8 →
ModelRouter - M9 →
DebugToolkit - M10 →
Observability
Cronograma sugerido
Uma semana por módulo — com integração progressiva ao projeto final.
| Semanas | Fase | O que você constrói |
|---|---|---|
| Semanas 1-2 | Fundamentos | LLMClient + PromptLoader |
| Semanas 3-5 | Arquitetura | Agent + Orchestrator + RAG |
| Semanas 6-7 | Produção | HITL + Tool Layer |
| Semanas 8-9 | Avançado | Otimização + Debug |
| Semana 10 | LLMOps | Observabilidade + Integração |
Módulo 1 · Fase 1 · LLMs & Prompt Engineering
Fundamentos de LLMs
Arquitetura, inferência e seleção de modelos em produção
O que é um LLM e como ele gera texto?
Um Large Language Model é uma rede neural treinada para prever o próximo token dado um contexto. Ele não "entende" como nós entendemos — aprendeu padrões estatísticos em trilhões de tokens de texto. Cada vez que gera uma palavra, está calculando uma distribuição de probabilidade sobre o vocabulário e amostrando dela.
Imagine que você completou milhões de exercícios de "continue esta frase". Com prática suficiente, você desenvolve intuição sobre quais palavras costumam vir depois de quais. Um LLM faz algo parecido, mas em escala massiva e com padrões muito mais complexos.
Arquitetura Transformer — o que você precisa saber
Você não precisa implementar um Transformer, mas precisa entender suas implicações práticas:
Conceitos-chave e seu impacto em produção
- Self-attention: cada token "presta atenção" a todos os outros do contexto. Implicação: o modelo consegue relacionar informações que estão distantes no texto.
- Janela de contexto: limite máximo de tokens ativos. GPT-4: 128K, Claude 3.5: 200K, Gemini 1.5 Pro: 1M. Mais contexto = mais custo e latência.
- Decoder-only: os modelos modernos (GPT, Claude, Gemini) só geram — não têm um encoder separado. Processam todo o contexto cada vez que geram um token.
- Tokenização (BPE): o texto é dividido em subpalavras. "tokenização" pode virar 3-4 tokens. O custo real de uma chamada depende do número de tokens, não de palavras nem de caracteres.
Parâmetros de inferência — o painel de controle
Quando você faz uma chamada à API, estes parâmetros determinam como o modelo amostra a resposta:
temperature
0 = determinístico (sempre o token mais provável). 1 = mais variado. Para agentes em produção: use 0–0.3. Para geração criativa: 0.7–1.0.
top_p (nucleus sampling)
Considera apenas os tokens cuja probabilidade acumulada chega a p. top_p=0.9 ignora os 10% de tokens menos prováveis. É uma alternativa à temperature; não se usam juntos.
max_tokens
Limite de tokens na resposta. Impacta diretamente custo e latência. Para respostas estruturadas (JSON), um limite baixo evita respostas truncadas de forma inesperada.
stop_sequences
O modelo para de gerar quando encontra essa string. Útil para delimitar outputs: ["</response>", "###"]. Mais confiável que max_tokens para outputs estruturados.
Usar temperature=0 não garante outputs idênticos. Os LLMs podem variar mesmo com temperature=0 por diferenças de hardware e paralelismo. Para reprodutibilidade exata, guarde o input completo e o output.
Seleção de modelo — o trade-off mais importante
Guia de seleção para produção 2025
- Claude 3 Haiku / GPT-4o-mini: classificação, routing, extração simples. ~$0.25/M tokens. Latência: <1s. Use quando o erro tem baixo impacto.
- Claude 3.5 Sonnet / GPT-4o: raciocínio, geração, tool calling complexo. ~$3/M tokens. Equilíbrio ideal para a maioria dos agentes em produção.
- Claude 3 Opus / GPT-4-turbo: análise profunda, decisões de alto impacto. ~$15/M tokens. Só quando a qualidade é crítica e o custo é secundário.
- Mistral / LLaMA 3 (self-hosted): dados sensíveis, conformidade regulatória, custo em escala extrema. Exige infraestrutura própria.
70-80% das consultas em um sistema de suporte são simples. Classificar automaticamente a complexidade e usar Haiku nos casos simples pode reduzir o custo total em 60% sem impacto perceptível na qualidade.
LLMClient — wrapper base do sistema
O padrão correto não é chamar o SDK diretamente de cada agente. Cria-se um wrapper centralizado que cuida de: retry automático, logging estruturado, contagem de tokens e seleção de modelo.
from anthropic import Anthropic
from tenacity import retry, stop_after_attempt, wait_exponential
from enum import Enum
import structlog, time
log = structlog.get_logger()
class ModelTier(Enum):
FAST = "claude-3-haiku-20240307" # barato y rápido
STANDARD = "claude-3-5-sonnet-20241022" # balance ideal
POWERFUL = "claude-3-opus-20240229" # máxima calidad
class LLMResponse:
text: str
input_tokens: int
output_tokens: int
cost_usd: float
latency_ms: float
class LLMClient:
def __init__(self):
self.client = Anthropic()
self.cost_per_token = {
ModelTier.FAST: (0.00025, 0.00125), # (input, output) por 1K tokens
ModelTier.STANDARD: (0.003, 0.015),
ModelTier.POWERFUL: (0.015, 0.075),
}
@retry(stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10))
def call(
self,
messages: list[dict],
model: ModelTier = ModelTier.STANDARD,
temperature: float = 0.3,
max_tokens: int = 1024,
trace_id: str = None,
) -> LLMResponse:
start = time.time()
response = self.client.messages.create(
model=model.value,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
latency = (time.time() - start) * 1000
cost = self._calculate_cost(model, response.usage)
# Log estructurado para observabilidad
log.info("llm_call",
trace_id=trace_id,
model=model.value,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
cost_usd=round(cost, 6),
latency_ms=round(latency, 1),
)
return LLMResponse(
text=response.content[0].text,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
cost_usd=cost,
latency_ms=latency,
)
def _calculate_cost(self, model, usage) -> float:
inp, out = self.cost_per_token[model]
return (usage.input_tokens/1000*inp) + (usage.output_tokens/1000*out)
O decorator @retry do tenacity lida automaticamente com rate limits (429) e erros temporários da API com backoff exponencial. Sem isso, qualquer falha transitória quebra o fluxo do agente.
Recursos
anthropic-sdk docs, tenacity, structlog, tiktoken
Benchmark de temperature
Entender empiricamente como a temperature afeta o output antes de escolher o valor para produção.
- Escreva um prompt que peça para classificar o sentimento de uma frase (positivo/negativo/neutro)
- Execute a mesma chamada 10 vezes com temperature=0 — o resultado é sempre o mesmo?
- Repita com temperature=0.5 e temperature=1.0 — o que muda?
- Registre o custo e a latência de cada chamada — a temperature afeta o custo?
- Conclusão: qual temperature você escolheria para o classificador de intent do projeto?
Contagem de tokens e estimativa de custo
Antes de projetar o sistema, saber quanto vai custar cada chamada.
- Escreva o system prompt do agente de suporte (rascunho inicial, ~200 palavras)
- Use
tiktokenpara contar quantos tokens ele ocupa - Simule 1000 conversas de 5 turnos: calcule o custo total com Haiku vs Sonnet
- Que porcentagem do custo vem do system prompt vs do histórico?
- Documente qual modelo você escolheria, e por quê, para o classificador de intent
Entregável do módulo
Vai para o projeto final: Wrapper LLMClient. Classe Python pronta para produção que encapsula todas as chamadas à API. Todos os agentes do projeto vão usar este wrapper — nunca o SDK diretamente. O LLMClient é a camada base do sistema. No M8 (Model Router) ele será estendido para selecionar o modelo dinamicamente por consulta, em vez de recebê-lo como parâmetro fixo.
Módulo 2 · Fase 1 · LLMs & Prompt Engineering
Prompt Engineering Avançado
System prompts, constraints, CoT controlado e versionamento
O system prompt é a constituição do agente
O system prompt não é uma "instrução inicial" — é o documento que define por completo quem é o agente, o que ele pode fazer, como deve se comportar e quando deve pedir ajuda. Um agente sem um system prompt bem estruturado é um agente imprevisível.
Estrutura em 6 seções (todas obrigatórias)
- IDENTITY: nome, propósito, personalidade. Define o "quem" do agente.
- CAPABILITIES: lista explícita do que ele pode e NÃO pode fazer. Os "não pode" são tão importantes quanto os "pode".
- CONTEXT: variáveis dinâmicas do ambiente: usuário, estado, ferramentas disponíveis.
- BEHAVIOR RULES: como agir em situações específicas — edge cases explícitos.
- OUTPUT FORMAT: estrutura exata, tamanho e canal da resposta.
- ESCALATION: critérios exatos para transferir para um humano.
O que não está explícito no system prompt, o modelo inventa. Cada comportamento esperado precisa estar especificado. A ambiguidade no prompt é a origem de 80% dos bugs em agentes.
Chain-of-Thought controlado — separar raciocínio de resposta
O CoT (Chain-of-Thought) melhora a qualidade do raciocínio, mas em produção não queremos mostrar o processo interno ao usuário. O padrão correto é separar os dois:
CoT sem controle
- O usuário vê o raciocínio interno
- Expõe lógica que pode ser manipulada
- Aumenta tokens de output sem necessidade
- Dificulta o parsing da resposta final
CoT com <thinking> separado
- O raciocínio fica em logs internos
- Permite debugging sem exposição ao usuário
- O output final é limpo e parseável
- Você pode monitorar a qualidade do raciocínio
Few-shot com exemplos negativos
Os exemplos positivos ensinam o comportamento esperado. Os exemplos negativos são igualmente críticos — mostram ao modelo exatamente o que evitar. Sem eles, o modelo pode cair em respostas "razoáveis, mas incorretas".
Incluir só exemplos positivos no few-shot. O modelo aprende "o que fazer", mas não "o que NÃO fazer". Os edge cases e as falhas mais frequentes devem aparecer como exemplos negativos explícitos.
Prompts como código — versionamento e testes
Um prompt que muda sem controle é uma regressão silenciosa. A mesma disciplina que aplicamos ao código vale para os prompts:
Pipeline de deployment de prompts
- PR no git com a mudança de prompt + justificativa na descrição
- Avaliação automática em CI contra o test set base
- Deploy em staging → 10% do tráfego → 48h de monitoramento
- Se as métricas estiverem OK → promover para 100%. Se piorarem → rollback automático
System prompt completo com CoT controlado
IDENTITY:
Eres SupportBot, asistente de atención al cliente de Nexus.
Objetivo: resolver consultas de soporte con empatía y precisión.
Tono: cercano, claro, sin jerga técnica innecesaria.
CAPABILITIES:
✓ Puedes: get_order_status, create_ticket, send_notification, schedule_callback
✗ NO puedes: modificar precios, eliminar cuentas, acceder a datos de pago
CONTEXT:
Usuario: {{user_name}} | Plan: {{plan_name}} | Estado: {{account_status}}
Canal: {{channel}} | Herramientas: {{available_tools}}
BEHAVIOR RULES:
- Lenguaje agresivo → desescalar sin confrontar: "Entiendo tu frustración,
mi objetivo es encontrar una solución que funcione para ti."
- Solicitud fuera de alcance → explicar límite + ofrecer alternativa real
- Input ambiguo → preguntar UNA cosa antes de actuar
- Señal de crisis o urgencia alta → escalar a humano inmediatamente
ESCALATION:
Transferir SIEMPRE cuando: ticket_priority="critical" OR usuario solicita
hablar con persona OR confidence_score < 0.70
REASONING FORMAT:
Antes de responder, razona en <thinking>:
1. ¿Qué pide exactamente el usuario?
2. ¿Qué información tengo vs qué me falta?
3. ¿Qué regla de comportamiento aplica?
4. ¿Debo escalar o puedo resolver?
El contenido de <thinking> NO se muestra al usuario.
OUTPUT FORMAT:
- Máx 3 oraciones por turno (canal: chat/WhatsApp)
- Cuando uses herramienta: responde SOLO JSON válido sin texto adicional:
{"action": "<tool>", "params": {...}, "reason": "<1 oración>", "confidence": 0.0-1.0}
PromptLoader — gestão centralizada de versões
import os
from pathlib import Path
from jinja2 import Template
PROMPTS_DIR = Path("prompts")
class PromptLoader:
def get(self, agent: str, version: str, context: dict) -> str:
"""Carga un prompt versionado e inyecta variables de contexto."""
path = PROMPTS_DIR / agent / f"v{version}_system.txt"
template_str = path.read_text(encoding="utf-8")
return Template(template_str).render(**context)
def latest(self, agent: str) -> str:
"""Lee la versión actual desde el archivo VERSION del agente."""
version_file = PROMPTS_DIR / agent / "VERSION"
return version_file.read_text().strip()
def load_latest(self, agent: str, context: dict) -> str:
"""Atajo: carga siempre la versión más reciente."""
return self.get(agent, self.latest(agent), context)
# Uso en un agente:
loader = PromptLoader()
system_prompt = loader.load_latest("support_agent", {
"user_name": "Ana López",
"plan_name": "Pro",
"account_status": "active",
"channel": "whatsapp",
"available_tools": "[get_order_status, create_ticket]",
})Recursos
jinja2, jsonlines, Anthropic prompting guide, Learn Prompting
Construção guiada do system prompt
Escreva o system prompt completo do SupportBot seguindo a estrutura de 6 seções.
- Escreva uma primeira versão sem estrutura — só o que vier naturalmente à cabeça
- Avalie: qual seção está faltando? Há ambiguidade em alguma regra?
- Reescreva usando as 6 seções. Adicione pelo menos 3 BEHAVIOR RULES específicas
- Teste o prompt enviando 5 mensagens edge case: input agressivo, pedido impossível, input ambíguo, pedido de dados sensíveis e uma consulta válida normal
- Ajuste as regras conforme os resultados e documente o que mudou no CHANGELOG.md
Few-shot com casos negativos
O few-shot mais valioso inclui exemplos do que NÃO fazer, não só do que fazer.
- Identifique os 3 tipos de erro mais comuns que o SupportBot poderia cometer (ex.: presumir antes de perguntar, responder fora do escopo, usar o tom errado)
- Para cada erro, escreva um par (mensagem do usuário → resposta INCORRETA do bot)
- Depois escreva a resposta CORRETA para a mesma mensagem
- Adicione esses 3 pares negativos ao system prompt e teste de novo com as 5 mensagens do EX1
- O comportamento melhorou? Em quais casos?
Entregável do módulo
Vai para o projeto final: Prompt Library v1.0 + PromptLoader. System prompts versionados para os 3 agentes do projeto (support, classifier, critic), com variáveis dinâmicas, test set base e um loader que injeta contexto em tempo de execução. O PromptLoader é usado por todos os agentes. No M10, o eval pipeline vai se conectar a ele para rodar o test set automaticamente em cada PR que modificar um prompt.
Módulo 3 · Fase 2 · Agentes & Memória
Padrões de Agentes
Planner/Executor, ReAct loop, Tool-using e Critic
O que é um agente LLM?
Um agente é um sistema em que o LLM não só gera texto — ele também toma decisões sobre quais ações executar, observa os resultados dessas ações e decide o que fazer em seguida. A diferença para um simples chatbot é que o agente tem agência: pode agir sobre o mundo.
Um chatbot é como um funcionário que só pode dar respostas verbais. Um agente é como um funcionário que também pode abrir sistemas, enviar e-mails, criar tickets e buscar informações — tudo em resposta ao que o cliente precisa.
Padrão ReAct — Reason + Act
ReAct é o padrão mais usado em produção. A cada turno, o agente: (1) raciocina sobre o estado atual, (2) decide uma ação, (3) observa o resultado e repete até ter informação suficiente para responder ao usuário.
Ciclo ReAct passo a passo
- Thought: "O usuário pergunta pelo pedido. Preciso chamar get_order_status com o ID dele."
- Action:
get_order_status(order_id="ORD-123") - Observation:
{"status": "en camino", "eta": "mañana 14:00"} - Thought: "Tenho a informação necessária. Posso responder ao usuário."
- FINISH: "Seu pedido está a caminho e vai chegar amanhã antes das 14:00."
Sem um hard limit de MAX_ITERATIONS, um agente pode ficar em ciclo indefinidamente se uma ferramenta falhar repetidamente ou se o raciocínio não convergir. Esse limite deve ficar no orquestrador, não no prompt.
Critic loop — o agente avalia o próprio output
O Critic é um segundo agente (ou uma segunda chamada ao LLM) que avalia a resposta do agente principal antes de enviá-la ao usuário. Responde PASS/FAIL + motivo. Dobra o custo, mas aumenta significativamente a qualidade em casos de alto impacto.
Use quando o custo de uma resposta incorreta for maior que o custo da chamada extra. Em suporte: quando o agente vai criar um ticket ou enviar uma notificação. Não use em toda resposta — só em ações com efeitos colaterais.
from dataclasses import dataclass, field
from enum import Enum
class AgentAction(Enum):
FINISH = "FINISH"
ESCALATE = "ESCALATE"
TOOL = "TOOL"
@dataclass
class AgentThought:
reasoning: str # contenido del <thinking>
action: AgentAction
tool_name: str | None = None
tool_params: dict = field(default_factory=dict)
final_answer: str | None = None
confidence: float = 1.0
class SupportAgent:
max_iterations = 8
def __init__(self, llm_client, tool_registry, prompt_loader):
self.llm = llm_client
self.tools = tool_registry
self.loader = prompt_loader
def run(self, user_message: str, context: dict) -> AgentResult:
system = self.loader.load_latest("support_agent", context)
history = []
for i in range(self.max_iterations):
# Paso 1: REASON — el agente piensa qué hacer
messages = self._build_messages(system, user_message, history)
response = self.llm.call(messages, trace_id=context["trace_id"])
thought = self._parse_thought(response.text)
# Paso 2: verificar stopping criteria
if thought.action == AgentAction.FINISH:
return AgentResult(answer=thought.final_answer, iterations=i+1)
if thought.action == AgentAction.ESCALATE:
return AgentResult(escalate=True, reason=thought.reasoning, iterations=i+1)
# Paso 3: ACT — ejecutar la herramienta
observation = self.tools.execute(thought.tool_name, thought.tool_params)
# Paso 4: OBSERVE — agregar al historial
history.append({"thought": thought, "observation": observation})
# MAX_ITERATIONS alcanzado → siempre escalar, nunca lanzar excepción
return AgentResult(escalate=True, reason="max_iterations_reached", iterations=self.max_iterations)
class CriticAgent:
def evaluate(self, agent_result: AgentResult, original_query: str) -> CriticVerdict:
"""Evalúa si la respuesta del agente es correcta antes de enviarla."""
prompt = f"""
Evalúa esta respuesta de soporte:
Consulta original: {original_query}
Respuesta del agente: {agent_result.answer}
Responde SOLO con JSON:
{{"status": "PASS" o "FAIL", "reason": "...", "suggestion": "..."}}
"""
response = self.llm.call([{"role": "user", "content": prompt}],
model=ModelTier.FAST) # Haiku para el critic = más barato
return CriticVerdict(**json.loads(response.text))Recursos
ReAct paper (Yao 2022), Anthropic tool use, pydantic v2
Implemente o loop ReAct do zero
Antes de usar o código base, entenda o padrão implementando-o você mesmo com um caso simples.
- Crie uma ferramenta falsa
get_weather(city)que retorna JSON hardcoded - Implemente o loop ReAct em ~30 linhas: reason → parse action → execute → observe → repeat
- Teste com: "Qual a temperatura em Madri?" — o agente deveria chamar a ferramenta
- Agora teste com: "Me conte uma piada" — o agente deveria terminar em 1 iteração sem usar ferramenta
- Force o loop infinito: faça
get_weathersempre retornar erro. O MAX_ITERATIONS funciona?
Construa o CriticAgent e teste a eficácia dele
Avaliar se o Critic realmente melhora a qualidade do sistema.
- Implemente o CriticAgent com o prompt do código base
- Gere 10 respostas do SupportAgent para consultas variadas
- Avalie cada uma com o Critic — quantas passam? Quantas falham e por quê?
- Nas que falham: o Critic tem razão? Há falsos positivos?
- Meça o custo adicional do Critic: quanto ele acrescenta por consulta? Vale a pena?
Entregável do módulo
Vai para o projeto final: SupportAgent Core + CriticAgent. Agente principal com ReAct loop, MAX_ITERATIONS, stopping criteria e escalonamento. Mais um CriticAgent que avalia respostas de alto impacto antes de enviá-las. O SupportAgent é o motor do sistema. No M4 ele será envolvido pelo Orchestrator. O CriticAgent vai se conectar ao pipeline de avaliação do M10 para medir qualidade em produção.
Módulo 4 · Fase 2 · Agentes & Memória
Multi-Agent Orchestration
Orquestrador, classifier, handoffs tipados e timeout global
Hub-and-spoke: o padrão mais robusto para produção
Um orquestrador central recebe todas as mensagens, classifica-as com um agente leve (barato e rápido) e delega ao agente especializado correto com o contexto completo.
Como uma recepcionista de hospital: ela não faz o diagnóstico, mas sabe exatamente para qual especialista te encaminhar. O classificador é a recepcionista — rápido, barato e com critério de routing.
O classificador é a peça mais crítica do sistema
- Use o modelo mais barato: Haiku com um prompt de 5 linhas classifica melhor que Sonnet com um prompt ambíguo
- Categorias exaustivas: toda consulta precisa cair em alguma categoria — inclua "GENERAL/OTHER"
- Output tipado: o classifier nunca retorna texto livre — retorna um enum com a categoria
- Fallback seguro: se o classifier falhar, o sistema roteia para o agente geral — nunca quebra
O erro mais frequente em multi-agent: o agente B recebe a mensagem do usuário, mas não sabe o que o agente A fez. O handoff deve incluir: histórico completo, ação já tomada e motivo da transferência.
class Intent(Enum):
ORDER_STATUS = "order_status"
CREATE_TICKET = "create_ticket"
ESCALATE = "escalate"
GENERAL = "general"
@dataclass
class AgentHandoff:
"""Contexto completo que pasa entre agentes en un handoff."""
user_id: str
user_message: str
intent: Intent
conversation_history: list[dict]
previous_actions: list[str] # qué ya intentó el agente anterior
context: dict # datos del usuario (plan, status, etc.)
trace_id: str
class Orchestrator:
global_timeout = 30 # segundos — nunca un workflow dura más
def handle(self, user_id: str, message: str) -> OrchestratorResponse:
context = self.context_builder.build(user_id)
trace_id = self._new_trace_id()
# 1. Clasificar intención con modelo barato (Haiku)
intent = self.classifier.classify(message, context)
# 2. Construir handoff con contexto completo
handoff = AgentHandoff(
user_id=user_id, user_message=message, intent=intent,
conversation_history=self.session.get_history(user_id),
previous_actions=[], context=context, trace_id=trace_id
)
# 3. Routing al agente correcto con timeout global
with timeout(self.global_timeout):
agent = self.router[intent]
result = agent.run(handoff)
# 4. Evaluar si escalar antes de responder al usuario
verdict = self.escalation_router.evaluate(result, context)
if verdict.should_escalate:
return self._escalate(handoff, verdict.reason)
return OrchestratorResponse(message=result.answer, trace_id=trace_id)Recursos
signal (timeout), claude-3-haiku, pydantic
Desenhe o esquema de routing
Antes de implementar, desenhe o mapa completo de intenções e agentes.
- Liste todas as consultas possíveis de um usuário de suporte (pelo menos 15)
- Agrupe em categorias — de quantos agentes você realmente precisa?
- Escreva o prompt do classificador com todas as categorias
- Teste o classifier com as 15 consultas — ele classifica corretamente?
- Ajuste até alcançar >90% de accuracy nas 15 consultas
Simule um handoff com falha
Entender o que acontece quando o contexto do handoff está incompleto.
- Implemente um handoff mínimo: passa só a mensagem do usuário, sem histórico nem contexto
- Teste com: um usuário que retoma uma conversa anterior
- O agente B "sabe" o que o agente A fez? Responde corretamente?
- Adicione o histórico completo ao handoff e repita. Melhora?
- Documente quais campos do AgentHandoff são indispensáveis
Entregável do módulo
Vai para o projeto final: Orchestrator + ClassifierAgent. Orquestrador com routing, timeout global e handoffs tipados. ClassifierAgent com Haiku que roteia as consultas corretamente. O Orchestrator é o ponto de entrada da API. No M6 ele ganha o EscalationRouter na saída, e no M7 o ToolRegistry é injetado no SupportAgent que o Orchestrator coordena.
Módulo 5 · Fase 2 · Agentes & Memória
Memória, Contexto e RAG
Embeddings, vector store, retrieval e estratégias de memória
O problema da memória em LLMs
Por padrão, um LLM não lembra de nada entre sessões. Cada chamada à API é stateless. Para um agente de suporte, isso é um problema: o usuário não deveria ter que repetir o problema dele a cada interação.
Os 4 tipos de memória e quando usar cada um
- Short-term (janela ativa): histórico do turno atual no contexto do LLM. Sem custo adicional, se perde ao encerrar a sessão.
- Long-term (vector store): knowledge base do domínio. Busca semântica por similaridade de embeddings. Para documentação, FAQs, políticas.
- Episodic (histórico de interações): o que o usuário disse em sessões anteriores. Banco de dados estruturado com timestamp.
- Semantic (entidades do usuário): dados persistentes: plano, preferências, histórico de tickets. Structured DB.
Short-term = o que ele lembra desta ligação. Long-term = o manual de suporte que ele consultou. Episodic = anotações de ligações anteriores com este cliente. Semantic = ficha do cliente com seus dados e plano.
Pipeline RAG — como funciona em produção
Os 4 passos do pipeline
- Ingestion: documento → chunking (512 tokens, 10% de overlap) → embedding → vector store + metadata
- Retrieval: query → embed → ANN search (top-10) → filtro por metadata → reranking → top-3
- Augmentation: chunks recuperados → injetar no contexto do LLM
- Evaluation: a resposta usa os chunks? Os chunks eram relevantes?
Chunks muito pequenos (< 200 tokens) perdem contexto. Chunks muito grandes (> 1500 tokens) introduzem ruído. Experimente com o seu domínio específico — não existe um tamanho ótimo universal.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.vector_stores.postgres import PGVectorStore
import redis
class MemoryManager:
def __init__(self, pg_conn_str: str, redis_url: str):
self.vector_store = PGVectorStore.from_params(pg_conn_str, embed_dim=1536)
self.session = redis.from_url(redis_url)
self.index = VectorStoreIndex.from_vector_store(self.vector_store)
def get_context(self, query: str, user_id: str) -> MemoryContext:
return MemoryContext(
# Short-term: historial de la sesión actual
short_term = self._get_session(user_id),
# Long-term: knowledge base del dominio (RAG)
long_term = self._retrieve_relevant(query, k=3),
# Episodic: últimas 3 interacciones del usuario
episodic = self._get_recent_episodes(user_id, n=3),
)
def _retrieve_relevant(self, query: str, k: int) -> list[str]:
retriever = self.index.as_retriever(similarity_top_k=k*3) # más para re-rankear
nodes = retriever.retrieve(query)
# Reranking: ordenar por relevancia real, no solo similitud vectorial
reranked = sorted(nodes, key=lambda n: n.score, reverse=True)[:k]
return [n.text for n in reranked]
def _get_session(self, user_id: str) -> list[dict]:
raw = self.session.get(f"session:{user_id}")
return json.loads(raw) if raw else []
def save_turn(self, user_id: str, user_msg: str, agent_response: str):
history = self._get_session(user_id)
history.append({"user": user_msg, "agent": agent_response})
self.session.setex(f"session:{user_id}", 3600, json.dumps(history)) # TTL 1hRecursos
llama-index, pgvector, redis-py, Cohere Rerank, RAGAS (eval)
Experimente com chunking
O tamanho do chunk é a variável de maior impacto na qualidade do RAG.
- Pegue 5 documentos de suporte (FAQs, guias, políticas)
- Indexe com chunk_size=256 tokens
- Faça 10 perguntas sobre o conteúdo — que % ele responde corretamente?
- Reindexe com chunk_size=512 e chunk_size=1024. Repita as perguntas
- Qual tamanho dá os melhores resultados para o seu domínio? Por quê?
Meça o impacto da memória episódica
Quantificar se a memória episódica melhora a experiência real.
- Simule uma conversa de 2 sessões: na primeira, o usuário reporta um problema. Na segunda, ele volta com o mesmo problema
- Teste sem memória episódica: o agente lembra do contexto anterior?
- Ative a memória episódica e repita. O agente responde de forma diferente?
- Meça o custo extra de incluir o histórico episódico no contexto
- Vale a pena? Documente a decisão em um ADR
Entregável do módulo
Vai para o projeto final: MemoryManager + RAG Pipeline. Pipeline completo de ingestion e retrieval, mais a classe MemoryManager que gerencia os 3 tipos de memória do agente. O MemoryManager é injetado no Orchestrator. Antes de cada chamada ao agente, o sistema recupera o contexto relevante (RAG + episódico) e o adiciona ao prompt dinamicamente.
Módulo 6 · Fase 3 · Produção & Integração
Human-in-the-Loop
Critérios de escalonamento, fallbacks em cascata e circuit breaker
Human-in-the-loop não é edge case — é design
O erro mais comum é tratar o escalonamento como algo excepcional. Em produção, entre 10-30% das interações vão terminar em um humano. O sistema deve ser projetado para isso desde o início, não receber isso depois.
Defina os critérios de escalonamento ANTES de ir para produção. Se você os define quando já há incidentes, está escolhendo sob pressão e sem dados. Os critérios devem ser configuráveis por ambiente e mensuráveis no dashboard.
5 tipos de critérios de escalonamento
- Limite de negócio: ticket_priority="critical", account_type="enterprise"
- Confiança baixa: confidence_score < 0.70 na ação a ser tomada
- Fora do escopo: o agente não consegue resolver a solicitação
- Pedido explícito: o usuário pede para falar com uma pessoa
- Sinal emocional: crise, urgência extrema, frustração acumulada
Fallback em cascata — o sistema nunca morre
Um sistema de produção deve responder sempre, mesmo quando tudo falha. O padrão de fallback em cascata define uma cadeia de degradação gradual:
Cadeia de fallback
- Nível 1: SupportAgent com Sonnet (normal)
- Nível 2: SupportAgent com Haiku (mais rápido e barato se houver latência)
- Nível 3: Resposta genérica hardcoded + escalonamento automático para humano
- Nível 4: Mensagem de erro amigável com número de ticket criado automaticamente
from pybreaker import CircuitBreaker, CircuitBreakerError
# Circuit breaker por herramienta — evita cascada de fallos
ticket_breaker = CircuitBreaker(fail_max=5, reset_timeout=60)
class EscalationRule:
name: str
check: callable # función que recibe (result, context) → bool
reason: str
class EscalationRouter:
rules: list[EscalationRule] = [
EscalationRule("critical_ticket",
lambda r, ctx: ctx.get("ticket_priority") == "critical", "ticket_critico"),
EscalationRule("low_confidence",
lambda r, ctx: r.confidence < 0.70, "confianza_baja"),
EscalationRule("user_requested",
lambda r, ctx: ctx.get("user_requested_human", False), "usuario_solicito"),
]
def evaluate(self, result, context) -> Verdict:
for rule in self.rules:
if rule.check(result, context):
audit_log.record("escalation", rule=rule.name)
return Verdict(should_escalate=True, reason=rule.reason)
return Verdict(should_escalate=False)
class FallbackChain:
def run(self, handoff: AgentHandoff) -> AgentResult:
try:
return self.support_agent.run(handoff) # Nivel 1: normal
except (TimeoutError, RateLimitError):
try:
return self.support_agent_fast.run(handoff) # Nivel 2: modelo barato
except Exception:
return self._static_fallback(handoff) # Nivel 3: respuesta fija
def _static_fallback(self, handoff) -> AgentResult:
ticket_id = self._create_fallback_ticket(handoff)
return AgentResult(
answer=f"Estamos experimentando problemas técnicos. Creamos el ticket #{ticket_id} y un agente te contactará pronto.",
escalate=True, reason="system_fallback"
)Recursos
pybreaker, structlog, pydantic
Defina e teste os critérios de escalonamento
Critérios mal definidos geram escalonamentos demais ou de menos — os dois custam caro.
- Defina 5 critérios de escalonamento para o SupportBot. Escreva-os como condições exatas
- Crie 10 cenários de teste: 5 que devem escalar, 5 que não devem
- Implemente o EscalationRouter e execute os 10 cenários
- Quantos falsos positivos (escala quando não deveria)? E falsos negativos?
- Ajuste os limites até chegar a 0 falsos negativos (prioridade) e menos de 10% de falsos positivos
Entregável do módulo
Vai para o projeto final: EscalationRouter + FallbackChain. Módulo completo de segurança: 5 critérios de escalonamento configuráveis, fallback em cascata de 3 níveis, circuit breaker e audit log. O EscalationRouter se conecta à saída do Orchestrator. No M10, a taxa de escalonamento vira uma métrica de negócio no dashboard de observabilidade.
Módulo 7 · Fase 3 · Produção & Integração
Tool Layer & APIs Externas
Wrappers tipados, idempotência, state management e tool registry
O agente nunca toca a infraestrutura diretamente
A regra mais importante do tool layer: o agente chama contratos (schemas tipados), não implementações. Isso permite trocar a implementação subjacente sem mexer no agente, e testar o agente com mocks sem infraestrutura real.
Anatomia de uma ferramenta bem projetada
- Schema tipado: model Pydantic com validação, descriptions e constraints
- Modo dry-run: validar sem executar efeitos — permite verificar antes de agir
- Timeout por ferramenta: cada tool tem seu próprio SLA — não o global do workflow
- Idempotência: executar a mesma ferramenta 2 vezes com os mesmos params = mesmo resultado
- Audit log: toda execução fica registrada — bem-sucedida ou com falha
O modelo pode gerar params fora do intervalo, tipos incorretos ou campos obrigatórios vazios. Nunca confie no output do LLM sem validação. O Pydantic lança ValidationError antes que a ação chegue ao serviço.
from pydantic import BaseModel, Field
from typing import Literal
# 1. Schema tipado — lo que el LLM ve y debe rellenar
class CreateTicketParams(BaseModel):
user_id: str = Field(description="ID único del usuario")
subject: str = Field(min_length=5, description="Asunto del ticket")
priority: Literal["low","medium","high","critical"]
category: str = Field(description="Categoría: billing, technical, general")
notes: str | None = None
# 2. Implementación con todas las capas de seguridad
class CreateTicketTool:
name = "create_ticket"
timeout = 5 # segundos
def execute(self, raw_params: dict) -> dict:
# Validación — lanza ValidationError si algo está mal
params = CreateTicketParams(**raw_params)
# Dry-run check — ¿hay conflicto con un ticket abierto?
existing = self.ticket_service.get_open(params.user_id)
if existing and existing.subject.lower() == params.subject.lower():
return {"warning": "duplicate_ticket", "existing_id": existing.id}
# Ejecución con timeout
with timeout(self.timeout):
result = self.ticket_service.create(params)
# Audit log — inmutable
audit_log.record(tool=self.name, params=params.dict(),
result={"ticket_id": result.id}, user_id=params.user_id)
return {"ticket_id": result.id, "status": "created"}
# 3. Registry — el agente solo conoce el registry, no las implementaciones
class ToolRegistry:
def __init__(self):
self._tools = {
"create_ticket": CreateTicketTool(),
"get_order_status": GetOrderStatusTool(),
"send_notification": SendNotificationTool(),
"schedule_callback": ScheduleCallbackTool(),
}
def execute(self, name: str, params: dict) -> dict:
if name not in self._tools:
raise ValueError(f"Herramienta desconocida: {name}")
return self._tools[name].execute(params)
def get_schemas(self) -> list[dict]:
# Genera los schemas para el API de Anthropic automáticamente
return [t.get_anthropic_schema() for t in self._tools.values()]Recursos
pydantic v2, redis-py, httpx (async)
Implemente as 4 ferramentas com seus testes
Cada ferramenta deve ter pelo menos 3 testes: happy path, parâmetros inválidos e timeout.
- Implemente
create_ticketcom mock do serviço de tickets - Escreva um teste: o que acontece se user_id estiver vazio?
- Escreva um teste: o que acontece se o serviço demorar mais de 5 segundos?
- Implemente
get_order_status,send_notificationeschedule_callbackcom a mesma estrutura - Verifique se o ToolRegistry gera corretamente os schemas para a API da Anthropic
Entregável do módulo
Vai para o projeto final: ToolRegistry + SessionManager. 4 ferramentas tipadas com validação, timeout e audit log. Registry centralizado. SessionManager para persistência entre turnos. O ToolRegistry é injetado no SupportAgent. Quando o agente decide usar uma ferramenta no loop ReAct, passa pelo registry — nunca chama o serviço diretamente.
Módulo 8 · Fase 4 · Trade-offs & Debugging
Trade-offs & Otimização
Model routing, prompt caching, context compression e decisões de arquitetura
Os 4 trade-offs que todo sênior precisa dominar
Latência vs Qualidade
Haiku responde em <500ms. Sonnet leva 1-3s. Opus pode levar 5-10s. A pergunta não é "qual é melhor", e sim "de qual o usuário precisa neste contexto".
Custo vs Profundidade
Sonnet custa 12x mais que Haiku. Para classificação (simples), Haiku basta. Para raciocínio complexo com ferramentas, Sonnet vale cada centavo.
Autonomia vs Controle
Mais autonomia = melhor experiência do usuário. Mais controle = menos risco de erros caros. A resposta depende da reversibilidade da ação.
Agente vs Pipeline
Se o fluxo sempre segue os mesmos passos, um DAG determinístico é mais rápido, barato e previsível. O agente agrega valor quando o input é ambíguo.
O engenheiro que sabe quando NÃO usar um agente vale mais do que aquele que usa agentes em tudo. Perguntar "eu realmente preciso de um agente aqui?" é a diferença entre soluções elegantes e sistemas complexos demais.
Model Routing — a otimização de maior impacto
O princípio: usar o modelo mais barato que resolve o caso corretamente. Um classificador leve (Haiku) decide de qual modelo cada consulta precisa. 70-80% das consultas de suporte são simples e podem ser resolvidas com Haiku.
Estratégias de redução de custo
- Prompt caching: a parte estática do system prompt fica em cache. A Anthropic oferece 90% de desconto em tokens em cache. Coloque sempre a parte estática primeiro.
- Context compression: resumir o histórico longo em vez de enviá-lo completo. Economia de 40-60% em conversas com muitos turnos.
- Batch API: 50% de desconto para tarefas não urgentes (avaliações, geração offline).
- O 80/20 do custo: 80% do gasto vem dos 20% de requisições mais longas. Otimize a cauda, não a média.
class ModelRouter:
def select(self, query: str, context: dict) -> ModelTier:
# Regla 1: casos críticos siempre al modelo estándar
if context.get("ticket_priority") == "critical":
return ModelTier.STANDARD
# Regla 2: clasificar complejidad con el modelo más barato posible
complexity_prompt = f"""Clasifica esta consulta: '{query}'
Responde SOLO con: SIMPLE o COMPLEX
SIMPLE: saludos, estado de pedido, preguntas de FAQ
COMPLEX: problemas técnicos, disputas, múltiples pasos"""
response = self.llm.call(
[{"role": "user", "content": complexity_prompt}],
model=ModelTier.FAST, # Haiku para clasificar
max_tokens=5
)
if "SIMPLE" in response.text:
return ModelTier.FAST # Haiku: 10x más barato
return ModelTier.STANDARD # Sonnet: balance ideal
class ContextCompressor:
max_history_tokens = 3000
def compress(self, history: list[dict]) -> list[dict]:
if self._count_tokens(history) <= self.max_history_tokens:
return history # No necesita compresión
# Mantener los últimos 3 turnos intactos (más relevantes)
recent = history[-3:]
older = history[:-3]
# Resumir los turnos más antiguos
summary_prompt = f"Resume en 2 oraciones los puntos clave de esta conversación: {older}"
summary = self.llm.call([{"role": "user", "content": summary_prompt}],
model=ModelTier.FAST)
return [{"role": "system",
"content": f"Contexto previo (resumido): {summary.text}"}] + recentRecursos
litellm, time.perf_counter, Anthropic prompt caching
Benchmark de model routing
Medir empiricamente quanto o model routing economiza sem sacrificar qualidade.
- Pegue 50 consultas reais de suporte (ou simuladas)
- Execute todas com Sonnet. Registre o custo total e a taxa de resolução correta
- Implemente o ModelRouter e execute as mesmas 50 consultas
- Compare: quanto você economizou? A taxa de resolução caiu?
- Ajuste o threshold do classifier até chegar ao melhor equilíbrio custo/qualidade
Entregável do módulo
Vai para o projeto final: ModelRouter + ContextCompressor + ADR. Módulo de otimização funcionando, mais um documento ADR com os trade-offs medidos do sistema. O ModelRouter substitui o modelo fixo do LLMClient do M1. Agora o sistema seleciona o modelo dinamicamente. O ContextCompressor é acionado automaticamente no MemoryManager do M5.
Módulo 9 · Fase 4 · Trade-offs & Debugging
Debugging Probabilístico
Framework de análise, loop detection e reprodutibilidade de falhas
Por que o debugging em sistemas probabilísticos é diferente
Em sistemas determinísticos, o mesmo input produz o mesmo output — sempre. Em sistemas com LLMs, o mesmo input pode produzir outputs ligeiramente diferentes a cada chamada. Isso muda completamente a estratégia de debugging.
Os 3 tipos de falha mais frequentes
- Alucinações: o modelo gera informação incorreta com alta confiança. Causa: contexto insuficiente ou constraints fracas no prompt. Mitigação: RAG + constraints explícitas + grounding checks.
- Loops: o agente repete a mesma ação indefinidamente. Causa: a ferramenta falha, mas o modelo não reconhece isso como erro. Mitigação: MAX_ITERATIONS + loop detector + circuit breaker.
- Degradação silenciosa: a qualidade cai gradualmente sem alerta visível. Causa: model drift do provedor ou prompt drift por edições acumuladas. Mitigação: avaliação contínua + alertas no dashboard.
Uma falha isolada é ruído. Um padrão de falhas é um sinal. Antes de mudar o código, quantifique: quantas vezes a mesma falha ocorre em 100 chamadas? Se for menos de 1%, documente e monitore. Se passar de 5%, aja.
Framework de debugging em 5 passos
O processo correto
- 1. Reproduzir: guardar o input completo (prompt, histórico, tool results, modelo, versão). Sem reprodutibilidade, o debugging é impossível.
- 2. Isolar: falha no planning, na execution ou na evaluation? Teste cada componente separadamente com inputs sintéticos.
- 3. Rastrear: revisar o
<thinking>do agente. O raciocínio estava correto? Os dados estavam certos? - 4. Quantificar: é um caso isolado ou sistêmico? Execute 20+ vezes antes de concluir.
- 5. Iterar: mudar UMA variável por vez. Sem teste A/B não há conclusões válidas.
import hashlib, json
from datetime import datetime
class LoopDetector:
def __init__(self, window: int = 3):
self.window = window # comparar los últimos N estados
def check(self, history: list) -> bool:
if len(history) < self.window:
return False
# Si los últimos N thoughts son iguales → loop detectado
last_n = history[-self.window:]
hashes = [hashlib.md5(json.dumps(h["thought"].tool_name).encode()).hexdigest()
for h in last_n]
return len(set(hashes)) == 1 # todos iguales = loop
class FailureStore:
def capture(self, context: dict, error: Exception, agent_history: list) -> str:
failure_id = f"fail-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
record = {
"id": failure_id,
"timestamp": datetime.utcnow().isoformat(),
"error_type": type(error).__name__,
"error_message": str(error),
"prompt_version": context.get("prompt_version"),
"model": context.get("model"),
"full_context": context, # TODO: redactar PII antes de guardar
"agent_history": agent_history,
}
self.db.save(failure_id, json.dumps(record))
return failure_id
def replay(self, failure_id: str) -> dict:
"""Recupera el contexto completo para reproducir el fallo exactamente."""
return json.loads(self.db.get(failure_id))Recursos
sqlite3, pytest fixtures, hashlib
Analise 2 falhas reais do sistema
A melhor forma de aprender debugging é analisar falhas reais, não simuladas.
- Execute o SupportAgent com 20 consultas variadas. O FailureStore captura tudo o que falhar
- Escolha as 2 falhas mais interessantes do store
- Para cada uma: use o ReplayRunner para reproduzir a falha exatamente
- Inspecione o
<thinking>do agente: onde o raciocínio errou? - Escreva a análise em
docs/failure-analysis-report.md: causa raiz, fix proposto, teste de regressão
Entregável do módulo
Vai para o projeto final: Debug Toolkit + Failure Analysis Report. FailureStore, LoopDetector, ReplayRunner e um relatório de análise de pelo menos 2 falhas reais com root cause e fix proposto. O FailureStore se conecta ao Orchestrator. O LoopDetector envolve o ReAct loop do SupportAgent. Qualquer exceção não tratada fica capturada automaticamente com contexto completo.
Módulo 10 · Fase 5 · Observabilidade & Avaliação
LLMOps: Observabilidade & Avaliação Contínua
Tracing, métricas de negócio, testes A/B de prompts e guardrails
Você não consegue melhorar o que não mede
O módulo de LLMOps é o que fecha o ciclo. Sem observabilidade, o sistema é uma caixa-preta que funciona (ou não) sem que ninguém saiba por quê. Com observabilidade, cada decisão de melhoria é respaldada por dados.
As métricas que importam — e em que ordem
- Task completion rate: a métrica mais importante. Que % das conversas terminou com o problema do usuário resolvido?
- Escalation rate: % escalado para humano. Se sobe → o agente está piorando. Se cai demais → pode estar deixando passar casos que deveria escalar.
- Cost per successful interaction: (tokens × preço) / interações bem-sucedidas. A métrica de eficiência do sistema.
- Latência p95: o percentil 95 de latência — o que 95% dos usuários experimentam. A média mente.
- Tool error rate: % de chamadas a ferramentas que falham. Sinaliza problemas em APIs externas.
O dashboard de métricas deve ser visível para todo o time — não só para a área técnica. Um dashboard com business metrics + technical metrics em uma única tela elimina 80% das discussões de priorização.
LLM-as-judge — avaliação automática escalável
Avaliar manualmente a qualidade de 1000 respostas por semana é inviável. O padrão LLM-as-judge usa um segundo LLM para avaliar o output do primeiro. O avaliador recebe: a consulta original, a resposta gerada e os critérios de avaliação.
O LLM-as-judge tem vieses: favorece respostas mais longas, mais formais ou que soam "mais seguras". Sempre valide o seu judge contra avaliações humanas em uma amostra antes de usá-lo como única fonte de verdade.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
tracer = trace.get_tracer("nexus-support-agent")
class AgentTracer:
def trace_request(self, trace_id: str, user_id: str):
return tracer.start_as_current_span("agent_request",
attributes={"trace_id": trace_id, "user_id": user_id})
class MetricsCollector:
def record_interaction(self, result: AgentResult, context: dict):
TASK_COMPLETION.inc(1 if result.success else 0)
ESCALATION_RATE.inc(1 if result.escalated else 0)
COST_COUNTER.inc(result.cost_usd)
LATENCY_HISTOGRAM.observe(result.latency_ms)
TOKEN_COUNTER.inc(result.total_tokens)
class LLMJudge:
judge_prompt = """Evalúa esta respuesta de soporte.
Query: {query}
Respuesta: {response}
Puntúa 1-5 en cada criterio y responde SOLO JSON:
{{"relevance": 1-5, "accuracy": 1-5, "tone": 1-5, "completeness": 1-5,
"overall": 1-5, "reasoning": "explicación breve"}}"""
def evaluate(self, query: str, response: str) -> dict:
result = self.llm.call(
[{"role": "user", "content": self.judge_prompt.format(
query=query, response=response)}],
model=ModelTier.STANDARD # el judge necesita buen criterio
)
return json.loads(result.text)
class EvalPipeline:
def run(self, prompt_version: str) -> EvalReport:
results = []
for case in self.load_test_set():
output = self.agent.run(case["input"], case["context"])
score = self.judge.evaluate(case["input"], output.answer)
results.append({"case_id": case["id"], "score": score, "passed": score["overall"] >= 3})
pass_rate = sum(1 for r in results if r["passed"]) / len(results)
return EvalReport(results=results, pass_rate=pass_rate,
version=prompt_version, baseline=self.get_baseline())Recursos
opentelemetry, langsmith, prometheus, grafana, presidio (PII)
Implemente o dashboard de métricas completo
O dashboard é a primeira coisa que você olha quando algo falha em produção.
- Instrumente o Orchestrator para que cada request gere as 5 métricas definidas
- Suba Prometheus + Grafana localmente com Docker Compose
- Crie um dashboard com: task_completion_rate, escalation_rate, cost_per_interaction, p95_latency e tool_error_rate
- Execute 50 consultas simuladas e verifique se as métricas são atualizadas corretamente
- Configure um alerta: se a escalation_rate subir mais de 20% em 1 hora, alertar o canal do Slack
Pipeline de avaliação em CI
O teste que impede que uma mudança de prompt quebre o sistema em produção.
- Crie uma GitHub Action que execute o EvalPipeline em cada PR que modificar um arquivo em prompts/
- A Action reprova o PR se o pass_rate cair mais de 5% em relação ao baseline
- Faça uma mudança de prompt intencionalmente ruim e verifique se o CI detecta
- Faça uma mudança boa e verifique se o CI aprova
- Documente o processo no README do repositório
Entregável do módulo
Vai para o projeto final: Observability Stack + Eval Pipeline em CI. Instrumentação completa do sistema: tracing, 5 métricas de negócio/técnicas, LLM-as-judge, pipeline de avaliação automática e guardrails de segurança. Este módulo instrumenta todos os componentes anteriores. O eval pipeline se conecta ao PromptLoader do M2, ao CriticAgent do M3 e ao FailureStore do M9, formando o ciclo completo de melhoria contínua.
Projeto final
Projeto final: Nexus Support Agent, sistema multi-agente end-to-end
Todos os entregáveis dos 10 módulos integrados em um sistema de suporte ao cliente observável, otimizado e pronto para produção.
Core
- Classificação automática com modelo leve
- RAG sobre knowledge base com reranking
- Memória episódica por usuário
- 4 ferramentas externas tipadas
- Critic loop antes da resposta
Segurança
- Human-in-the-loop com 5 critérios
- Fallback em cascata de 3 níveis
- Circuit breaker por ferramenta
- PII detection em inputs/outputs
- Timeout global por workflow
LLMOps
- Tracing completo com trace_id
- Dashboard com 5 KPIs em tempo real
- Eval pipeline em CI automático
- Model routing dinâmico
- ADR documentado com dados
Estrutura do repositório
src/agents/ llm/ memory/ tools/ safety/ observability/ optimization/ debug/prompts/support_agent/ classifier/ critic/ com versionamentoevals/test_set.jsonl · eval_pipeline.py · llm_judge.pydocs/ADR-001.md · ADR-002.md · failure-analysis.mdtests/unit/ integration/ coverage >70%.github/workflows/eval_on_pr.yml · ci.yml
Critérios de aprovação
| Critério |
|---|
Seu progresso fica salvo neste navegador.