Observabilidad para Sistemas de IA
Tu dashboard de Datadog está en verde. Todos los health
checks pasan. Los tiempos de respuesta están dentro del SLA.
Y tu funcionalidad de IA está produciendo respuestas
seguras, bien formateadas y completamente incorrectas para
el 12% de las consultas. Nadie lo sabe.
Esta es la brecha que la observabilidad tradicional nunca
fue diseñada para cerrar. Pasé seis meses construyendo
monitoreo para sistemas con LLM en producción, y la lección
más grande fue esta: las herramientas que tenemos funcionan
bien para la capa de infraestructura. Son inútiles para la
capa de inteligencia.
Por Qué el APM Tradicional Falla con IA
Application Performance Monitoring fue diseñado para un
mundo donde el código es determinista. Envías una petición,
recibes una respuesta, y la respuesta es correcta o tu
código tiene un bug. Puedes escribir aserciones. Puedes
comparar salidas. Puedes alertar con códigos de error.
Los sistemas de IA rompen cada una de esas suposiciones.
Misma entrada, distinta salida. Envía el mismo prompt a
GPT-4o dos veces y podrías obtener dos respuestas
diferentes. Ambas podrían ser correctas. Ambas podrían
estar mal. El código 200 OK no te dice nada sobre si la
respuesta fue realmente útil.
La latencia es una distribución, no un número. Un
prompt de 50 tokens puede regresar en 800ms. Un prompt de
2,000 tokens con un system message complejo puede tardar
8 segundos. Ambos están "funcionando correctamente." Tu
latencia p50 no significa nada si tu p99 es 10x mayor por
la variación en longitud de prompts.
"Éxito" es subjetivo. Cuando un usuario pregunta "¿Cuál
es nuestra política de devoluciones?" y el LLM responde con
una política plausible pero desactualizada de 2023, ¿eso es
un éxito? Tu HTTP status dice que sí. Tu cliente dice que
no. No existe un código de error para "técnicamente fluido
pero factualmente incorrecto."
Intenté montar monitoreo de LLM sobre nuestro setup
existente de Datadog. Obtuvimos gráficas bonitas que nos
decían que el API estaba arriba. No teníamos idea de si las
respuestas eran buenas.
Los Tres Pilares de la Observabilidad para IA
Llegué a tres pilares que realmente importan para sistemas
de IA en producción. No métricas, logs y traces -- esos
son detalles de implementación. Los pilares son lo que
realmente estás tratando de observar.
1. Economía de Tokens
Cada llamada a un LLM tiene un costo, y ese costo varía
enormemente según la selección de modelo, la longitud del
prompt y la longitud de la salida. Si no estás rastreando
esto por llamada, estás volando a ciegas en unit economics.
Registro cinco números en cada llamada a un LLM:
interface LLMCallMetrics {
model: string // 'gpt-4o' | 'claude-sonnet-4-20250514'
inputTokens: number // prompt + system message
outputTokens: number // longitud del completion
costUsd: number // calculado desde pricing del modelo
cachedTokens: number // cache hits del prompt (si aplica)
}
Por qué cada campo importa:
- model: Vas a tener múltiples modelos en producción.
El costo por consulta no significa nada sin saber qué
modelo la sirvió.
- inputTokens: Esta es tu palanca de control de costos.
Prompt engineering es realmente ingeniería de tokens.
- outputTokens: La parte que no puedes controlar del
todo. Configura
max_tokens como un guardrail, no como
un objetivo.
- costUsd: Pre-calcula esto. No hagas que tu equipo de
finanzas lo descifre de la factura del API.
- cachedTokens: Tanto Anthropic como OpenAI soportan
prompt caching. Si tu tasa de cache hit está por debajo
del 60% en system prompts repetidos, estás dejando dinero
sobre la mesa.
Rastreamos costo por funcionalidad, por segmento de usuario
y por modelo. Nuestro reporte semanal de costos muestra
exactamente a dónde va cada dólar. Cuando una funcionalidad
subió 3x en costo de un día para otro, lo detectamos en
el standup de la mañana porque el dashboard lo marcó --
no tres semanas después en la factura.
2. Señales de Calidad
Esta es la parte difícil. ¿Cómo mides si una respuesta de
IA fue "buena"?
No puedes automatizar el criterio. Pero puedes construir
proxies que correlacionen con calidad, y puedes rastrearlos
en el tiempo para detectar regresiones.
Tasa de alucinación. Si tu sistema usa RAG, puedes
medir si la respuesta está fundamentada en el contexto
recuperado. Uso un clasificador ligero que verifica si las
afirmaciones clave en la respuesta aparecen en los
documentos fuente. No es perfecto, pero atrapa las
fabricaciones obvias. Nuestro objetivo: menos del 3% de
tasa de alucinación en consultas factuales.
Precisión de citas. Cuando el sistema cita una fuente,
¿esa fuente es real? ¿Realmente dice lo que el sistema
afirma? Registramos la URL de la cita, la frase citada
y un booleano que indica si la cita existe en la fuente.
Esto es barato de verificar y atrapa un modo de falla
común.
Señales de satisfacción del usuario. Pulgar arriba o
abajo en las respuestas. Si el usuario reformuló su
consulta (una señal de que no obtuvo lo que necesitaba).
Tasa de abandono de sesión después de una interacción con
IA. Tiempo hasta la siguiente acción después de recibir
una respuesta. Ninguna es perfecta. Juntas, pintan un
panorama.
Consistencia de respuestas. Para la misma clase de
consulta, ¿cuánto varía la respuesta? Alta varianza en
preguntas factuales es una señal de alerta. Alta varianza
en tareas creativas podría estar bien. Hasheo la estructura
semántica de las respuestas (no el texto exacto) y rastrea
la varianza por categoría de consulta.
Construimos un dashboard de calidad que muestra estas
métricas en una ventana rodante de 7 días. Cuando la tasa
de alucinación cruza el 5%, recibimos una alerta en Slack.
Ha disparado tres veces en seis meses, y cada vez atrapó
una regresión real -- generalmente un cambio de prompt que
inadvertidamente debilitó las instrucciones de
fundamentación.
3. Perfilado de Latencia
La latencia de un LLM no es un solo número. Es una pila
de números, y necesitas descomponerla para optimizarla.
Latencia total: 4,200ms
├── Generación de embedding: 45ms
├── Búsqueda vectorial: 120ms
├── Reranking: 280ms
├── Ensamblaje de contexto: 15ms
├── LLM time-to-first-token: 890ms
├── LLM streaming: 2,800ms
└── Post-procesamiento: 50ms
El desglose importa porque la estrategia de optimización
difiere para cada segmento. ¿Latencia de embedding? Ponlo
en cache. ¿Búsqueda vectorial lenta? Revisa la
configuración de tu índice. ¿El streaming del LLM tarda
demasiado? Quizá tu ventana de contexto está inflada y
necesitas recortar los chunks recuperados.
Rastrea tres percentiles de latencia por segmento: p50,
p95 y p99. El p50 te dice sobre la experiencia típica.
El p99 te dice sobre tus peores casos. En nuestro sistema,
el p99 era 6x el p50 -- casi completamente por la
variación en longitud del prompt. Agregamos un presupuesto
de tokens que limita el contexto a 4,000 tokens y el p99
bajó un 40%.
Qué Registrar en Cada Llamada a un LLM
Este es el payload exacto que adjunto a cada llamada a un
LLM en producción. Cada campo se ganó su lugar a través de
una sesión de debugging donde deseé haberlo tenido.
interface LLMCallLog {
// Identidad
traceId: string
spanId: string
parentSpanId: string | null
feature: string // 'search' | 'chat' | 'summary'
// Request
model: string
promptHash: string // SHA-256 de system + user prompt
inputTokens: number
temperature: number
maxTokens: number
// Response
outputTokens: number
responseHash: string // SHA-256 del completion
finishReason: string // 'stop' | 'length' | 'tool_use'
toolCalls: string[] // nombres de tools invocados
// Economía
costUsd: number
cachedTokens: number
// Timing
latencyMs: number
ttftMs: number // time to first token
// Calidad
groundednessScore: number | null // 0-1, solo RAG
userFeedback: 'positive' | 'negative' | null
}
El promptHash merece mención especial. No registro el
prompt completo -- eso es un problema de privacidad y
almacenamiento. Pero lo hasheo para poder correlacionar
regresiones de calidad con cambios de prompt. Cuando la
tasa de alucinación sube, verifico si el prompt hash
cambió recientemente. Generalmente sí.
El campo finishReason me ha salvado dos veces. Una
cuando un cambio de prompt causó que el 30% de las
respuestas alcanzara el límite de tokens y se truncara
a media oración. El dashboard mostró
finish_reason: 'length' subiendo del 2% al 30% y lo
detectamos en menos de una hora.
Arquitectura de Traces para Flujos de Agentes
Las llamadas individuales a un LLM son directas. Los
flujos de agentes -- donde una llamada a un LLM dispara
el uso de herramientas, lo cual dispara otra llamada al
LLM, que dispara más herramientas -- son donde la
observabilidad se pone interesante.
Uso spans de OpenTelemetry con una jerarquía padre-hijo
que refleja la ejecución del agente:
Agent Trace (span padre)
├── Paso de Planificación (span hijo)
│ ├── LLM Call: "¿Qué tools necesito?" (span hoja)
│ └── Decisión: usar [search, calculator]
├── Tool: search (span hijo)
│ ├── Formulación de query (span hoja)
│ ├── API call al servicio de búsqueda (span hoja)
│ └── Parseo de resultados (span hoja)
├── Tool: calculator (span hijo)
│ └── Computación (span hoja)
└── Paso de Síntesis (span hijo)
├── LLM Call: "Combinar resultados" (span hoja)
└── Formateo de respuesta (span hoja)
Cada span lleva los campos de LLMCallLog cuando implica
una llamada a un LLM. El span padre agrega: costo total,
latencia total, tokens totales, número de llamadas al LLM,
número de invocaciones de herramientas.
Esta estructura me permite responder preguntas que logs
planos no pueden:
- "¿Cuál tool call es el cuello de botella en este flujo?"
- "¿Qué porcentaje de ejecuciones del agente usan más de
3 tool calls?" (El nuestro: 15%. Esas ejecuciones
cuestan 4x más.)
- "Cuando el agente reintenta un tool call, ¿el segundo
intento tiene éxito?" (El nuestro: 60% de las veces.
El otro 40% es gasto desperdiciado.)
El detalle clave de implementación: propaga el contexto de
trace a través de los tool calls. Si tu herramienta de
búsqueda hace sus propias llamadas API, esas deben ser
spans hijos del span de la herramienta, no traces
huérfanos. La propagación de contexto de OpenTelemetry
maneja esto, pero tienes que conectarlo deliberadamente.
Diseño de Alertas: Qué Merece una Llamada
No todo lo que es interesante es accionable. Aprendí esto
por las malas después de configurar 23 alertas y recibir
llamadas por cosas que no requerían acción inmediata.
Esto es lo que sobrevivió la poda:
Amerita llamada (PagerDuty, inmediato):
- Costo por hora excede 2x el promedio rodante de 7 días.
Esto atrapa loops descontrolados, ataques de prompt
injection que causan uso excesivo de tokens, y upgrades
de modelo que silenciosamente aumentan el precio por
token.
- La latencia p99 excede 15 segundos. A este punto, los
usuarios están abandonando. Algo está roto.
- La tasa de error en llamadas al LLM excede el 5%. El
proveedor tiene una caída o tu API key está limitada.
Amerita alerta (Slack, horario laboral):
- La tasa de alucinación excede el 5% en una ventana de
24 horas. Regresión de calidad. Investiga cambios de
prompt o cambios en el pipeline de retrieval.
finish_reason: 'length' excede el 10%. Las respuestas
se están truncando. O los prompts se hicieron más largos
o max_tokens necesita ajuste.
- La tasa de cache hit baja del 40%. Estás pagando por
computación redundante.
Solo dashboard (revisar semanalmente):
- Tendencias de costo por funcionalidad. Útil para
planificación de capacidad, no para respuesta a
incidentes.
- Distribución de modelos. ¿Qué porcentaje de llamadas
va a cada modelo? Patrones cambiantes indican cambios
de routing.
- Ratio de feedback de usuarios. Las tendencias importan,
las fluctuaciones diarias no.
La alerta de anomalía de costos es la más valiosa que
tenemos. Disparó a las 2 AM de un martes cuando un deploy
introdujo un bug que causó que el agente entrara en loop
infinito. Para cuando desperté, la alerta había activado
nuestro circuit breaker automático que limita el gasto a
$50/hora. Daño total: $127 en vez de lo que habría sido
$2,000+ para la mañana.
El Stack que Uso
No voy a pretender que hay una herramienta perfecta. Esto
es lo que realmente corro en producción y por qué.
OpenTelemetry para la recolección de traces. Es
vendor-neutral, tiene buen soporte de SDK en TypeScript,
y el modelo de spans se mapea naturalmente a flujos de
agentes. Exporto a un collector que distribuye a múltiples
backends.
Supabase para datos de costo y uso. Cada LLMCallLog
se inserta en una tabla de Postgres. Elegí esto sobre una
base de datos de series temporales porque necesito hacer
JOINs entre datos de llamadas al LLM con datos de usuario,
feature flags y métricas de negocio. SQL es la herramienta
correcta cuando tus queries involucran JOINs. Corremos una
materialized view que agrega costo por funcionalidad,
modelo y hora. El dashboard consulta la materialized view,
no la tabla raw.
CREATE MATERIALIZED VIEW llm_cost_hourly AS
SELECT
date_trunc('hour', created_at) AS hour,
feature,
model,
COUNT(*) AS call_count,
SUM(input_tokens) AS total_input_tokens,
SUM(output_tokens) AS total_output_tokens,
SUM(cost_usd) AS total_cost,
AVG(latency_ms) AS avg_latency,
PERCENTILE_CONT(0.99)
WITHIN GROUP (ORDER BY latency_ms) AS p99_latency
FROM llm_calls
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY 1, 2, 3;
Langfuse para tracking de calidad. Está construido
específicamente para observabilidad de LLMs y maneja las
cosas que las herramientas de propósito general no manejan:
versionamiento de prompts, scoring de evaluaciones y
métricas de calidad a nivel de sesión. Envío las señales
de calidad (score de fundamentación, feedback de usuario,
precisión de citas) a Langfuse y lo uso para A/B testing
de prompts y detección de regresiones.
La división es intencional. Costo y latencia son
preocupaciones de infraestructura -- pertenecen al mismo
sistema que tus otros datos operacionales. Calidad es una
preocupación de producto -- necesita herramientas
especializadas que entiendan los modos de falla específicos
de los LLMs.
La Meta-Lección
Seis meses después de correr observabilidad de IA en
producción, el patrón que sigo viendo es este: los equipos
instrumentan lo fácil (latencia, tasas de error, uptime)
e ignoran lo difícil (calidad, eficiencia de costos,
satisfacción del usuario).
Lo fácil te dice si tu sistema está corriendo. Lo difícil
te dice si tu sistema está funcionando.
Son preguntas diferentes, y necesitas herramientas
diferentes para responderlas. Tu dashboard de APM no va
a desaparecer. Pero ya no es suficiente. La capa
probabilística necesita su propio stack de observabilidad,
sus propios umbrales de alerta y sus propios playbooks
de guardia.
Empieza con tres cosas: registra cada llamada al LLM con
los campos que listé arriba, construye un dashboard de
costos que se actualice cada hora, y agrega una métrica
de tasa de alucinación a tu dashboard de calidad. Puedes
hacer las tres en un fin de semana. El resto --
arquitectura de traces, ajuste de alertas, clasificadores
de calidad -- puede venir después, informado por lo que
realmente veas en producción.
La peor observabilidad es la que construyes después del
incidente. La segunda peor es la que monitorea las cosas
equivocadas. Apunta a la tercera opción: monitoreo que te
diga lo que tus usuarios experimentan, no solo lo que tu
infraestructura reporta.