Observabilidade de LLMs a Fundo: Como Monitorar, Rastrear e Debugar Agentes de IA em Produção
Seu agente de IA acabou de custar $2.400 pra um cliente. Entrou num loop infinito de tool calls às 3 da manhã, queimando tokens enquanto gerava respostas sem sentido. Seu dashboard de APM tradicional mostra tudo verdinho: latência normal, sem erros, sem crashes. Mas o agente tava respondendo errado com total confiança por seis horas seguidas, e você não tinha visibilidade nenhuma.
Esse é o gap de observabilidade que mata produtos de IA. Ferramentas de monitoramento tradicionais foram feitas pra software determinístico: request entra, response sai, mede o tempo. Agentes de IA são fundamentalmente diferentes. Eles raciocinam, ramificam, chamam ferramentas, buscam documentos e tomam decisões que variam com inputs idênticos. Quando algo dá errado, não dá pra só olhar o status code HTTP. Você precisa rastrear a cadeia de raciocínio: cada ponto de decisão, cada invocação de ferramenta, cada token consumido.
Esse guia cobre tudo que você precisa pra construir observabilidade de nível produção pra sistemas baseados em LLMs: de tracing distribuído e avaliações automatizadas até tracking de custos e o ecossistema de ferramentas. Não é teoria. São padrões testados em batalha por times que rodam agentes processando milhões de requests por dia.
Por que monitoramento tradicional falha em aplicações LLM
Se você tá rodando agentes de IA só com Datadog, Grafana ou New Relic, tá voando no escuro. O motivo:
O problema do determinismo
Software tradicional é determinístico. Dado o mesmo input, você recebe o mesmo output. Monitoramento é direto: rastreia latência, taxa de erros e throughput. Se a latência P99 dispara, investiga.
LLMs são não-determinísticos. O mesmo prompt pode produzir outputs diferentes toda vez. Uma resposta HTTP 200 "bem-sucedida" pode conter uma resposta completamente alucinada. Sua taxa de erros é 0%, mas sua acurácia é 40%. Ferramentas de APM tradicionais literalmente não conseguem detectar esse modo de falha.
O problema multi-step
Uma chamada de API simples é um span só: request → response. Um agente de IA é um grafo de execução complexo:
Query do usuário: "Encontra o voo mais barato de NYC pra Tóquio mês que vem"
│
├─ Step 1: Classificação de intenção (LLM call, 200ms, 150 tokens)
├─ Step 2: Extração de parâmetros (LLM call, 180ms, 120 tokens)
├─ Step 3: Tool call - API de voos (API externa, 2.1s)
├─ Step 4: Parsing de resultados (LLM call, 250ms, 800 tokens)
├─ Step 5: Comparação de preços (LLM call, 300ms, 1200 tokens)
├─ Step 6: Geração de resposta (LLM call, 400ms, 500 tokens)
│
Total: 5 LLM calls, 3.4s, 2770 tokens, $0.008
Quando esse agente retorna resultados errados, qual step falhou? Foi a classificação de intenção? A ferramenta retornou dados errados? O LLM alucinou durante a comparação de preços? Sem tracing por step, debugar é impossível.
O problema de custos
Chamadas de LLM são caras. Diferente de compute tradicional onde ciclos de CPU são praticamente de graça, cada token tem um custo direto em dólar. Um loop de agente descontrolado pode queimar centenas de dólares em minutos. Você precisa de tracking de custos em tempo real por agente, usuário e organização, e nenhuma ferramenta APM tradicional te dá isso.
O stack de observabilidade pra LLMs
Observabilidade de nível produção pra LLMs precisa de quatro camadas:
┌─────────────────────────────────────────────────────┐
│ Camada 4: DASHBOARDS │
│ Analytics de custo, tendências, SLA tracking │
├─────────────────────────────────────────────────────┤
│ Camada 3: AVALIAÇÃO │
│ Evals automatizados, detecção de regressão, A/B │
├─────────────────────────────────────────────────────┤
│ Camada 2: TRACING │
│ Traces distribuídos, hierarquia de spans, tokens │
├─────────────────────────────────────────────────────┤
│ Camada 1: INSTRUMENTAÇÃO │
│ Integração SDK, captura automática, anotações │
└─────────────────────────────────────────────────────┘
Vamos construir cada camada do zero.
Camada 1: Instrumentação
Instrumentação é a base. Você precisa capturar dados em cada ponto de decisão sem destruir a performance da sua aplicação.
OpenTelemetry pra LLMs
A indústria tá convergindo no OpenTelemetry (OTel) como camada de instrumentação padrão. O projeto OpenLLMetry estende o OTel com convenções semânticas específicas pra LLMs:
import * as traceloop from '@traceloop/node-server-sdk'; // Inicializar antes de importar módulos LLM traceloop.initialize({ baseUrl: 'https://your-collector.example.com', appName: 'my-ai-agent', }); // Ou use a abordagem modular com OpenTelemetry diretamente: import { NodeSDK } from '@opentelemetry/sdk-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { OpenAIInstrumentation } from '@traceloop/instrumentation-openai'; import { AnthropicInstrumentation } from '@traceloop/instrumentation-anthropic'; const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ url: 'https://your-collector.example.com/v1/traces', }), instrumentations: [ new OpenAIInstrumentation({ captureInputs: true, // Logar prompts (cuidado em prod!) captureOutputs: true, // Logar completions }), new AnthropicInstrumentation(), ], }); sdk.start();
Isso auto-instrumenta cada chamada às APIs da OpenAI e Anthropic, capturando:
- Nome do modelo e parâmetros (temperature, max_tokens)
- Prompts de entrada e completions de saída
- Consumo de tokens (prompt tokens, completion tokens)
- Latência por chamada
- Detalhes de tool/function calls
Anotações manuais de spans
Auto-instrumentação captura as chamadas ao LLM, mas você precisa de spans manuais pra lógica de negócio:
import { trace, SpanStatusCode } from '@opentelemetry/api'; const tracer = trace.getTracer('ai-agent'); async function processUserQuery(query: string, userId: string) { return tracer.startActiveSpan('agent.process_query', async (span) => { span.setAttributes({ 'user.id': userId, 'agent.query': query, 'agent.type': 'flight-search', }); try { // Step 1: Classificar intenção const intent = await tracer.startActiveSpan( 'agent.classify_intent', async (intentSpan) => { const result = await classifyIntent(query); intentSpan.setAttributes({ 'agent.intent': result.intent, 'agent.confidence': result.confidence, }); return result; } ); // Step 2: Executar tool calls const toolResults = await tracer.startActiveSpan( 'agent.execute_tools', async (toolSpan) => { toolSpan.setAttribute('agent.tools_count', intent.tools.length); return Promise.all( intent.tools.map((tool) => executeTool(tool)) ); } ); // Step 3: Gerar resposta const response = await tracer.startActiveSpan( 'agent.generate_response', async (respSpan) => { const result = await generateResponse(toolResults); respSpan.setAttributes({ 'agent.response_length': result.length, 'agent.tokens_total': result.tokenUsage.total, }); return result; } ); span.setStatus({ code: SpanStatusCode.OK }); return response; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.recordException(error); throw error; } }); }
O que capturar (e o que não)
Uma decisão crítica: quais dados você loga?
| Dado | Capturar em Dev | Capturar em Prod | Por quê |
|---|---|---|---|
| Prompts completos | ✅ Sim | ⚠️ Amostrado | Risco de PII, custo de armazenamento |
| Completions completas | ✅ Sim | ⚠️ Amostrado | Igual acima |
| Contagem de tokens | ✅ Sim | ✅ Sim | Tracking de custos é crítico |
| Parâmetros do modelo | ✅ Sim | ✅ Sim | Debugar regressões |
| Inputs/outputs de tools | ✅ Sim | ✅ Sim | Essencial pra debugging |
| IDs de usuário | ✅ Sim | ✅ Sim | Tracking de custo por usuário |
| Latência por step | ✅ Sim | ✅ Sim | Monitoramento de performance |
| Vetores de embedding | ❌ Não | ❌ Não | Muito grandes, raramente úteis |
| Respostas raw de API | ✅ Sim | ❌ Não | Explosão de armazenamento |
Em produção, use sampling pro log de prompts/completions completas. Capture 100% de metadata (tokens, latência, modelo) mas só 10-20% do conteúdo de texto completo. Quando tiver debugando um issue específico, aumente temporariamente o sampling pra usuários ou queries específicas.
Camada 2: Tracing distribuído
Com a instrumentação no lugar, você precisa de um backend de tracing que entenda dados específicos de LLMs. É aqui que as ferramentas especializadas brilham.
Estrutura de traces pra agentes de IA
Um trace bem estruturado pra um agente de IA se parece com isso:
Trace: agent_run_abc123
│
├─ Span: agent.process_query (raiz)
│ ├─ Attributes: user_id, query, session_id
│ │
│ ├─ Span: agent.classify_intent
│ │ ├─ Span: llm.openai.chat (model: gpt-4.1-mini)
│ │ │ └─ Attributes: tokens_in=150, tokens_out=30, cost=$0.0001
│ │ └─ Result: intent=flight_search, confidence=0.95
│ │
│ ├─ Span: agent.retrieve_context (RAG)
│ │ ├─ Span: vectordb.query (provider: pinecone)
│ │ │ └─ Attributes: top_k=5, similarity_threshold=0.8
│ │ └─ Span: agent.rerank
│ │ └─ Span: llm.anthropic.chat (model: claude-haiku-4.5)
│ │ └─ Attributes: tokens_in=2000, tokens_out=500
│ │
│ ├─ Span: agent.execute_tool
│ │ ├─ Span: tool.flight_api.search
│ │ │ └─ Attributes: duration=2100ms, results_count=15
│ │ └─ Span: tool.flight_api.get_prices
│ │ └─ Attributes: duration=800ms, results_count=15
│ │
│ └─ Span: agent.generate_response
│ └─ Span: llm.openai.chat (model: gpt-4.1)
│ └─ Attributes: tokens_in=3000, tokens_out=800, cost=$0.02
│
└─ Total: 4 LLM calls, 6800 tokens, $0.021, 4.2s
Essa estrutura te permite responder perguntas como:
- "Por que esse agente demorou 10 segundos?" → A API de voos demorou 8 segundos.
- "Por que custou 0.02?" → O agente fez 100 loops de tool calls.
- "Por que alucinou?" → A busca RAG retornou documentos irrelevantes com scores de similaridade baixos.
Implementando propagação de traces
Pra arquiteturas multi-serviço, o contexto de trace precisa se propagar entre serviços:
// Serviço A: Orquestrador do agente import { context, propagation } from '@opentelemetry/api'; async function callToolService(toolName: string, params: any) { const headers: Record<string, string> = {}; propagation.inject(context.active(), headers); const response = await fetch(`https://tools.internal/${toolName}`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); return response.json(); } // Serviço B: Serviço de execução de ferramentas import { context, propagation } from '@opentelemetry/api'; app.post('/flight-search', (req, res) => { const ctx = propagation.extract(context.active(), req.headers); context.with(ctx, async () => { const span = tracer.startSpan('tool.flight_search'); // ... execução da tool com linhagem de trace completa span.end(); }); });
Camada 3: Avaliação automatizada
Tracing te diz o que aconteceu. Avaliação te diz quão bem aconteceu. Essa é a camada que a maioria dos times pula, e a camada que define o sucesso ou fracasso da IA em produção.
O pipeline de avaliação
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Store │ → │ Sample │ → │ Score │ → │ Alert │
│ Traces │ │ Select │ │ Eval │ │ Report │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
10-20% dos LLM-as-Judge Slack/PD
traces + Regras se qualidade
determinísticas cair
Avaliação LLM-as-Judge
O padrão de eval mais poderoso é usar um LLM separado pra julgar os outputs do seu agente:
interface EvalResult { score: number; // 0-1 reasoning: string; // Por que essa nota dimension: string; // O que foi avaliado } async function evaluateResponse( query: string, response: string, groundTruth?: string ): Promise<EvalResult[]> { const evaluations: EvalResult[] = []; // Eval 1: Precisão factual const accuracyEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `Você é um avaliador especialista. Dê uma nota pra precisão factual da resposta da IA numa escala de 0 a 1. Critérios: - 1.0: Todos os fatos são corretos e verificáveis - 0.7: Majoritariamente correto com imprecisões menores - 0.4: Contém erros factuais significativos - 0.0: Completamente fabricado ou errado Responda em JSON: { "score": number, "reasoning": string }`, }, { role: 'user', content: `Pergunta: ${query} Resposta da IA: ${response} ${groundTruth ? `Resposta correta: ${groundTruth}` : ''}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(accuracyEval.choices[0].message.content), dimension: 'factual_accuracy', }); // Eval 2: Relevância const relevanceEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `Dê uma nota de quão relevante a resposta é pra pergunta. 1.0 = Responde diretamente a pergunta 0.5 = Parcialmente relevante 0.0 = Completamente fora do assunto Responda em JSON: { "score": number, "reasoning": string }`, }, { role: 'user', content: `Pergunta: ${query}\nResposta: ${response}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(relevanceEval.choices[0].message.content), dimension: 'relevance', }); return evaluations; }
Guards determinísticos
Nem tudo precisa de um juiz LLM. Use checks determinísticos pra padrões de falha conhecidos:
interface GuardResult { passed: boolean; violation?: string; } function runDeterministicGuards( trace: AgentTrace ): GuardResult[] { const results: GuardResult[] = []; // Guard 1: Orçamento de tokens estourado const totalTokens = trace.spans .filter((s) => s.name.startsWith('llm.')) .reduce((sum, s) => sum + (s.attributes.tokens_total || 0), 0); results.push({ passed: totalTokens < 50000, violation: totalTokens >= 50000 ? `Orçamento de tokens estourado: ${totalTokens} tokens` : undefined, }); // Guard 2: Detecção de loop em tool calls const toolCalls = trace.spans .filter((s) => s.name.startsWith('tool.')); const uniqueTools = new Set(toolCalls.map((s) => s.name)); for (const tool of uniqueTools) { const count = toolCalls .filter((s) => s.name === tool).length; results.push({ passed: count <= 10, violation: count > 10 ? `Loop potencial: ${tool} chamado ${count} vezes` : undefined, }); } // Guard 3: Orçamento de latência const totalLatency = trace.duration; results.push({ passed: totalLatency < 30000, violation: totalLatency >= 30000 ? `Orçamento de latência estourado: ${totalLatency}ms` : undefined, }); // Guard 4: Resposta vazia ou suspeitamente curta const finalResponse = trace.output; results.push({ passed: finalResponse && finalResponse.length > 20, violation: !finalResponse || finalResponse.length <= 20 ? 'Resposta tá vazia ou suspeitamente curta' : undefined, }); return results; }
Alertas automatizados
Conecte as avaliações ao seu sistema de alertas:
async function runEvalPipeline(trace: AgentTrace) { // Guards determinísticos (rápidos, rodam em todos os traces) const guardResults = runDeterministicGuards(trace); const guardViolations = guardResults .filter((r) => !r.passed); if (guardViolations.length > 0) { await sendAlert({ severity: 'high', title: 'Violação de guard do agente', details: guardViolations .map((v) => v.violation) .join('\n'), traceId: trace.traceId, }); } // LLM-as-Judge (caro, roda em traces amostrados) if (shouldSample(trace, 0.1)) { const evalResults = await evaluateResponse( trace.input, trace.output ); const lowScores = evalResults .filter((e) => e.score < 0.5); if (lowScores.length > 0) { await sendAlert({ severity: 'medium', title: 'Degradação de qualidade do agente', details: lowScores .map((e) => `${e.dimension}: ${e.score} - ${e.reasoning}` ) .join('\n'), traceId: trace.traceId, }); } await storeEvalResults(trace.traceId, evalResults); } }
Camada 4: Tracking de custos e analytics
Custos de tokens são a conta de cloud de aplicações de IA. Sem tracking granular de custos, você tá chutando.
Cálculo de custos em tempo real
const MODEL_PRICING: Record<string, { input: number; // por 1M tokens output: number; // por 1M tokens }> = { 'gpt-4.1': { input: 2.00, output: 8.00 }, 'gpt-4.1-mini': { input: 0.40, output: 1.60 }, 'gpt-4.1-nano': { input: 0.10, output: 0.40 }, 'claude-sonnet-4.6': { input: 3.00, output: 15.00 }, 'claude-haiku-4.5': { input: 1.00, output: 5.00 }, 'gemini-2.5-pro': { input: 1.25, output: 10.00 }, 'gemini-2.5-flash': { input: 0.30, output: 2.50 }, }; function calculateCost( model: string, inputTokens: number, outputTokens: number ): number { const pricing = MODEL_PRICING[model]; if (!pricing) return 0; return ( (inputTokens / 1_000_000) * pricing.input + (outputTokens / 1_000_000) * pricing.output ); } // Trackear custo por trace function aggregateTraceCost(trace: AgentTrace): CostBreakdown { const llmSpans = trace.spans .filter((s) => s.name.startsWith('llm.')); let totalCost = 0; const breakdown: Record<string, number> = {}; for (const span of llmSpans) { const model = span.attributes.model; const cost = calculateCost( model, span.attributes.tokens_in, span.attributes.tokens_out ); totalCost += cost; breakdown[model] = (breakdown[model] || 0) + cost; } return { totalCost, breakdown, tokenCount: llmSpans.reduce( (sum, s) => sum + s.attributes.tokens_in + s.attributes.tokens_out, 0 ), }; }
Limiares de alerta de custos
// Guard de custo por request const MAX_COST_PER_REQUEST = 0.50; // $0.50 // Orçamento por usuário por hora const MAX_COST_PER_USER_HOUR = 5.00; // $5.00 // Orçamento por organização por dia const MAX_COST_PER_ORG_DAY = 500.00; // $500.00 async function checkCostBudgets( cost: number, userId: string, orgId: string ) { if (cost > MAX_COST_PER_REQUEST) { await sendAlert({ severity: 'high', title: `Custo de request estourado: $${cost.toFixed(4)}`, }); } const userHourlyCost = await redis.incrbyfloat( `cost:user:${userId}:${getCurrentHour()}`, cost ); await redis.expire( `cost:user:${userId}:${getCurrentHour()}`, 7200 ); if (userHourlyCost > MAX_COST_PER_USER_HOUR) { await sendAlert({ severity: 'critical', title: `Orçamento por hora do usuário ${userId} estourado`, }); } }
O panorama de ferramentas: LangSmith vs Langfuse vs Arize
Escolher a plataforma de observabilidade certa é uma decisão crítica. Aqui vai a comparação honesta:
LangSmith
Ideal pra: Times que já usam LangChain/LangGraph
import { Client } from 'langsmith'; import { traceable } from 'langsmith/traceable'; const client = new Client({ apiKey: process.env.LANGSMITH_API_KEY, }); const processQuery = traceable( async (query: string) => { const intent = await classifyIntent(query); const results = await searchFlights(intent); return generateResponse(results); }, { name: 'process_query', tags: ['production'] } );
Pontos fortes:
- Integração profunda com LangChain/LangGraph (first-party)
- Playground de prompts e versionamento integrado
- Hub pra compartilhar e descobrir prompts
- Framework de eval sólido com human-in-the-loop
- Excelente visualização de grafos de execução de agentes
Pontos fracos:
- Vendor lock-in pro ecossistema LangChain
- Closed-source, só hosted (sem self-hosting)
- Pricing escala com volume de traces (pode ficar caro)
- Suporte limitado pra frameworks fora do LangChain
Langfuse
Ideal pra: Times que querem open-source, agnóstico a framework
// Langfuse v5+ (recomendado: @langfuse/tracing) import { observe } from '@langfuse/tracing'; // Tracing baseado em decoradores (mais simples) const processQuery = observe( { name: 'flight-search-agent' }, async (query: string) => { const intent = await classifyIntent(query); const results = await searchFlights(intent); return generateResponse(results); } ); // Ou use o cliente clássico do Langfuse pra controle granular: import Langfuse from 'langfuse'; const langfuse = new Langfuse({ publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY, }); const trace = langfuse.trace({ name: 'flight-search-agent', userId: 'user-123', metadata: { environment: 'production' }, }); const generation = trace.generation({ name: 'classify-intent', model: 'gpt-4.1-mini', input: [{ role: 'user', content: query }], output: response, usage: { promptTokens: 150, completionTokens: 30, }, });
Pontos fortes:
- Open-source (licença MIT), self-hostável
- Agnóstico a framework (funciona com qualquer provedor de LLM)
- Tracking de custos e analytics de tokens integrado
- Gerenciamento e versionamento de prompts
- Free tier generoso
Pontos fracos:
- Comunidade menor que LangSmith
- Self-hosting requer gerenciamento de infra
- Features de avaliação menos maduros que LangSmith
- UI menos polida (melhorando rápido)
Arize Phoenix
Ideal pra: Times com background em ML/data science
import { trace as otelTrace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { OpenAIInstrumentation } from '@arizeai/openinference-instrumentation-openai'; registerInstrumentations({ instrumentations: [new OpenAIInstrumentation()], });
Pontos fortes:
- Construído sobre OpenTelemetry (sem lock-in proprietário)
- Excelente visualização de embeddings e detecção de drift
- Ferramentas de análise de RAG muito boas
- Experiência de dev local-first (Phoenix roda local)
- Melhor da categoria pra debugar qualidade de retrieval
Pontos fracos:
- Curva de aprendizado mais íngreme
- Menos foco em tracing de orquestração de agentes
- Ecossistema de integrações diretas menor
- Features enterprise precisam do Arize cloud
Matriz de comparação
| Feature | LangSmith | Langfuse | Arize Phoenix |
|---|---|---|---|
| Open Source | ❌ | ✅ MIT | ✅ (Phoenix) |
| Self-Hosting | ❌ | ✅ | ✅ (Phoenix) |
| Integração LangChain | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| Agnóstico a framework | ⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Tracking de custos | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Framework de eval | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| Análise de RAG | ⭐⭐ | ⭐ | ⭐⭐⭐ |
| Gestão de prompts | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| Análise de embeddings | ⭐ | ⭐ | ⭐⭐⭐ |
| Preço (startup) | $$$ | Grátis/$ | Grátis/$ |
Padrões e anti-padrões de produção
Padrão 1: O circuit breaker
Impeça agentes descontrolados de queimar seu orçamento:
class AgentCircuitBreaker { private tokenCount = 0; private llmCalls = 0; private toolCalls = 0; private startTime: number; constructor( private limits: { maxTokens: number; maxLLMCalls: number; maxToolCalls: number; maxDurationMs: number; } ) { this.startTime = Date.now(); } check(event: { type: 'llm' | 'tool'; tokens?: number }) { if (event.type === 'llm') { this.llmCalls++; this.tokenCount += event.tokens || 0; } else { this.toolCalls++; } const elapsed = Date.now() - this.startTime; if (this.tokenCount > this.limits.maxTokens) { throw new CircuitBreakerError( `Limite de tokens estourado: ${this.tokenCount}` ); } if (this.llmCalls > this.limits.maxLLMCalls) { throw new CircuitBreakerError( `Limite de chamadas LLM estourado: ${this.llmCalls}` ); } if (this.toolCalls > this.limits.maxToolCalls) { throw new CircuitBreakerError( `Limite de tool calls estourado: ${this.toolCalls}` ); } if (elapsed > this.limits.maxDurationMs) { throw new CircuitBreakerError( `Limite de duração estourado: ${elapsed}ms` ); } } } // Uso const breaker = new AgentCircuitBreaker({ maxTokens: 50000, maxLLMCalls: 20, maxToolCalls: 30, maxDurationMs: 60000, // 1 minuto }); for (const step of agentSteps) { breaker.check({ type: step.type, tokens: step.tokenUsage, }); await executeStep(step); }
Padrão 2: Workflow de debugging baseado em traces
Quando algo quebra, siga essa abordagem sistemática:
1. DETECTAR: Eval automatizada flagra queda de qualidade
↓
2. IDENTIFICAR: Filtrar traces por eval scores baixos
↓
3. COMPARAR: Side-by-side com traces bem-sucedidos
↓
4. ISOLAR: Encontrar o ponto de divergência
↓
5. CAUSA RAIZ: Examinar inputs/outputs naquele span
↓
6. CORRIGIR: Atualizar prompt, contexto ou config da tool
↓
7. VALIDAR: Rodar evals da correção contra dataset de teste
Anti-padrão 1: Logar tudo
Não logue cada token de cada request.
// ❌ Não faça isso logger.info('LLM Response', { fullPrompt: systemPrompt + userMessage + context, // 50KB fullResponse: completion, // 10KB metadata: entireTraceObject, // 5KB }); // Resultado: 65KB por request × 1M requests/dia = 65GB/dia // ✅ Faça isso logger.info('LLM Response', { traceId: trace.id, model: 'gpt-4.1-mini', tokensIn: 150, tokensOut: 30, cost: 0.0001, latencyMs: 200, evalScore: 0.95, }); // Resultado: 200 bytes por request × 1M requests/dia = 200MB/dia
Anti-padrão 2: Tratar erros de LLM como erros HTTP
// ❌ Enganoso: HTTP 200 mas a resposta do agente tá horrível if (response.status === 200) { metrics.increment('agent.success'); } // ✅ Correto: Meça a qualidade real const evalScore = await quickEval(response.body); if (evalScore > 0.7) { metrics.increment('agent.quality.good'); } else { metrics.increment('agent.quality.poor'); // Esse é o verdadeiro "erro" — dispara investigação }
Anti-padrão 3: Sem baseline
// ❌ Alerta: "Eval score é 0.72" — Isso é bom? Ruim? // ✅ Primeiro estabeleça uma baseline // Semana 1-2: Colete eval scores sem alertar // Semana 3: Calcule baselines P50, P90, P99 // Semana 4+: Alerte em desvios da baseline const baseline = { accuracy: { p50: 0.85, p90: 0.92, p99: 0.97 }, relevance: { p50: 0.90, p90: 0.95, p99: 0.99 }, latency: { p50: 2000, p90: 5000, p99: 10000 }, }; function shouldAlert( dimension: string, value: number ): boolean { const b = baseline[dimension]; return value < b.p50 * 0.8; // Alertar se 20% abaixo da mediana }
O stack de observabilidade mínimo viável
Se você tá começando do zero, aqui vai o caminho mais rápido pra observabilidade de nível produção:
Dia 1: Instrumentação básica
// 1. Instale Langfuse (mais rápido pra começar) // npm install langfuse import Langfuse from 'langfuse'; const langfuse = new Langfuse(); // 2. Wrapie a função principal do seu agente async function runAgent(query: string, userId: string) { const trace = langfuse.trace({ name: 'agent-run', userId, input: query, }); // Seu código de agente existente... trace.update({ output: response }); await langfuse.flushAsync(); }
Semana 1: Adicione tracking de custos
const generation = trace.generation({ name: 'main-llm-call', model: 'gpt-4.1-mini', input: messages, output: completion, usage: { promptTokens: usage.prompt_tokens, completionTokens: usage.completion_tokens, }, // Langfuse auto-calcula custo pela contagem de tokens });
Semana 2: Adicione guards determinísticos
// Aplique o circuit breaker do Padrão 1 // Adicione detecção de respostas vazias // Adicione detecção de loops // Configure alertas no Slack/PagerDuty pra violações de guards
Semana 4: Adicione evals LLM-as-Judge
// Rodar em 10% dos traces de produção // Começar com duas dimensões: acurácia + relevância // Estabelecer baselines antes de ativar alertas
Mês 2: Formatura pro stack completo
Langfuse (Tracing + Custos)
+ Pipeline de Eval Customizado (Qualidade)
+ Grafana/Datadog (Infra)
+ PagerDuty (Alertas)
Checklist de observabilidade pra LLMs
Antes de cada deploy em produção de uma feature de IA:
- Toda chamada ao LLM tá instrumentada com contexto de trace
- Contagem de tokens e nome do modelo capturados em cada chamada
- Tool calls têm logging de input/output
- Tracking de custos ativo por request e por usuário
- Limites de circuit breaker configurados (tokens, chamadas, duração)
- Guards determinísticos rodando em 100% dos traces
- Evals LLM-as-Judge rodando em traces amostrados
- Baselines estabelecidas pra métricas de qualidade
- Alertas configurados pra violações de guards e quedas de qualidade
- Log completo de prompts/completions usa sampling, não 100%
- Scrubbing de PII aplicado antes de logar prompts
- Dashboard mostra tendências de custo, qualidade e latência em tempo real
- Política de retenção de traces definida (30-90 dias tipicamente)
Agentes de IA não são software determinístico. Monitorá-los como APIs tradicionais vai te dar uma falsa sensação de segurança até o dia que eles enlouquecerem silenciosamente e você não tenha como entender por quê. Os padrões de observabilidade desse guia foram testados em batalha em sistemas de produção que processam milhões de interações de agentes por dia. O insight chave é simples: se você não consegue rastrear o raciocínio, não consegue debugar a falha. Instrumente tudo, avalie continuamente, e nunca confie num dashboard verde se você não mediu a qualidade do output do seu agente.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit