🎯 El Desafío de Debugging en Sistemas RAG
Imagina que tu sistema RAG está en producción:
- ✅ API recibe 100+ req/s
- ✅ Pipeline complejo: Embedding → Qdrant → Reranking → LLM
- ✅ Múltiples servicios integrados
Pero hay un problema: ¿Cómo debuggeas cuando algo sale mal?
Preguntas sin Respuesta
- Performance: "¿Por qué esta query tardó 5 segundos cuando debería tomar 2?"
- Quality: "¿Por qué el LLM generó una respuesta incorrecta?"
- Errors: "¿Dónde falló exactamente en el pipeline?"
- Cost: "¿Cuántos tokens estoy consumiendo por día?"
- User Experience: "¿Qué % de usuarios obtienen respuestas relevantes?"
Sin observabilidad: debugging a ciegas con print statements y logs dispersos.
La Complejidad del RAG Pipeline
User Query: "¿Cuántos días de vacaciones?"
↓
├─ [1] Embedding generation (30ms) ✅
│ └─ Model: multilingual-e5-small, Dimensions: 384
↓
├─ [2] Qdrant search (35ms) ✅
│ └─ Found 10 documents, scores: 0.85-0.91
↓
├─ [3] Reranking (20ms) ✅
│ └─ Model: ms-marco-MiniLM, Top-5 kept
↓
├─ [4] LLM generation (1800ms) ⚠️ SLOW!
│ └─ OpenAI GPT-3.5-turbo, 1500 tokens
↓
└─ [5] Evaluation (async, 2500ms)
└─ Relevance: 0.92, Hallucination: 0.05
TOTAL: 1885ms + eval async
¿Cómo sabes si cada paso funcionó correctamente?
📊 La Magnitud del Problema
Requisitos de Observabilidad para RAG
- 📊 Tracing: Visualizar cada paso del pipeline end-to-end
- 🔍 Instrumentation: Auto-track llamadas a LLM APIs (OpenAI, Gemini)
- 📈 Metrics: Latency, throughput, error rates por servicio
- 🎯 Evaluations: Calidad automática de respuestas (LLM-as-judge)
- 🔗 Context Propagation: Correlacionar logs/spans de una misma request
- 💰 Cost Tracking: Tokens consumidos, costo por query
- 🐛 Error Tracking: Stack traces con contexto completo
- 📊 Dashboards: Visualización en tiempo real
Desafíos Técnicos
- 🕸️ Distributed Tracing: Correlacionar operaciones asíncronas
- 🔄 Async Evaluation: No bloquear respuesta del usuario
- 📦 Complex Payloads: Serializar metadatos complejos para OTel
- ⚡ Low Overhead: Tracing no debe agregar >10ms de latency
- 🎯 Sampling: En producción, no trackear todo (costo + overhead)
💡 La Solución: Phoenix + OpenTelemetry
¿Qué es Phoenix?
Phoenix (por Arize AI) es una plataforma de observabilidad especializada en LLMs y RAG:
- 🎯 Purpose-built: Diseñado específicamente para aplicaciones LLM
- 📊 Tracing: Visualización de pipelines RAG completos
- 🤖 LLM-as-Judge: Evaluación automática de respuestas
- 📈 Analytics: Métricas de calidad, costo, performance
- 🔍 Debugging: Drill-down en queries problemáticas
- 💰 Open Source: Self-hosted o cloud (Phoenix Cloud)
¿Qué es OpenTelemetry?
OpenTelemetry es el estándar de observabilidad open source:
- 📏 Specification: Define cómo instrumentar aplicaciones
- 📦 SDKs: Librerías para Python, JS, Go, Java, etc.
- 🔌 Exporters: Envía datos a backends (Phoenix, Datadog, New Relic, etc.)
- 🎯 Vendor-neutral: No lock-in a un provider específico
Arquitectura Phoenix + OpenTelemetry
┌─────────────────────────────────────────────────────────┐
│ Lus Laboris API │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ PhoenixMonitoringService │ │
│ │ - Tracer Provider (OpenTelemetry) │ │
│ │ - Instrumentors (OpenAI auto-instrumented) │ │
│ │ - Custom Spans (manual tracking) │ │
│ └───────────────┬──────────────────────────────┘ │
│ │ │
│ OpenTelemetry SDK │
│ │ │
└──────────────────┼──────────────────────────────────────┘
│
│ gRPC (4317) o HTTP (6006)
│
┌───────▼────────┐
│ Phoenix │
│ Collector │
│ │
│ - Receives │
│ traces │
│ - Stores │
│ spans │
│ - Runs │
│ evals │
└───────┬────────┘
│
┌───────▼────────┐
│ Phoenix UI │
│ (Port 6006) │
│ │
│ - Traces │
│ - Metrics │
│ - Evals │
│ - Analytics │
└────────────────┘
🚀 Configuración Paso a Paso
1. Variables de Entorno
# .env
# Phoenix Configuration
API_PHOENIX_ENABLED=true
API_PHOENIX_ENDPOINT=http://localhost:6006 # Phoenix HTTP
API_PHOENIX_GRPC_ENDPOINT=localhost:4317 # Phoenix gRPC (más rápido)
API_PHOENIX_USE_GRPC=true # Preferir gRPC
API_PHOENIX_API_KEY= # Vacío para local, requerido para cloud
API_PHOENIX_PROJECT_NAME=lus-laboris-api
# Environment
API_ENVIRONMENT=development # Options: development, production, testing
Configuraciones por ambiente:
Variable | Development | Production |
---|---|---|
API_PHOENIX_ENABLED |
true |
true |
API_PHOENIX_ENDPOINT |
http://localhost:6006 |
https://app.phoenix.arize.com |
API_PHOENIX_USE_GRPC |
true |
true |
API_PHOENIX_API_KEY |
(vacío) |
phx_... (cloud API key) |
API_ENVIRONMENT |
development |
production |
2. Docker Compose con Phoenix
# src/lus_laboris_api/docker-compose.yml
services:
# Qdrant vector database
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_storage:/qdrant/storage
networks:
- api-network
# Phoenix observability
phoenix:
image: arizephoenix/phoenix:latest
container_name: phoenix
ports:
- "6006:6006" # HTTP UI
- "4317:4317" # gRPC collector
environment:
- PHOENIX_PORT=6006
- PHOENIX_GRPC_PORT=4317
networks:
- api-network
# Lus Laboris API
api:
build: .
ports:
- "8000:8000"
environment:
- API_PHOENIX_ENDPOINT=http://phoenix:6006
- API_PHOENIX_GRPC_ENDPOINT=phoenix:4317
- API_PHOENIX_USE_GRPC=true
- API_QDRANT_URL=http://qdrant:6333
depends_on:
- qdrant
- phoenix
networks:
- api-network
volumes:
qdrant_storage:
networks:
api-network:
driver: bridge
Iniciar stack completo:
cd src/lus_laboris_api
docker-compose up -d
# Verificar que Phoenix está up
curl http://localhost:6006/healthz
# Acceder a Phoenix UI
open http://localhost:6006
3. Settings con Pydantic
# src/lus_laboris_api/api/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application settings"""
# Phoenix Monitoring Configuration
api_phoenix_enabled: bool = True
api_phoenix_endpoint: str | None = None
api_phoenix_grpc_endpoint: str | None = "localhost:4317"
api_phoenix_use_grpc: bool = True
api_phoenix_api_key: str | None = None
api_phoenix_project_name: str = "lus-laboris-api"
# Environment Configuration
api_environment: str = "development" # development, production, testing
class Config:
env_file = ".env"
case_sensitive = False
settings = Settings()
4. PhoenixMonitoringService
El servicio se inicializa al arrancar la aplicación y configura el stack completo de observabilidad:
¿Qué hace PhoenixMonitoringService
?
-
Lee configuración de
settings
- Project name, endpoints (gRPC/HTTP), API keys
- Detecta ambiente (development vs production)
-
Conecta a Phoenix con estrategia inteligente
- Prioridad 1: gRPC (2-3x más rápido que HTTP)
- Fallback: HTTP si gRPC no está disponible
- Graceful degradation: Si falla, API sigue funcionando
-
Registra tracer provider con
phoenix.otel.register()
-
project_name
: Para separar proyectos en Phoenix UI -
auto_instrument=True
: Tracking automático de librerías -
batch=True
en producción: Agrupa spans para eficiencia
-
-
Auto-instrumenta OpenAI con
OpenInference
- OpenAI: Auto-tracked (tokens, latency, errors)
- Gemini: Manual tracking (no hay instrumentor aún)
Crea tracer para tracking manual con OpenTelemetry
Configuración según ambiente:
Aspecto | Development | Production |
---|---|---|
Span Processor | SimpleSpanProcessor (inmediato) | BatchSpanProcessor (agrupa 512 spans) |
Transport | gRPC o HTTP | gRPC preferido (performance) |
Batch |
false (debugging) |
true (eficiencia) |
Beneficios clave:
- ✅ gRPC: <2ms overhead vs 5-10ms con HTTP
- ✅ Auto-instrumentation: OpenAI tracked sin código extra
- ✅ Graceful: Si Phoenix falla, API no se rompe
- ✅ Environment-aware: Optimizado por ambiente
🔍 Tracking del Pipeline RAG
1. Session Management
def create_session(self, user_id: str | None = None) -> str:
"""Create a monitoring session for a user request"""
session_id = str(uuid.uuid4())
self.session_tracker[session_id] = {
"session_id": session_id,
"user_id": user_id,
"start_time": datetime.now(),
"actions": [],
"llm_calls": [],
"metrics": {},
}
logger.info(f"Created monitoring session: {session_id}")
return session_id
def end_session(self, session_id: str) -> dict[str, Any]:
"""End session and calculate final metrics"""
if session_id not in self.session_tracker:
return {}
session_data = self.session_tracker[session_id]
session_data["end_time"] = datetime.now()
session_data["duration"] = (
session_data["end_time"] - session_data["start_time"]
).total_seconds()
# Calculate final metrics
session_metrics = self._calculate_session_metrics(session_data)
session_data["final_metrics"] = session_metrics
logger.info(f"Session {session_id} ended. Duration: {session_data['duration']:.2f}s")
# Cleanup
del self.session_tracker[session_id]
return session_data
2. Track Embedding Generation
Este método registra el paso de generación de embeddings en el pipeline:
¿Qué registra?
- Modelo usado (
multilingual-e5-small
) - Longitud del texto de entrada
- Tiempo de generación (típicamente ~30ms)
- Session ID para correlación
Cómo funciona:
- Crea un span de OpenTelemetry llamado
"embedding_generation"
- Añade attributes (modelo, tiempo, longitud de texto)
- Registra en session tracker local
- Marca como
Status.OK
si todo sale bien
Visible en Phoenix UI: Como un span dentro del trace completo del request.
3. Track Vectorstore Search
Registra la búsqueda en Qdrant (vectorstore):
¿Qué registra?
- Longitud de la query
- Cantidad de documentos retornados (típicamente 5-10)
- Tiempo de búsqueda (~30-35ms)
Visible en Phoenix: Span "vectorstore_search"
que muestra performance de Qdrant.
4. Track Reranking
Registra el paso de reranking con cross-encoder:
¿Qué registra?
- Cantidad de documentos rerankeados (10 → 5)
- Tiempo de reranking (~20ms)
Visible en Phoenix: Span "document_reranking"
que muestra overhead del reranking.
5. Track LLM Call
Registra llamadas a LLMs (OpenAI o Gemini):
¿Qué registra?
- Provider y modelo (
openai/gpt-3.5-turbo
ogemini/gemini-1.5-flash
) - Longitud de prompt y respuesta
- Tokens consumidos (para cálculo de costos)
- Métricas básicas de calidad (coherence, relevance, completeness)
Auto-instrumentation:
- OpenAI: Tracked automáticamente por OpenInference (tokens, latency, errors)
- Gemini: Tracking manual porque no hay instrumentor aún
Visible en Phoenix: Span "llm_call_openai_gpt-3.5-turbo"
que domina el tiempo total (~1.8s de 2s total).
🎯 Integración con RAG Pipeline
En el RAGService
# src/lus_laboris_api/api/services/rag_service.py
from .phoenix_service import phoenix_service
async def answer_question(self, question: str, session_id: str | None = None):
"""Complete RAG pipeline with Phoenix tracking"""
start_time = time.time()
# 1. Create session
if not session_id:
session_id = phoenix_service.create_session()
try:
# 2. Retrieve documents (tracked internally)
documents, retrieval_metadata = self._retrieve_documents(question, session_id)
# 3. Generate response (tracked internally)
answer = await self._generate_response(question, documents, session_id)
# 4. Calculate total time
processing_time = time.time() - start_time
# 5. Enqueue evaluation (async, non-blocking)
context = self._build_context(documents)
evaluation_service.enqueue_evaluation(
session_id=session_id,
question=question,
context=context,
answer=answer,
documents=documents,
metadata={
"processing_time": processing_time,
"llm_provider": self.llm_provider,
"llm_model": self.llm_model,
}
)
# 6. Return response
return {
"success": True,
"question": question,
"answer": answer,
"processing_time_seconds": round(processing_time, 3),
"session_id": session_id,
...
}
except Exception as e:
logger.exception(f"[{session_id}] Failed to answer question")
return {
"success": False,
"error": str(e),
"session_id": session_id,
}
finally:
# End session
phoenix_service.end_session(session_id)
Flujo completo:
- Create session
- Track embedding → Track Qdrant search → Track reranking → Track LLM
- Enqueue async evaluation
- Return response to user
- End session (calculates metrics)
📊 Visualización en Phoenix UI
1. Acceder a Phoenix
# Local
open http://localhost:6006
# Cloud
open https://app.phoenix.arize.com
2. Traces View
Muestra todos los requests con breakdown detallado:
┌────────────────────────────────────────────────────────┐
│ Trace: session_abc123 (1.8s) │
│ │
│ ├─ embedding_generation (30ms) │
│ │ ├─ model: multilingual-e5-small │
│ │ └─ text_length: 45 │
│ │
│ ├─ vectorstore_search (35ms) │
│ │ ├─ results_count: 10 │
│ │ └─ search_time: 0.035s │
│ │
│ ├─ document_reranking (20ms) │
│ │ ├─ documents_count: 10 │
│ │ └─ reranking_time: 0.020s │
│ │
│ └─ llm_call_openai_gpt-3.5-turbo (1.7s) │
│ ├─ provider: openai │
│ ├─ model: gpt-3.5-turbo │
│ ├─ prompt_length: 1450 │
│ ├─ response_length: 320 │
│ ├─ quality.coherence: 0.85 │
│ ├─ quality.relevance: 0.92 │
│ └─ quality.completeness: 0.78 │
│ │
│ TOTAL: 1.815s │
└────────────────────────────────────────────────────────┘
3. LLM Calls Dashboard
┌─────────────────────────────────────────────────────┐
│ LLM Calls Summary (Last 24h) │
│ │
│ Total Calls: 1,234 │
│ OpenAI: 856 (69%) │
│ Gemini: 378 (31%) │
│ │
│ Avg Latency: 1.8s │
│ p50: 1.5s | p95: 2.8s | p99: 4.2s │
│ │
│ Total Tokens: 2.3M │
│ Input: 1.5M | Output: 800K │
│ │
│ Estimated Cost: $3.45 │
│ OpenAI: $2.80 | Gemini: $0.65 │
└─────────────────────────────────────────────────────┘
4. Evaluations View
┌─────────────────────────────────────────────────────┐
│ Evaluation Results (Last 100 queries) │
│ │
│ Relevance (avg): 0.89 ████████████████░░ 89% │
│ Correctness (avg): 0.85 ██████████████░░░░ 85% │
│ Completeness (avg): 0.78 █████████████░░░░░ 78% │
│ Hallucination rate: 5% ░░░░░░░░░░░░░░░░░░ 5% │
│ Grounding (avg): 0.92 ████████████████▓░ 92% │
│ │
│ Top Issues: │
│ - 8 queries with low completeness (<0.6) │
│ - 3 potential hallucinations detected │
│ - 2 queries with poor grounding (<0.7) │
└─────────────────────────────────────────────────────┘
🤖 LLM-as-a-Judge Evaluations
Evaluation Service: Calidad Automática
El EvaluationService
evalúa la calidad de cada respuesta usando un LLM como juez:
¿Cómo funciona?
-
Enqueue evaluation (no bloqueante)
- Usuario recibe respuesta inmediatamente
- Evaluación se ejecuta en background thread
-
Evalúa múltiples dimensiones con prompts específicos:
- Relevance: ¿La respuesta es relevante a la pregunta? (0-1)
- Correctness: ¿Es factualmente correcta según el contexto? (0-1)
- Hallucination: ¿Contiene info no presente en el contexto? (0-1)
- Grounding: Inverso de hallucination (1 - hallucination)
-
Envía resultados a Phoenix con
phoenix_service.track_evaluation()
- Métricas visibles en Phoenix UI
- Historial completo para análisis de tendencias
Métricas típicas:
- Relevance: ~0.89 (89%)
- Correctness: ~0.85 (85%)
- Hallucination: ~0.05 (5%)
- Grounding: ~0.92 (92%)
Ventajas:
- ✅ Non-blocking: No impacta latencia del usuario
- ✅ Automated: Evalúa 100% de respuestas sin intervención humana
- ✅ Continuous: Detecta degradación de calidad en tiempo real
- ✅ Actionable: Identifica queries problemáticas automáticamente
🎯 Casos de Uso Reales
Para Debugging de Latency:
"¿Por qué esta query tardó 5 segundos?"
Phoenix UI:
- Abrir Phoenix → Traces
- Filtrar por
processing_time > 5s
- Ver breakdown:
- Embedding: 30ms ✅
- Qdrant: 35ms ✅
- Reranking: 20ms ✅
- LLM: 4.8s ⚠️ CULPABLE
Acción: LLM es el cuello de botella. Opciones:
- Cambiar a Gemini 1.5 Flash (más rápido)
- Reducir max_tokens
- Implementar caching de respuestas
Para Debugging de Calidad:
"¿Por qué el LLM generó una respuesta incorrecta?"
Phoenix UI:
- Abrir Phoenix → Evaluations
- Filtrar por
correctness < 0.6
- Ver query específica:
- Question: "¿Cuántos días de preaviso?"
- Context: [5 documentos irrelevantes]
- Answer: "15 días" (incorrecto)
- Correctness: 0.4
Acción: El problema es retrieval, no LLM. Opciones:
- Mejorar query embedding
- Ajustar top_k o reranking
- Revisar metadatos de documentos
Para Cost Optimization:
"¿Cuánto estoy gastando en OpenAI?"
Phoenix UI:
- Abrir Phoenix → LLM Calls
- Ver dashboard:
- Total tokens (24h): 2.3M
- GPT-3.5-turbo: $2.80/día
- GPT-4: $12.50/día (70 calls)
Acción: GPT-4 es muy costoso. Opciones:
- Usar GPT-4 solo para queries complejas
- Implementar clasificador (simple → GPT-3.5, complex → GPT-4)
- Migrar a Gemini 1.5 Pro (60% más barato)
Para Quality Monitoring:
"¿Qué % de respuestas tienen hallucinations?"
Phoenix UI:
- Abrir Phoenix → Evaluations
- Ver dashboard:
- Hallucination rate: 5% (61 de 1,234 queries)
Acción: 5% es aceptable, pero revisar casos:
- 3 queries con hallucination score >0.8
- Común cuando context es irrelevante
- Agregar guardrail: "No tengo información suficiente"
🚀 El Impacto Transformador
Antes de Phoenix:
- 🐛 Debugging: print() statements en 10 archivos diferentes
- ⏱️ Performance: "Parece lento, pero no sé por qué"
- 💰 Cost: "Creo que gasto $50/mes, pero no estoy seguro"
- 🎯 Quality: "Los usuarios dicen que a veces está mal, pero no sé cuándo"
- 📊 Metrics: CSV files con logs dispersos
Después de Phoenix:
- 🐛 Debugging: Ver trace completo en Phoenix UI en 10 segundos
- ⏱️ Performance: "LLM tarda 1.8s avg, p99 es 4.2s"
- 💰 Cost: "$3.45/día, OpenAI usa $2.80, Gemini $0.65"
- 🎯 Quality: "89% relevance, 5% hallucination rate"
- 📊 Metrics: Dashboard en tiempo real con drill-down completo
Métricas de Mejora:
Aspecto | Sin Phoenix | Con Phoenix | Mejora |
---|---|---|---|
MTTR (Mean Time To Resolution) | 2-4 horas | 10-30 minutos | -85% |
Quality Detection | Manual sampling (10%) | Automated (100%) | +900% |
Cost Visibility | Weekly estimates | Real-time tracking | N/A |
Performance Insights | Guessing | Precise breakdown | N/A |
💡 Lecciones Aprendidas
1. gRPC es 2-3x Más Rápido que HTTP
Phoenix con gRPC tiene latency de <2ms para enviar spans. HTTP puede agregar 5-10ms.
2. Batch Processing en Producción es Crítico
En dev, usar SimpleSpanProcessor
(inmediato). En prod, usar BatchSpanProcessor
(agrupa 512 spans antes de enviar).
3. Session IDs son el Glue
Correlacionar todos los spans de una request con un session_id único permite drill-down completo en Phoenix.
4. Async Evaluation no Bloquea
Evaluar calidad en background (evaluationservice) no impacta latencia de respuesta al usuario.
5. Auto-Instrumentation > Manual
OpenInference auto-instrumenta OpenAI (tracks tokens, latency, errors). Gemini requiere tracking manual porque no tiene instrumentor aún.
6. Serialize Complex Metadata
OpenTelemetry solo acepta primitives (str, int, float, bool). Dicts/lists deben serializarse a JSON string.
🎯 El Propósito Más Grande
Phoenix + OpenTelemetry no es solo "nice to have" - es el sistema nervioso del RAG pipeline. Sin observabilidad:
- Debugging es adivinanza
- Performance es misterio
- Quality es anécdota
- Cost es estimación
Con Phoenix + OpenTelemetry:
- ✅ Visibility: Ver cada paso del pipeline en tiempo real
- ✅ Debuggability: Drill-down en queries problemáticas en segundos
- ✅ Quality: Evaluación automática de 100% de respuestas
- ✅ Optimization: Identificar cuellos de botella con precisión
- ✅ Cost Control: Track tokens y costos en tiempo real
- ✅ Continuous Improvement: Métricas históricas para optimización
Estamos convirtiendo un sistema RAG de "caja negra" a cristal transparente, donde cada operación es visible, medible, y optimizable.
🔗 Recursos y Enlaces
Repositorio del Proyecto
- GitHub: lus-laboris-py
Documentación Técnica
-
Phoenix Service:
src/lus_laboris_api/api/services/phoenix_service.py
-
Evaluation Service:
src/lus_laboris_api/api/services/evaluation_service.py
-
RAG Service:
src/lus_laboris_api/api/services/rag_service.py
Recursos Externos
- Phoenix Docs: docs.arize.com/phoenix
- OpenTelemetry Python: opentelemetry.io/docs/languages/python
- OpenInference: github.com/Arize-ai/openinference
- Phoenix Cloud: app.phoenix.arize.com
Próximo Post: LLPY-10 - Autenticación JWT con RSA
En el siguiente post exploraremos cómo implementar autenticación segura con JWT y claves RSA, generación de tokens, validación en endpoints, y integración con GCP Secret Manager.
Top comments (0)