Módulo 3: Features Esenciales para RAG
Cápsula 07: Observability — qué medir cuando ya nadie debugea con prints
Descripción de la cápsula
En desarrollo, debugear un RAG es trivial: ejecutás el script, lees los prints, ves qué chunks devolvió, comparás con la respuesta del LLM. En producción nada de eso aplica. El sistema procesa 10,000 queries por hora, cada query genera 5-7 chunks, y el LLM contesta cosas distintas según el contexto. Si una query devuelve mala respuesta a las 3 AM de un sábado, ¿cómo te enterás? ¿Cómo investigás la causa? ¿Cómo distinguís un bug real de varianza esperada?
Observability es la respuesta. No es "agregar logs" — es diseñar el sistema para que sea diagnosticable desde el inicio, midiendo continuamente las cinco dimensiones que importan para RAG: latencia, throughput, accuracy, costo y health del índice. Cuando algo falla, la observability te dice dónde falla en menos de 5 minutos en lugar de 5 horas. Cuando el sistema funciona bien, te dice por qué y cómo replicar ese estado.
Esta cápsula es conceptual — el código operativo profundo viene en M07 (Production Considerations) y guía #18 (Monitoring & Observability). Acá construís el modelo mental: qué importa medir, por qué, qué thresholds disparan acción, y cómo diseñar el schema de métricas desde el día 1 para que tu yo del futuro no te odie.
Al finalizar esta cápsula serás capaz de:
- ✅ Listar las cinco dimensiones críticas a monitorear en RAG (latencia, throughput, accuracy, costo, índice)
- ✅ Diferenciar entre métricas de health (sistema funciona) y métricas de quality (sistema funciona bien)
- ✅ Distinguir qué medir continuamente vs qué medir periódicamente (golden set evaluation)
- ✅ Diseñar alerting basado en SLO (Service Level Objectives) en vez de alertas arbitrarias
- ✅ Anticipar la trampa más cara: optimizar lo que medís y romper lo que no medís
Tiempo estimado: 25-30 minutos
Por qué necesitás observability desde el día 1
Tres situaciones que pasan en sistemas RAG sin observability:
Situación 1 — el query lento sin causa visible. Los usuarios se quejan de que "el bot está lento". Vos abrís el código, no ves nada raro. Probás localmente y va rápido. ¿Es la red? ¿El LLM? ¿Vector DB? Sin métricas instrumentadas, son 4 horas de bisección manual antes de descubrir que era el rate limit de OpenAI activándose en horas pico.
Situación 2 — la respuesta mala que el cliente reporta. Un cliente dice "esta respuesta está mal, dijo X cuando la documentación dice Y". Vos abrís el sistema y... ¿cómo reproducís la query exacta de hace 3 días? ¿Qué chunks devolvió? ¿Qué prompt usó? Sin trazas, no podés ni empezar a investigar.
Situación 3 — la degradación lenta que nadie nota. Tu accuracy baja del 95% al 78% a lo largo de 3 meses. Nadie se entera porque no hay test continuo. Los usuarios se acostumbran a respuestas peores. Hasta que el competidor mejora y empezás a perder clientes — y solo entonces te das cuenta de que llevás meses con un sistema degradado.
Cada una de estas se previene con observability bien diseñada. El costo de instrumentar el día 1 es ~10% del tiempo de desarrollo. El costo de instrumentar después de un incidente es 10x — más el costo del incidente.
Las cinco dimensiones que importan en RAG
Dimensión 1: Latency (¿qué tan rápido responde?)
Métricas a registrar por cada query:
{
"total_latency_ms": 245, # Tiempo end-to-end
"embedding_latency_ms": 110, # Tiempo de embebir la query (OpenAI)
"retrieval_latency_ms": 18, # Tiempo de búsqueda en ChromaDB
"generation_latency_ms": 117, # Tiempo de generation con GPT
"total_chunks_retrieved": 5,
}
Por qué desglosar: "el sistema está lento" es información inútil. "El embedding tarda 110ms" te dice que el cuello de botella es OpenAI, no ChromaDB. El desglose es lo que permite arreglar el problema correcto.
Métricas agregadas a calcular:
p50_latency,p95_latency,p99_latencypor componente- Distribución de latencias en histograma (10ms, 50ms, 100ms, 200ms, 500ms, >500ms)
SLO típicos para RAG production:
| Componente | Target p95 | Target p99 |
|---|---|---|
| Embedding (OpenAI) | <200ms | <400ms |
| Retrieval (ChromaDB) | <50ms | <150ms |
| Generation (GPT-4o-mini) | <800ms | <2000ms |
| Total | <1200ms | <3000ms |
Si tu producto tolera más latencia (chatbot async, bot interno), los targets son más relajados. Si compite con buscadores (Google ~200ms total), son más estrictos.
Dimensión 2: Throughput (¿cuánto trabajo pasa por el sistema?)
Métricas:
- QPS (queries per second): carga actual y promedio
- Concurrent users: cuántos usuarios activos al mismo tiempo
- Rate of fallbacks: % de queries donde el LLM dijo "no tengo información" (metric de cobertura del dataset)
Por qué importa: dimensiona tu infraestructura. Si tu pico es 50 QPS y tu sistema soporta 100, todo bien. Si tu pico es 50 QPS y tu sistema soporta 60, una promoción de marketing puede tirar el servicio.
Alerta típica: if qps_current > 0.8 * qps_capacity: alert("approaching capacity"). Hace falta saber el qps_capacity real de tu sistema, no el teórico — eso lo descubrís haciendo load testing antes de producción.
Dimensión 3: Accuracy (¿qué tan bien responde?)
Esta es la más sutil. La latencia se mide trivial; la accuracy requiere golden set evaluation.
Cómo funciona:
- Construís un golden set: 30-100 queries con respuestas anotadas por humanos como "correctas" o "esperadas".
- Ejecutás el golden set contra el sistema periódicamente (diario, semanal).
- Métricas que produce:
- Recall@K: % de chunks correctos en los top-K retrieved
- MRR (Mean Reciprocal Rank): qué tan alto rankea el primer chunk correcto
- NDCG@K: ranking ponderado de relevancia
- Answer correctness: evaluación humana o LLM-as-judge sobre la respuesta final
Trampa común: medir solo retrieval accuracy, no answer correctness. El sistema puede recuperar los chunks correctos y aún así el LLM puede contestar mal (alucinación, ignorar contexto). Las dos métricas miden cosas distintas.
# Pseudo-código de golden set evaluation
def evaluate_golden_set(rag_system, golden_set):
metrics = {"retrieval_accuracy": 0, "answer_correctness": 0}
for item in golden_set:
response = rag_system.ask(item["query"])
# Métrica 1: ¿retrieval encontró los chunks correctos?
retrieved_ids = {s["doc_id"] for s in response.sources}
relevant_ids = set(item["expected_doc_ids"])
if retrieved_ids & relevant_ids:
metrics["retrieval_accuracy"] += 1
# Métrica 2: ¿la respuesta es correcta?
is_correct = check_answer(response.answer, item["expected_answer"])
if is_correct:
metrics["answer_correctness"] += 1
n = len(golden_set)
return {
"retrieval_accuracy": metrics["retrieval_accuracy"] / n,
"answer_correctness": metrics["answer_correctness"] / n,
}
Frecuencia recomendada: diario en producción. Si la métrica baja >5% del baseline, alertar para investigar.
Profundización: la guía #12 (Evaluation Frameworks) cubre esto en detalle, incluyendo Ragas, TruLens y LLM-as-judge.
Dimensión 4: Costo (¿cuánto cuesta cada query?)
{
"embedding_cost_usd": 0.000010, # OpenAI embedding de la query
"generation_cost_usd": 0.000180, # OpenAI GPT-4o-mini generation
"total_cost_usd": 0.000190,
}
Métricas agregadas:
- Costo total mensual por componente (embeddings, generation, vector DB hosting)
- Costo promedio por query
- Costo por usuario activo
Por qué medir: sin tracking, los costos crecen sin freno. Un cambio en el prompt que agrega 2K tokens de contexto puede subir el costo mensual 30% sin que nadie lo note hasta la factura.
Trampa común: medir solo el costo de OpenAI generation (típicamente el más alto) e ignorar embeddings y vector DB. Cuando el dataset crece a millones, el costo de re-embedding (cuando cambiás de modelo) y de hosting de la vector DB pueden volverse significativos.
Dimensión 5: Index health (¿está bien el índice?)
Estas son métricas operacionales del backend:
{
"index_size_bytes": 6_400_000_000, # ~6 GB para 1M vectores 1536-dim
"total_documents": 1_023_456,
"ram_usage_pct": 78, # % de RAM disponible usada
"failed_inserts_last_hour": 12,
"failed_queries_last_hour": 0,
"last_index_rebuild": "2026-05-01T03:15:00Z",
"ef_search_current": 50,
"ef_construction_at_build": 200,
}
Alertas:
failed_inserts > 100/hour: probablemente rate limit o problema de networkram_usage_pct > 90: peligro de OOM, escalar o migrar a IVF+PQlast_index_rebuild>30 días: considerar rebuild para mantener calidad
Continuo vs periódico — qué medir cuándo
No todas las métricas necesitan medirse en cada query. Algunas son demasiado caras o ruidosas a ese ritmo.
Métricas continuas (cada query)
- Latencia (todos los componentes)
- Costo
- Cantidad de chunks retrieved
- Si hubo fallback (LLM dijo "no sé")
- Errores (timeout, rate limit, etc.)
Estas se loguean en un sistema de observability (Datadog, Prometheus, OpenTelemetry) y se procesan en tiempo real para dashboards.
Métricas periódicas (golden set evaluation)
- Recall@K
- MRR
- Answer correctness
Estas requieren ejecutar contra un set fijo de queries con respuestas conocidas. Se corren diaria o semanalmente, no cada query (sería caro y ruidoso).
Métricas por evento
- Drift detection: cuando cambia el modelo de embeddings o el prompt, comparar accuracy del antes/después en el golden set
- Regression alerts: PR que mete cambio al sistema corre el golden set en CI; si baja accuracy >2%, bloquea el merge
Diseñar alerting con SLO, no thresholds arbitrarios
El error común: definir alertas con valores que "suenan razonables" sin justificación.
# ❌ Thresholds arbitrarios
if p95 > 200:
alert()
if accuracy < 0.85:
alert()
¿Por qué 200ms y no 250ms? ¿Por qué 0.85 y no 0.80? Sin justificación, vas a tener alertas que disparan sin que sea problema real (alert fatigue) o no disparan cuando sí lo es.
El approach correcto: SLO (Service Level Objectives) basados en lo que el producto necesita:
# ✅ SLO derivado de requisitos del producto
# "El 95% de las queries deben responder en <500ms" → SLO = p95 latency <500ms
# "Tolerable hasta 1% de error rate por hora" → SLO = error rate <1%
# "Accuracy debe mantenerse dentro del 5% del baseline" → SLO = accuracy >= baseline * 0.95
SLOs = {
"p95_latency_ms": 500,
"error_rate_pct": 1.0,
"accuracy_relative_to_baseline": 0.95,
}
# Alerta cuando llevás más de 5 minutos fuera del SLO
def check_slo(metric_name, value, threshold):
if not within_slo(value, threshold):
increment_breach_counter(metric_name)
if breach_counter[metric_name] > 5: # 5 minutos consecutivos
alert(severity="page_oncall", metric=metric_name, value=value)
Niveles de severidad:
- Page (despertar a alguien): breach de SLO crítico (sistema down, accuracy <80% del baseline)
- Ticket (revisar mañana): breach de SLO de calidad menor (latency 10% sobre target sostenido)
- Log only (no acción inmediata): anomalía detectada pero dentro de tolerancia (1 query en 10K tardó 3s)
Regla de oro: una alerta que despierta a alguien debería tener acción clara para resolverla. Si la alerta dispara y nadie sabe qué hacer, no es alerta útil — es ruido.
La trampa más cara: optimizar lo que medís y romper lo que no
Goodhart's Law aplicado: "Cuando una métrica se convierte en objetivo, deja de ser una buena métrica."
Ejemplo concreto: mediste latencia y nada más. El equipo optimiza p95 de 500ms a 200ms agresivamente. ¿Cómo? Bajan n_results de 10 a 3, y ef_search de 50 a 10. Latencia mejora, todo se ve verde en el dashboard.
Lo que no estabas midiendo: accuracy bajó del 92% al 76%. El sistema responde rápido con respuestas peores. Los usuarios empiezan a quejarse, pero no en métricas — en tickets de soporte que tardás semanas en correlacionar con el cambio.
Cómo prevenir:
- Medir las cinco dimensiones siempre, no solo las que parecen importantes. Un cambio que mejora una métrica puede empeorar otra silenciosamente.
- Tener "métricas de protección": cuando optimizás latencia, definir un SLO en accuracy que NO debe romperse. Si lo rompe, revertir el cambio aunque la latencia haya mejorado.
- Compound metrics: monitorear ratios como "latencia × (1 / accuracy)" — empeora si cualquiera de las dos se degrada.
- A/B testing antes de deployar cambios grandes — comparar las dos versiones contra el golden set + métricas continuas durante 1-2 semanas antes de promover.
Trampas y errores comunes
Trampa 1: medir promedios, ignorar percentiles
Error: dashboard muestra "promedio latency 80ms" → se ve bien.
Realidad: 90% de queries responden en 30ms, 10% en 500ms. Promedio 80ms esconde que 1 de cada 10 usuarios sufre.
Cómo prevenir: siempre p50, p95, p99 (cubierto en M04/06). El promedio solo se reporta como complemento.
Trampa 2: golden set que envejece
Error: golden set armado hace 8 meses con queries que ya no representan el uso real. La accuracy reportada se mantiene en 95%, pero las queries que de verdad llegan están fallando.
Cómo prevenir: revisitar el golden set cada 3-6 meses. Mejor aún: muestrear 20-50 queries reales por mes (anonimizadas), anotarlas, y agregarlas al golden set rotativo.
Trampa 3: alerting sin runbook
Error: alerta dispara "p95 high". El que está de guardia no sabe qué hacer. Llama al equipo. Tardan 2 horas en triagear.
Cómo prevenir: cada alerta debe tener un runbook asociado:
## Alerta: p95_latency_ms > 500 por >5 min
**Pasos:**
1. Verificar dashboard de embedding latency (https://...). Si >300ms p95, problema con OpenAI → ver runbook OpenAI-Outage.
2. Verificar QPS actual vs capacity. Si >85%, escalar manualmente: `kubectl scale deployment rag-api --replicas=8`.
3. Verificar errors en logs últimos 5 min: `kubectl logs ... | grep ERROR`. Si hay rate limit, aumentar backoff temporal.
4. Si nada de lo anterior aplica, hacer rollback al deploy anterior: `./scripts/rollback.sh`. Investigación post-mortem mañana.
Trampa 4: solo métricas técnicas, sin métricas de producto
Error: monitoreás latencia, accuracy técnica, costo. Pero no monitoreás "% de usuarios que vuelven a hacer otra query después de la primera" o "rating promedio de respuesta" o "% de fallbacks".
Síntoma: los técnicos dicen "todo está verde" mientras el producto está en caída de retención.
Cómo prevenir: alinear con el equipo de producto. Definir 2-3 métricas de impacto al usuario (retention, NPS, % de queries con thumbs up) y monitorear esas también.
Trampa 5: trazas sin contexto suficiente para reproducir
Error: loggeás "query: 'how do I configure X', response: 'no information found'". 3 días después no podés saber por qué falló — ¿qué chunks se recuperaron? ¿Cuál fue el prompt exacto? ¿Qué versión del modelo era?
Cómo prevenir: loggear traza completa por query (con muestreo en producción para no llenar storage):
trace = {
"trace_id": "uuid-...",
"timestamp": "2026-05-08T...",
"query": query_text,
"query_embedding_model": "text-embedding-3-small",
"retrieved_chunks": [{"doc_id": ..., "score": ..., "text_preview": ...}],
"prompt_template_version": "v3",
"llm_model": "gpt-4o-mini",
"llm_temperature": 0,
"response": response_text,
"fallback": False,
"user_feedback": None, # se completa después si el usuario rate-ea
}
Sample 10% en producción, 100% en staging. Storage barato, valor alto cuando aparece un bug.
Trampa 6: privacy violations en logs
Error: loggeás query completa del usuario. Algunas queries contienen PII (nombres, emails, datos médicos). Logs no encriptados se vuelven liability legal.
Cómo prevenir:
- Definir qué se loguea y qué no según políticas de privacidad y regulación (GDPR, HIPAA si aplica).
- PII redaction antes de loguear (regex sobre emails, IDs).
- Encryption at rest para logs.
- Retention policies (ej: borrar trazas detalladas a los 30 días).
Ejercicio aplicado
Escenario: te uniste a un equipo donde el RAG está en producción hace 6 meses sin observability seria. Los logs son print() en stdout. No hay golden set. Hay un dashboard que muestra "promedio de latency = 120ms" y nada más.
Síntomas reportados:
- Algunos usuarios dicen "el bot responde lento", otros dicen "responde mal"
- Nadie sabe si el sistema empeoró o siempre fue así
- El equipo de producto pide "¿podemos saber qué tipos de query fallan más?" y nadie tiene respuesta
Tu trabajo: diseñá un plan de instrumentación de observability para los próximos 30 días. Especificá qué medir, cómo, en qué orden de prioridad, y qué reportarle al stakeholder al final del mes.
Solución
Plan en 4 sprints semanales:
Semana 1: instrumentación básica de latencia y errores
Goal: dejar de operar a ciegas. Saber lo más básico al final de la semana.
Acciones:
-
Agregar middleware de tracing (OpenTelemetry o solución managed como Datadog) que capture por cada query:
total_latency_ms,embedding_latency_ms,retrieval_latency_ms,generation_latency_mschunks_retrieved,fallback_triggered(bool)error_typesi hubo errortrace_idúnico
-
Dashboard básico con:
- p50/p95/p99 de cada componente
- QPS actual y de las últimas 24h
- Error rate
- Distribución de fallback rate
-
Alertas mínimas:
- p95 total > 1500ms por >5 min → ticket
- Error rate > 5% por >2 min → page
Stakeholder report (fin de semana 1): "Ahora sabemos cuánto tarda el sistema de verdad. p95 actual = 850ms, vs 'promedio 120ms' que veníamos viendo. El cuello de botella es generation (450ms p95), no retrieval (50ms p95). Próximo paso: medir qué tan bien responde, no solo qué tan rápido."
Semana 2: golden set + accuracy baseline
Goal: establecer baseline de calidad para detectar degradación.
Acciones:
- Construir golden set inicial: 50 queries representativas con respuestas anotadas por equipo de producto.
- Pipeline de evaluación automatizado que corre el golden set diariamente:
- Recall@5 (¿se recuperaron los chunks correctos?)
- Answer correctness (LLM-as-judge inicialmente, luego review humano semanal)
- Dashboard de accuracy:
- Trend de accuracy últimos 30 días
- Breakdown por categoría de query
- Alerta:
- Daily accuracy < baseline * 0.95 → ticket para investigación
Stakeholder report (fin de semana 2): "Baseline de accuracy establecido: recall@5 = 87%, answer correctness = 84%. Esto es nuestro punto de comparación de aquí en adelante. Si baja >5%, sabremos que algo cambió."
Semana 3: trazas detalladas y debugging
Goal: poder investigar por qué falló una query específica.
Acciones:
- Trace logging con sampling 10% en producción, 100% en staging:
- Query text (con PII redaction)
- Chunks retrieved con scores
- Prompt completo enviado al LLM
- Respuesta del LLM
- Modelo, temperature, otros parámetros
- Búsqueda de trazas por trace_id (Datadog APM, Honeycomb, etc.).
- Endpoint admin para "investigar query" — ingresás trace_id y te da el flujo completo.
Bonus: muestreo dirigido — guardar 100% de trazas cuando hay error o cuando user rating es negativo.
Stakeholder report (fin de semana 3): "Cuando un usuario reporta una respuesta mala, ahora podemos investigar en <10 minutos en vez de 'no sabemos qué pasó'. Identificamos 3 patrones de error sistemáticos: (a) queries en español devuelven chunks en inglés irrelevantes, (b) queries muy específicas a productos nuevos no tienen documentación, (c) ambigüedad de términos legales causa retrieval pobre."
Semana 4: SLOs, runbooks, métricas de producto
Goal: alinear con producto y formalizar operación.
Acciones:
- Definir SLOs con stakeholders del producto:
- p95 latency < 1000ms
- Error rate < 0.5%
- Daily accuracy >= baseline_accuracy * 0.95
- Runbooks para cada alerta crítica (qué hacer cuando dispara).
- Agregar métricas de producto:
- User feedback (thumbs up/down) por respuesta
- Tasa de queries reformuladas (señal de respuesta inicial mala)
- Retention semanal de usuarios que usaron el bot
- Reporte mensual automatizado para stakeholders.
Stakeholder report final (fin del mes):
"En 30 días pasamos de operar a ciegas a tener observability completa. Resumen:
- Latencia real medida: p95 = 850ms (no 120ms como creíamos). Identificamos generation con OpenAI como cuello de botella; oportunidad de optimización clara. - Accuracy baseline: recall@5 = 87%, answer correctness = 84%. Trend monitoreado diario. - 3 patrones de error identificados que ahora podemos atacar con prioridad. - SLOs definidos y alineados con el producto. Alertas con runbook accionable. - Capacidad de debugging: de 'no sabemos qué pasó' a investigación en <10 min.
Próximos 30 días: atacar los 3 patrones de error identificados (priorizar el de queries en español que afecta el 22% de tráfico) y trabajar en bajar latencia p95 de 850ms a 500ms."
Resumen y siguiente paso
Lo que aprendiste:
- Observability no es agregar logs — es diseñar el sistema para ser diagnosticable desde el día 1.
- Cinco dimensiones a medir en RAG: latencia, throughput, accuracy, costo, health del índice.
- La diferencia entre métricas continuas (cada query) y periódicas (golden set evaluation diaria/semanal).
- Alertas basadas en SLO derivado del producto, no thresholds arbitrarios.
- Cada alerta crítica necesita un runbook con pasos accionables — alerta sin runbook es ruido.
- Goodhart's Law: optimizar lo que medís puede romper lo que no medís — siempre medir las cinco dimensiones, no solo las que parecen importantes.
Checkpoint: antes de avanzar, deberías poder:
- Listar las cinco dimensiones que importan en RAG y dar un ejemplo de métrica concreta de cada una.
- Diferenciar continuous monitoring (latencia, costo) de periodic evaluation (accuracy via golden set).
- Diseñar una alerta con SLO + runbook para latencia p95 fuera de target.
Siguiente cápsula: 08 — Comparación de features y resumen del módulo.
Cerrás el Módulo 3 con una vista panorámica: las features que cubrimos (metadata filtering, hybrid search, multi-tenancy, batch ops, distance metrics, observability) y cómo encajan juntas en un sistema RAG production-ready. Es el resumen que vas a usar como referencia rápida cuando empiéces el hands-on en M4.
Recursos
- Google SRE Book — Service Level Objectives — Concepto de SLO/SLI/SLA
- The Four Golden Signals (Google SRE) — Latency, traffic, errors, saturation
- OpenTelemetry — Vendor-neutral Observability — Estándar emergente para tracing
- Honeycomb — Observability for RAG — Plataforma con buen soporte para tracing distribuido
- Ragas — RAG Evaluation Framework — Para automatizar golden set evaluation
- Goodhart's Law — Wikipedia — La trampa de optimizar lo que medís
Tiempo estimado: 25-30 minutos Siguiente: 08-features-comparison-resumen.md