Observabilidad de LLMs a Fondo: Cómo Monitorear, Rastrear y Debuggear Agentes de IA en Producción
Tu agente de IA le acaba de costar $2,400 a un cliente. Entró en un loop infinito de tool calls a las 3 AM, quemando tokens mientras generaba respuestas sin sentido. Tu dashboard de APM tradicional muestra todo en verde: latencia normal, sin errores, sin crashes. Pero el agente estuvo respondiendo mal con total confianza durante seis horas seguidas, y no tuviste visibilidad alguna.
Esta es la brecha de observabilidad que mata productos de IA. Las herramientas de monitoreo tradicionales se construyeron para software determinístico: request adentro, response afuera, medí el tiempo. Los agentes de IA son fundamentalmente distintos. Razonan, bifurcan, llaman herramientas, buscan documentos y toman decisiones que varían con inputs idénticos. Cuando algo falla, no alcanza con mirar el status code HTTP. Necesitás rastrear la cadena de razonamiento — cada punto de decisión, cada invocación de herramienta, cada token consumido.
Esta guía cubre todo lo que necesitás para construir observabilidad de grado producción para sistemas basados en LLMs: desde trazado distribuido y evaluaciones automatizadas hasta tracking de costos y el ecosistema de herramientas. No es teoría. Son patrones probados en batalla por equipos que corren agentes que manejan millones de requests por día.
Por qué el monitoreo tradicional falla con aplicaciones LLM
Si estás corriendo agentes de IA solo con Datadog, Grafana o New Relic, estás volando a ciegas. Acá va el porqué:
El problema del determinismo
El software tradicional es determinístico. Dado el mismo input, obtenés el mismo output. El monitoreo es directo: trackeá latencia, tasa de errores y throughput. Si la latencia P99 se dispara, investigás.
Los LLMs son no determinísticos. El mismo prompt puede producir outputs distintos cada vez. Una respuesta HTTP 200 "exitosa" puede contener una respuesta completamente alucinada. Tu tasa de errores es 0%, pero tu accuracy es 40%. Las herramientas de APM tradicionales literalmente no pueden detectar este modo de falla.
El problema multi-paso
Un API call simple es un solo span: request → response. Un agente de IA es un grafo de ejecución complejo:
Query del usuario: "Encontrá el vuelo más barato de NYC a Tokio el mes que viene"
│
├─ Step 1: Clasificación de intención (LLM call, 200ms, 150 tokens)
├─ Step 2: Extracción de parámetros (LLM call, 180ms, 120 tokens)
├─ Step 3: Tool call - API de vuelos (API externa, 2.1s)
├─ Step 4: Parsing de resultados (LLM call, 250ms, 800 tokens)
├─ Step 5: Comparación de precios (LLM call, 300ms, 1200 tokens)
├─ Step 6: Generación de respuesta (LLM call, 400ms, 500 tokens)
│
Total: 5 LLM calls, 3.4s, 2770 tokens, $0.008
Cuando este agente devuelve resultados mal, ¿qué paso falló? ¿Fue la clasificación de intención? ¿La herramienta devolvió datos incorrectos? ¿El LLM alucinó durante la comparación de precios? Sin trazado a nivel de paso, debuggear es imposible.
El problema de costos
Las llamadas a LLMs son caras. A diferencia del compute tradicional donde los ciclos de CPU son esencialmente gratis, cada token tiene un costo directo en dólares. Un solo loop de agente fuera de control puede quemar cientos de dólares en minutos. Necesitás tracking de costos en tiempo real a nivel de agente, usuario y organización, y ninguna herramienta APM tradicional te lo da.
El stack de observabilidad para LLMs
Observabilidad de grado producción para LLMs requiere cuatro capas:
┌─────────────────────────────────────────────────────┐
│ Capa 4: DASHBOARDS │
│ Analíticas de costo, tendencias, SLA tracking │
├─────────────────────────────────────────────────────┤
│ Capa 3: EVALUACIÓN │
│ Evals automatizados, detección de regresión, A/B │
├─────────────────────────────────────────────────────┤
│ Capa 2: TRAZADO │
│ Trazas distribuidas, jerarquía de spans, tokens │
├─────────────────────────────────────────────────────┤
│ Capa 1: INSTRUMENTACIÓN │
│ Integración SDK, captura automática, anotaciones │
└─────────────────────────────────────────────────────┘
Construyamos cada capa desde cero.
Capa 1: Instrumentación
La instrumentación es la base. Necesitás capturar datos en cada punto de decisión sin destruir el rendimiento de tu aplicación.
OpenTelemetry para LLMs
La industria está convergiendo en OpenTelemetry (OTel) como la capa de instrumentación estándar. El proyecto OpenLLMetry extiende OTel con convenciones semánticas específicas para 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', }); // O usá el enfoque modular con OpenTelemetry directamente: 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, // Loggear prompts (¡cuidado en prod!) captureOutputs: true, // Loggear completions }), new AnthropicInstrumentation(), ], }); sdk.start();
Esto auto-instrumenta cada llamada a la API de OpenAI y Anthropic, capturando:
- Nombre del modelo y parámetros (temperature, max_tokens)
- Prompts de entrada y completions de salida
- Uso de tokens (prompt tokens, completion tokens)
- Latencia por llamada
- Detalles de tool/function calls
Anotaciones manuales de spans
La auto-instrumentación captura las llamadas al LLM, pero necesitás spans manuales para la lógica de negocio:
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: Clasificar intención 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: Ejecutar 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: Generar respuesta 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; } }); }
Qué capturar (y qué no)
Una decisión crítica: ¿qué datos loggeás?
| Dato | Capturar en Dev | Capturar en Prod | Por qué |
|---|---|---|---|
| Prompts completos | ✅ Sí | ⚠️ Sampleado | Riesgo de PII, costo de almacenamiento |
| Completions completas | ✅ Sí | ⚠️ Sampleado | Igual que arriba |
| Conteo de tokens | ✅ Sí | ✅ Sí | Tracking de costos es crítico |
| Parámetros del modelo | ✅ Sí | ✅ Sí | Debuggear regresiones |
| Inputs/outputs de tools | ✅ Sí | ✅ Sí | Esencial para debugging |
| IDs de usuario | ✅ Sí | ✅ Sí | Tracking de costo por usuario |
| Latencia por paso | ✅ Sí | ✅ Sí | Monitoreo de performance |
| Vectores de embedding | ❌ No | ❌ No | Muy grandes, raramente útiles |
| Respuestas raw de API | ✅ Sí | ❌ No | Explosión de almacenamiento |
En producción, usá sampling para el loggeo de prompts/completions completos. Capturá 100% de metadata (tokens, latencia, modelo) pero solo 10-20% del contenido de texto completo. Cuando debuggeás un issue específico, aumentá temporalmente el sampling para usuarios o queries puntuales.
Capa 2: Trazado distribuido
Con la instrumentación en su lugar, necesitás un backend de trazado que entienda datos específicos de LLMs. Acá es donde las herramientas especializadas brillan.
Estructura de trazas para agentes de IA
Una traza bien estructurada para un agente de IA se ve así:
Trace: agent_run_abc123
│
├─ Span: agent.process_query (raíz)
│ ├─ 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
Esta estructura te permite responder preguntas como:
- "¿Por qué tardó 10 segundos este agente?" → La API de vuelos tardó 8 segundos.
- "¿Por qué costó 0.02?" → El agente hizo 100 loops de tool calls.
- "¿Por qué alucinó?" → La búsqueda RAG devolvió documentos irrelevantes con scores de similitud bajos.
Implementando propagación de trazas
Para arquitecturas multi-servicio, el contexto de traza debe propagarse entre servicios:
// Servicio A: Orquestador del 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(); } // Servicio B: Servicio de ejecución de herramientas 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'); // ... ejecución del tool con linaje de traza completo span.end(); }); });
Capa 3: Evaluación automatizada
El trazado te dice qué pasó. La evaluación te dice qué tan bien salió. Esta es la capa que la mayoría de los equipos se saltea, y la que separa el éxito del fracaso de la IA en producción.
El pipeline de evaluación
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Store │ → │ Sample │ → │ Score │ → │ Alert │
│ Trazas │ │ Select │ │ Eval │ │ Report │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
10-20% de LLM-as-Judge Slack/PD
trazas + Reglas si la calidad
determinísticas baja
Evaluación LLM-as-Judge
El patrón de eval más poderoso es usar un LLM separado para juzgar los outputs de tu agente:
interface EvalResult { score: number; // 0-1 reasoning: string; // Por qué este score dimension: string; // Qué se evaluó } async function evaluateResponse( query: string, response: string, groundTruth?: string ): Promise<EvalResult[]> { const evaluations: EvalResult[] = []; // Eval 1: Exactitud factual const accuracyEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `Sos un evaluador experto. Puntuá la exactitud factual de la respuesta del AI en una escala de 0 a 1. Rúbrica: - 1.0: Todos los hechos son correctos y verificables - 0.7: Mayormente correcto con inexactitudes menores - 0.4: Contiene errores factuales significativos - 0.0: Completamente fabricado o incorrecto Respondé en JSON: { "score": number, "reasoning": string }`, }, { role: 'user', content: `Query: ${query} Respuesta AI: ${response} ${groundTruth ? `Ground Truth: ${groundTruth}` : ''}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(accuracyEval.choices[0].message.content), dimension: 'factual_accuracy', }); // Eval 2: Relevancia const relevanceEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `Puntuá qué tan relevante es la respuesta para la query. 1.0 = Responde directamente la pregunta 0.5 = Parcialmente relevante 0.0 = Completamente fuera de tema Respondé en JSON: { "score": number, "reasoning": string }`, }, { role: 'user', content: `Query: ${query}\nRespuesta: ${response}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(relevanceEval.choices[0].message.content), dimension: 'relevance', }); return evaluations; }
Guards determinísticos
No todo necesita un juez LLM. Usá checks determinísticos para patrones de falla conocidos:
interface GuardResult { passed: boolean; violation?: string; } function runDeterministicGuards( trace: AgentTrace ): GuardResult[] { const results: GuardResult[] = []; // Guard 1: Presupuesto de tokens excedido 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 ? `Presupuesto de tokens excedido: ${totalTokens} tokens` : undefined, }); // Guard 2: Detección de loops en 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} llamado ${count} veces` : undefined, }); } // Guard 3: Presupuesto de latencia const totalLatency = trace.duration; results.push({ passed: totalLatency < 30000, violation: totalLatency >= 30000 ? `Presupuesto de latencia excedido: ${totalLatency}ms` : undefined, }); // Guard 4: Respuesta vacía o sospechosamente corta const finalResponse = trace.output; results.push({ passed: finalResponse && finalResponse.length > 20, violation: !finalResponse || finalResponse.length <= 20 ? 'La respuesta está vacía o sospechosamente corta' : undefined, }); return results; }
Alertas automatizadas
Conectá las evaluaciones a tu sistema de alertas:
async function runEvalPipeline(trace: AgentTrace) { // Guards determinísticos (rápidos, corren en todas las trazas) const guardResults = runDeterministicGuards(trace); const guardViolations = guardResults .filter((r) => !r.passed); if (guardViolations.length > 0) { await sendAlert({ severity: 'high', title: 'Violación de guard del agente', details: guardViolations .map((v) => v.violation) .join('\n'), traceId: trace.traceId, }); } // LLM-as-Judge (caro, corre en trazas sampleadas) 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: 'Degradación de calidad del agente', details: lowScores .map((e) => `${e.dimension}: ${e.score} - ${e.reasoning}` ) .join('\n'), traceId: trace.traceId, }); } await storeEvalResults(trace.traceId, evalResults); } }
Capa 4: Tracking de costos y analíticas
Los costos de tokens son la factura cloud de las aplicaciones de IA. Sin tracking granular de costos, estás adivinando.
Cálculo de costos en tiempo 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 costo por traza 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 ), }; }
Umbrales de alerta de costos
// Guard de costo por request const MAX_COST_PER_REQUEST = 0.50; // $0.50 // Presupuesto por usuario por hora const MAX_COST_PER_USER_HOUR = 5.00; // $5.00 // Presupuesto por organización por día 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: `Costo de request excedido: $${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: `Presupuesto por hora del usuario ${userId} excedido`, }); } }
El panorama de herramientas: LangSmith vs Langfuse vs Arize
Elegir la plataforma de observabilidad correcta es una decisión crítica. Acá va la comparación honesta:
LangSmith
Ideal para: Equipos que ya usan 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'] } );
Fortalezas:
- Integración profunda con LangChain/LangGraph (first-party)
- Playground de prompts y versionado integrado
- Hub para compartir y descubrir prompts
- Framework de eval sólido con human-in-the-loop
- Excelente visualización de grafos de ejecución de agentes
Debilidades:
- Vendor lock-in al ecosistema LangChain
- Closed-source, solo hosted (no self-hosting)
- El pricing escala con volumen de trazas (puede salir caro)
- Soporte limitado para frameworks que no son LangChain
Langfuse
Ideal para: Equipos que quieren open-source, agnóstico al framework
// Langfuse v5+ (recomendado: @langfuse/tracing) import { observe } from '@langfuse/tracing'; // Tracing basado en decoradores (lo más simple) const processQuery = observe( { name: 'flight-search-agent' }, async (query: string) => { const intent = await classifyIntent(query); const results = await searchFlights(intent); return generateResponse(results); } ); // O usá el cliente clásico de Langfuse para control 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, }, });
Fortalezas:
- Open-source (licencia MIT), self-hosteable
- Agnóstico al framework (funciona con cualquier proveedor de LLM)
- Tracking de costos y analíticas de tokens integrado
- Gestión y versionado de prompts
- Free tier generoso
Debilidades:
- Comunidad más chica que LangSmith
- Self-hosting requiere gestión de infra
- Features de evaluación menos maduros que LangSmith
- UI menos pulida (mejorando rápidamente)
Arize Phoenix
Ideal para: Equipos con background en ML/ciencia de datos
import { trace as otelTrace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { OpenAIInstrumentation } from '@arizeai/openinference-instrumentation-openai'; registerInstrumentations({ instrumentations: [new OpenAIInstrumentation()], });
Fortalezas:
- Construido sobre OpenTelemetry (sin lock-in propietario)
- Excelente visualización de embeddings y detección de drift
- Herramientas de análisis de RAG muy buenas
- Experiencia de desarrollo local-first (Phoenix corre localmente)
- Mejor en su clase para debuggear calidad de retrieval
Debilidades:
- Curva de aprendizaje más empinada
- Menos foco en trazado de orquestación de agentes
- Ecosistema de integraciones directas más chico
- Features enterprise requieren Arize cloud
Matriz de comparación
| Feature | LangSmith | Langfuse | Arize Phoenix |
|---|---|---|---|
| Open Source | ❌ | ✅ MIT | ✅ (Phoenix) |
| Self-Hosting | ❌ | ✅ | ✅ (Phoenix) |
| Integración LangChain | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| Agnóstico al framework | ⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Tracking de costos | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Framework de eval | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| Análisis de RAG | ⭐⭐ | ⭐ | ⭐⭐⭐ |
| Gestión de prompts | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| Análisis de embeddings | ⭐ | ⭐ | ⭐⭐⭐ |
| Precio (startup) | $$$ | Gratis/$ | Gratis/$ |
Patrones y anti-patrones de producción
Patrón 1: El circuit breaker
Evitá que agentes fuera de control quemen tu presupuesto:
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( `Límite de tokens excedido: ${this.tokenCount}` ); } if (this.llmCalls > this.limits.maxLLMCalls) { throw new CircuitBreakerError( `Límite de llamadas LLM excedido: ${this.llmCalls}` ); } if (this.toolCalls > this.limits.maxToolCalls) { throw new CircuitBreakerError( `Límite de tool calls excedido: ${this.toolCalls}` ); } if (elapsed > this.limits.maxDurationMs) { throw new CircuitBreakerError( `Límite de duración excedido: ${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); }
Patrón 2: Workflow de debugging basado en trazas
Cuando algo se rompe, seguí este enfoque sistemático:
1. DETECTAR: La eval automatizada flaguea caída de calidad
↓
2. IDENTIFICAR: Filtrar trazas por eval scores bajos
↓
3. COMPARAR: Side-by-side con trazas exitosas
↓
4. AISLAR: Encontrar el punto de divergencia
↓
5. CAUSA RAÍZ: Examinar inputs/outputs en ese span
↓
6. CORREGIR: Actualizar prompt, contexto o config del tool
↓
7. VALIDAR: Correr evals del fix contra dataset de test
Anti-patrón 1: Loggear todo
No loggees cada token de cada request.
// ❌ No hagas esto logger.info('LLM Response', { fullPrompt: systemPrompt + userMessage + context, // 50KB fullResponse: completion, // 10KB metadata: entireTraceObject, // 5KB }); // Resultado: 65KB por request × 1M requests/día = 65GB/día // ✅ Hacé esto 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/día = 200MB/día
Anti-patrón 2: Tratar errores de LLM como errores HTTP
// ❌ Engañoso: HTTP 200 pero la respuesta del agente es terrible if (response.status === 200) { metrics.increment('agent.success'); } // ✅ Correcto: Medí la calidad real const evalScore = await quickEval(response.body); if (evalScore > 0.7) { metrics.increment('agent.quality.good'); } else { metrics.increment('agent.quality.poor'); // Este es el verdadero "error" — disparar investigación }
Anti-patrón 3: Sin baseline
// ❌ Alerta: "Eval score es 0.72" — ¿Eso es bueno? ¿Malo? // ✅ Primero establecé una baseline // Semana 1-2: Recolectá eval scores sin alertar // Semana 3: Calculá baselines P50, P90, P99 // Semana 4+: Alertá en desviaciones de la 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 si es 20% menor a la mediana }
El stack de observabilidad mínimo viable
Si estás empezando de cero, acá va el camino más rápido a observabilidad de grado producción:
Día 1: Instrumentación básica
// 1. Instalá Langfuse (lo más rápido para arrancar) // npm install langfuse import Langfuse from 'langfuse'; const langfuse = new Langfuse(); // 2. Wrapeá la función principal de tu agente async function runAgent(query: string, userId: string) { const trace = langfuse.trace({ name: 'agent-run', userId, input: query, }); // Tu código de agente existente... trace.update({ output: response }); await langfuse.flushAsync(); }
Semana 1: Agregá tracking de costos
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 costo desde conteo de tokens });
Semana 2: Agregá guards determinísticos
// Aplicá el circuit breaker del Patrón 1 // Agregá detección de respuestas vacías // Agregá detección de loops // Configurá alertas en Slack/PagerDuty ante violaciones de guards
Semana 4: Agregá evals LLM-as-Judge
// Correr en 10% de trazas de producción // Empezar con dos dimensiones: accuracy + relevancia // Establecer baselines antes de activar alertas
Mes 2: Graduarse al stack completo
Langfuse (Trazado + Costos)
+ Pipeline de Eval Custom (Calidad)
+ Grafana/Datadog (Infra)
+ PagerDuty (Alertas)
Checklist de observabilidad para LLMs
Antes de cada deploy a producción de un feature de IA:
- Cada llamada al LLM está instrumentada con contexto de traza
- Conteo de tokens y nombre de modelo capturado en cada llamada
- Tool calls tienen logging de input/output
- Tracking de costos activo a nivel de request y usuario
- Límites de circuit breaker configurados (tokens, llamadas, duración)
- Guards determinísticos corriendo en 100% de las trazas
- Evals LLM-as-Judge corriendo en trazas sampleadas
- Baselines establecidas para métricas de calidad
- Alertas configuradas para violaciones de guards y caídas de calidad
- Logging de prompts/completions completo usa sampling, no 100%
- Scrubbing de PII aplicado antes de loggear prompts
- Dashboard muestra tendencias de costo, calidad y latencia en tiempo real
- Política de retención de trazas definida (30-90 días típicamente)
Los agentes de IA no son software determinístico. Monitorearlos como APIs tradicionales te va a dar una falsa sensación de seguridad hasta el día que se descontrolen silenciosamente y no tengas forma de entender por qué. Los patrones de observabilidad de esta guía fueron testeados en batalla en sistemas de producción que manejan millones de interacciones de agentes por día. El insight clave es simple: si no podés rastrear el razonamiento, no podés debuggear la falla. Instrumentá todo, evaluá continuamente, y nunca confíes en un dashboard verde si no mediste la calidad del output de tu agente.
Explora herramientas relacionadas
Prueba estas herramientas gratuitas de Pockit