DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on • Edited on

LLPY-14: Evaluación y Métricas de Calidad - Midiendo el Éxito del RAG

🎯 El Desafío de Medir Calidad en RAG

Imagina que tu sistema RAG está en producción:

  • ✅ API responde en 1-3 segundos
  • ✅ 100+ requests por día
  • ✅ Usuarios parecen contentos

Pero hay preguntas críticas sin respuesta:

  • ¿Las respuestas son correctas?
  • ¿El LLM está inventando información (hallucinations)?
  • ¿Los documentos recuperados son relevantes?
  • ¿Qué % de queries tienen buena calidad?
  • ¿Cómo identificar y fix problemas sistemáticos?

Sin evaluación: Volando a ciegas, optimizando por intuición, descubriendo problemas cuando usuarios se quejan.

📊 La Magnitud del Problema

Dimensiones de Calidad en RAG

Un sistema RAG tiene múltiples puntos de falla:

Query → Embedding → Qdrant Search → Reranking → LLM Generation → Answer

Cada paso puede fallar:
├─ Embedding: Query mal entendido
├─ Search: Documentos irrelevantes recuperados
├─ Reranking: Orden incorrecto
└─ LLM: Hallucination, respuesta incompleta, tono inadecuado
Enter fullscreen mode Exit fullscreen mode

Tipos de Métricas Necesarias

  1. 📊 Retrieval Metrics: ¿Recuperamos los documentos correctos?

    • Precision@k
    • Recall@k
    • MRR (Mean Reciprocal Rank)
    • nDCG (Normalized Discounted Cumulative Gain)
  2. 🤖 Generation Metrics: ¿El LLM genera buenas respuestas?

    • Relevance (pregunta ↔ respuesta)
    • Correctness (contexto ↔ respuesta)
    • Completeness
    • Hallucination rate
    • Grounding (respuesta basada en contexto)
  3. ⚡ System Metrics: ¿El sistema es rápido y confiable?

    • Latency (p50, p95, p99)
    • Throughput (requests/s)
    • Error rate
    • Availability (uptime)
  4. 💰 Cost Metrics: ¿Es sostenible económicamente?

    • Tokens consumidos por query
    • Costo por query
    • Costo mensual proyectado

💡 La Solución: Evaluación Multi-Nivel

Arquitectura de Evaluación

┌───────────────────────────────────────────────────────┐
│              User Query (Production)                  │
└─────────────────┬─────────────────────────────────────┘
                  │
         ┌────────▼────────┐
         │  RAG Pipeline   │
         │  (Fast path)    │
         └────────┬────────┘
                  │
    ┌─────────────┴──────────────┐
    │                            │
Response to User         Enqueue Evaluation
(1-3 seconds)           (Non-blocking, async)
                                 │
                   ┌─────────────▼──────────────┐
                   │   Evaluation Worker        │
                   │   (Background thread)      │
                   └─────────────┬──────────────┘
                                 │
                   ┌─────────────▼──────────────┐
                   │   Phoenix Evals            │
                   │   (LLM-as-a-Judge)        │
                   ├────────────────────────────┤
                   │  - Relevance               │
                   │  - Hallucination           │
                   │  - Toxicity                │
                   │  - Grounding               │
                   └─────────────┬──────────────┘
                                 │
                   ┌─────────────▼──────────────┐
                   │   Phoenix UI               │
                   │   - Dashboards             │
                   │   - Metrics                │
                   │   - Drill-down             │
                   └────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key insight: Evaluación asíncrona no bloquea respuesta al usuario.

🤖 LLM-as-a-Judge Evaluation

EvaluationService

Archivo src/lus_laboris_api/api/services/evaluation_service.py:

"""
Asynchronous evaluation service using Phoenix Evals
"""

import asyncio
import logging
import queue
from concurrent.futures import ThreadPoolExecutor
from typing import Any

from phoenix.evals import (
    HALLUCINATION_PROMPT_TEMPLATE,
    RAG_RELEVANCY_PROMPT_TEMPLATE,
    OpenAIModel,
)

from ..config import settings
from .phoenix_service import phoenix_service

logger = logging.getLogger(__name__)


class EvaluationService:
    """Service for asynchronous RAG response evaluation"""

    def __init__(self):
        self.enabled = settings.api_phoenix_enabled
        self.evaluation_queue = queue.Queue()
        self.executor = ThreadPoolExecutor(
            max_workers=2,
            thread_name_prefix="eval-worker"
        )

        if self.enabled:
            self._initialize_evaluators()
            self._start_evaluation_worker()
            logger.info("Evaluation service initialized")
        else:
            logger.warning("Evaluation service disabled")

    def _initialize_evaluators(self):
        """Initialize Phoenix evaluators with LLM"""
        try:
            # Use cost-effective model for evaluations
            eval_model = "gpt-4o-mini"  # Barato y bueno para evals

            self.eval_model = OpenAIModel(
                model=eval_model,
                api_key=settings.openai_api_key
            )

            logger.info(f"Evaluators initialized with model: {eval_model}")

        except Exception as e:
            logger.exception("Failed to initialize evaluators")
            self.enabled = False

    def _start_evaluation_worker(self):
        """Start worker that processes evaluations in background"""

        def worker():
            logger.info("Evaluation worker started")
            while True:
                try:
                    # Get task from queue
                    eval_task = self.evaluation_queue.get(timeout=1.0)

                    if eval_task is None:  # Shutdown signal
                        break

                    # Execute evaluation
                    self._run_evaluation(eval_task)

                    self.evaluation_queue.task_done()

                except queue.Empty:
                    continue
                except Exception as e:
                    logger.exception("Error in evaluation worker")

        # Start worker in separate thread
        self.executor.submit(worker)

    def enqueue_evaluation(
        self,
        session_id: str,
        question: str,
        context: str,
        answer: str,
        documents: list[dict],
        metadata: dict | None = None,
    ):
        """Enqueue evaluation for async processing (non-blocking)"""
        if not self.enabled:
            return

        eval_task = {
            "session_id": session_id,
            "question": question,
            "context": context,
            "answer": answer,
            "documents": documents,
            "metadata": metadata or {},
            "timestamp": datetime.now().isoformat(),
        }

        self.evaluation_queue.put(eval_task)
        logger.debug(f"Evaluation enqueued for session {session_id}")

    def _run_evaluation(self, eval_task: dict):
        """Execute evaluations (runs async loop)"""
        try:
            # Create new event loop for this thread
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.run_until_complete(self._run_evaluation_async(eval_task))
        finally:
            loop.close()

    async def _run_evaluation_async(self, eval_task: dict):
        """Execute Phoenix Evals in parallel (optimized)"""
        session_id = eval_task["session_id"]
        question = eval_task["question"]
        context = eval_task["context"]
        answer = eval_task["answer"]

        try:
            logger.info(f"Running parallel evaluations for session {session_id}")

            start_time = time.time()

            # ✅ Run all 3 evaluations in parallel
            evaluation_results = await asyncio.gather(
                asyncio.to_thread(
                    self._evaluate_relevance,
                    question, context, answer
                ),
                asyncio.to_thread(
                    self._evaluate_hallucination,
                    question, context, answer
                ),
                asyncio.to_thread(
                    self._evaluate_toxicity,
                    answer
                ),
                return_exceptions=True,  # Don't fail if one fails
            )

            eval_time = time.time() - start_time

            # Extract results
            relevance_score = (
                evaluation_results[0] 
                if not isinstance(evaluation_results[0], Exception) 
                else None
            )
            hallucination_score = (
                evaluation_results[1]
                if not isinstance(evaluation_results[1], Exception)
                else None
            )
            toxicity_score = (
                evaluation_results[2]
                if not isinstance(evaluation_results[2], Exception)
                else None
            )

            # Track results in Phoenix
            phoenix_service.track_evaluation(
                session_id=session_id,
                metrics={
                    "relevance": relevance_score,
                    "hallucination": hallucination_score,
                    "toxicity": toxicity_score,
                    "grounding": 1 - hallucination_score if hallucination_score else None,
                },
                metadata={
                    **eval_task["metadata"],
                    "evaluation_time": eval_time,
                }
            )

            logger.info(
                f"Evaluation completed for session {session_id} in {eval_time:.2f}s"
            )

        except Exception as e:
            logger.exception(f"Evaluation failed for session {session_id}")

    def _evaluate_relevance(self, question: str, context: str, answer: str) -> float:
        """Evaluate if answer is relevant to question"""
        try:
            # Format prompt with Phoenix RAG_RELEVANCY_PROMPT_TEMPLATE
            # Template expects: input, reference
            variable_values = {
                "input": question,     # Spanish question
                "reference": context,   # Spanish context
            }
            prompt = RAG_RELEVANCY_PROMPT_TEMPLATE.format(variable_values)

            # Call model directly (it's callable)
            eval_response = self.eval_model(prompt)

            # Parse response (Phoenix returns "relevant" or "irrelevant")
            if eval_response and "relevant" in eval_response.lower():
                return 1.0  # Highly relevant
            if eval_response and "irrelevant" in eval_response.lower():
                return 0.0  # Not relevant
            return 0.5  # Indeterminate

        except Exception as e:
            logger.exception("Failed to evaluate relevance")
            return None

    def _evaluate_hallucination(self, question: str, context: str, answer: str) -> float:
        """Evaluate if answer contains hallucinations"""
        try:
            # Format prompt with Phoenix HALLUCINATION_PROMPT_TEMPLATE
            # Template expects: input, reference, output
            variable_values = {
                "input": question,   # Spanish question
                "reference": context,  # Spanish context
                "output": answer,    # Spanish answer
            }
            prompt = HALLUCINATION_PROMPT_TEMPLATE.format(variable_values)

            # Call model directly (it's callable)
            eval_response = self.eval_model(prompt)

            # Parse response (Phoenix returns "factual" or "hallucinated")
            if eval_response and "factual" in eval_response.lower():
                return 0.0  # No hallucinations
            if eval_response and ("hallucinated" in eval_response.lower() or 
                                  "hallucination" in eval_response.lower()):
                return 1.0  # High hallucination
            return 0.5  # Indeterminate

        except Exception as e:
            logger.exception("Failed to evaluate hallucination")
            return None

    def _evaluate_toxicity(self, answer: str) -> float:
        """Evaluate if answer contains toxic content"""
        try:
            # Use LLM to evaluate toxicity (Spanish prompt)
            toxicity_prompt = f"""
            Evalúa si la siguiente respuesta contiene contenido tóxico, ofensivo o inapropiado.

            RESPUESTA: {answer}

            Responde únicamente con "tóxico" o "no-tóxico".
            """

            # Call model directly
            eval_response = self.eval_model(toxicity_prompt)

            # Parse response
            if eval_response and ("no-tóxico" in eval_response.lower() or 
                                  "no toxico" in eval_response.lower()):
                return 0.0  # Non-toxic
            if eval_response and ("tóxico" in eval_response.lower() or 
                                  "toxico" in eval_response.lower()):
                return 1.0  # Toxic
            return 0.0  # Default: non-toxic (legal domain)

        except Exception as e:
            logger.exception("Failed to evaluate toxicity")
            return None


# Global service instance
evaluation_service = EvaluationService()
Enter fullscreen mode Exit fullscreen mode

Características del código REAL (simplificado):

  • Async/Non-blocking: No retrasa respuesta al usuario
  • Queue-based: ThreadPoolExecutor procesa en background
  • Parallel evaluations: 3 evals en paralelo con asyncio.gather
  • Phoenix templates: HALLUCINATION_PROMPT_TEMPLATE, RAG_RELEVANCY_PROMPT_TEMPLATE
  • Direct model calls: self.eval_model(prompt) - más simple que run_evals() con pandas
  • Cost-effective: usa gpt-4o-mini ($0.15/1M tokens vs $30/1M GPT-4)
  • Binary scoring: Retorna 1.0 (positivo), 0.0 (negativo), 0.5 (indeterminado)

Evaluaciones Implementadas

1. Relevance (Relevancia):

Question: "¿Cuántos días de vacaciones?"
Answer: "Según el Artículo 218, son 12 días hábiles."

Eval Prompt:
"Is the answer relevant to the question?
 Question: {question}
 Answer: {answer}

 Score 0-1 where 1 = highly relevant"

Score: 0.95 ✅ (muy relevante)
Enter fullscreen mode Exit fullscreen mode

2. Hallucination (Invención):

Context: "Artículo 218: todo trabajador que cumpla un año... 12 días hábiles"
Answer: "Son 12 días hábiles según Artículo 218"

Eval Prompt:
"Does the response contain information NOT in the reference?
 Reference: {context}
 Response: {answer}

 Score 0-1 where 1 = full hallucination"

Score: 0.05 ✅ (casi sin hallucination)
Enter fullscreen mode Exit fullscreen mode

3. Toxicity (Toxicidad):

Answer: "Según el Artículo 218..."

Eval Prompt:
"Does the text contain toxic, offensive, or harmful content?
 Text: {answer}

 Score 0-1 where 1 = very toxic"

Score: 0.0 ✅ (sin toxicidad)
Enter fullscreen mode Exit fullscreen mode

📊 Métricas de Retrieval (Conceptos clave)

Nota: El proyecto actual NO implementa estas métricas automáticamente, pero son conceptos importantes para entender cómo evaluar sistemas RAG.

Métricas Tradicionales de Retrieval

Para evaluar si Qdrant está recuperando los documentos correctos, se usan métricas estándar:

1. Precision@k: ¿Cuántos de los top-k resultados son relevantes?

Precision@k = (Documentos relevantes en top-k) / k

Ejemplo:
Retrieved: [218, 42, 219, 81, 220]  # Top-5
Relevant:  [218, 219, 220]           # Ground truth

Precision@1 = 1/1 = 1.0   # Doc #1 es relevante ✅
Precision@3 = 2/3 = 0.67  # 2 de 3 son relevantes
Precision@5 = 3/5 = 0.60  # 3 de 5 son relevantes
Enter fullscreen mode Exit fullscreen mode

2. Recall@k: ¿Qué % de documentos relevantes recuperamos?

Recall@k = (Documentos relevantes en top-k) / Total relevantes

Ejemplo:
Recall@5 = 3/3 = 1.0  # ✅ Recuperamos TODOS los relevantes
Enter fullscreen mode Exit fullscreen mode

3. MRR (Mean Reciprocal Rank): Posición promedio del primer resultado relevante

MRR = 1 / (Posición del primer relevante)

Query 1: Relevante en #1 → MRR = 1/1 = 1.0
Query 2: Relevante en #3 → MRR = 1/3 = 0.33
Promedio: 0.67
Enter fullscreen mode Exit fullscreen mode

Cómo implementarlas:

  • Crear un ground truth dataset: 50-100 queries con documentos relevantes anotados manualmente
  • Ejecutar queries contra Qdrant
  • Comparar resultados vs ground truth
  • Calcular métricas

📊 Evaluación pre-producción del proyecto: Antes de implementar el sistema en producción, se realizaron evaluaciones exhaustivas documentadas en:

  1. 02_vectorstore_embedding_exploration.ipynb

    • Evaluación de 11 modelos de embeddings
    • Métricas de performance y calidad
    • Justificación del modelo seleccionado (multilingual-e5-small)
  2. 03_rag_pipeline_evaluation.ipynb

    • Ground truth dataset de 50+ queries legales
    • Métricas de retrieval: Precision@k, Recall@k, MRR, nDCG@k, Hit Rate@k
    • Evaluación de reranking: Comparación antes/después con cross-encoder
    • Evaluación LLM: LLM-as-a-Judge para medir calidad de respuestas
    • Análisis comparativo: Múltiples configuraciones del pipeline

Estos notebooks representan la evidencia empírica que respaldó las decisiones técnicas del sistema RAG.

Herramientas recomendadas:

🎯 Integración en el RAG Pipeline

En el RAGService

async def answer_question(self, question: str, session_id: str):
    """Complete RAG pipeline with evaluation"""

    # 1. Retrieve documents
    documents, _ = self._retrieve_documents(question, session_id)

    # 2. Generate answer
    answer = await self._generate_response(question, documents, session_id)

    # 3. Build context
    context = self._build_context(documents)

    # 4. Enqueue evaluation (NON-BLOCKING)
    evaluation_service.enqueue_evaluation(
        session_id=session_id,
        question=question,
        context=context,
        answer=answer,
        documents=documents,
        metadata={
            "llm_provider": self.llm_provider,
            "llm_model": self.llm_model,
            "processing_time": processing_time,
        }
    )

    # 5. Return response immediately (evaluation runs in background)
    return {
        "success": True,
        "answer": answer,
        "session_id": session_id,
        ...
    }
Enter fullscreen mode Exit fullscreen mode

Timeline:

t=0ms:    User request arrives
t=100ms:  Documents retrieved
t=2000ms: LLM generates answer
t=2010ms: Response returned to user ← USER HAPPY
t=2050ms: Evaluation enqueued (async)
t=4500ms: Evaluations complete (background) ← METRICS TRACKED
Enter fullscreen mode Exit fullscreen mode

User experience: 2 seconds (no ve los 2.5s de evaluación)

📊 Visualización en Phoenix UI

Dashboard de Métricas

Acceso: http://localhost:6006 (local) o Phoenix Cloud

Vista de Evaluaciones:

┌─────────────────────────────────────────────────────┐
│ Evaluation Results (Last 7 days)                   │
│                                                     │
│ Total Queries Evaluated: 1,234                     │
│                                                     │
│ Relevance (avg):      0.89 ████████████████░░ 89%  │
│ Hallucination (avg):  0.05 ░░░░░░░░░░░░░░░░░░  5%  │
│ Toxicity (avg):       0.01 ░░░░░░░░░░░░░░░░░░  1%  │
│ Grounding (avg):      0.95 █████████████████░ 95%  │
│                                                     │
│ ┌─────────────────────────────────────────────┐   │
│ │  Relevance Distribution                     │   │
│ │  ┌────────────────────────────────────┐    │   │
│ │  │ [0.0-0.2]:  ▓ 2%                    │    │   │
│ │  │ [0.2-0.4]:  ▓ 3%                    │    │   │
│ │  │ [0.4-0.6]:  ▓▓ 5%                   │    │   │
│ │  │ [0.6-0.8]:  ▓▓▓▓▓ 12%               │    │   │
│ │  │ [0.8-1.0]:  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 78%     │    │   │
│ │  └────────────────────────────────────┘    │   │
│ └─────────────────────────────────────────────┘   │
│                                                     │
│ Top Issues:                                         │
│ - 12 queries with low relevance (<0.6)             │
│ - 6 potential hallucinations detected (>0.3)       │
│ - 0 toxic responses                                │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Drill-Down Individual

┌─────────────────────────────────────────────────────┐
│ Session: abc-123-def                                │
│ Timestamp: 2024-10-16 14:30:22                      │
│                                                     │
│ Question:                                           │
│ "¿Cuántos días de vacaciones corresponden?"        │
│                                                     │
│ Answer:                                             │
│ "Según el Artículo 218 del Código del Trabajo..."  │
│                                                     │
│ Evaluations:                                        │
│ ├─ Relevance:      0.92 ✅ (Highly relevant)       │
│ ├─ Hallucination:  0.03 ✅ (Minimal)               │
│ ├─ Toxicity:       0.00 ✅ (None)                  │
│ └─ Grounding:      0.97 ✅ (Well grounded)         │
│                                                     │
│ Retrieved Documents:                                │
│ 1. Art. 218 (score: 0.912, rerank: 0.987)         │
│ 2. Art. 219 (score: 0.876, rerank: 0.921)         │
│ 3. Art. 220 (score: 0.845, rerank: 0.889)         │
│                                                     │
│ Metadata:                                           │
│ - LLM: OpenAI GPT-3.5-turbo                        │
│ - Processing time: 1.234s                           │
│ - Reranking: Enabled                                │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

🎯 Casos de Uso Reales

Para Identificar Problemas de Calidad:

"Algunas respuestas parecen incorrectas, pero no sé cuáles"

Solución:

  1. Abrir Phoenix UI
  2. Filtrar por relevance < 0.6
  3. Ver queries problemáticas:
    • "¿Qué es un contrato eventual?" → Relevance: 0.4
    • Contexto recuperado: artículos sobre "contratos colectivos"
    • Problema: Embeddings confunden "eventual" con "colectivo"

Fix: Mejorar pre-processing de queries o usar modelo de embeddings más grande

Para Detectar Hallucinations:

"¿El LLM está inventando información?"

Solución:

  1. Phoenix UI → Filter hallucination > 0.3
  2. Ver casos:
    • Query: "¿Cuál es el salario mínimo?"
    • Context: (vacío - no hay artículos sobre salario)
    • Answer: "El salario mínimo es $2,500,000..." ⚠️
    • Hallucination score: 0.9

Fix: Agregar guardrail: Si no hay contexto relevante → "No tengo información suficiente"

Para Optimizar Performance:

"¿Qué configuración da mejores resultados?"

Solución: A/B Testing

# Configuración A: Sin reranking
results_a = run_eval_suite(use_reranking=False)
# Relevance: 0.85

# Configuración B: Con reranking
results_b = run_eval_suite(use_reranking=True)
# Relevance: 0.92 (+7%)

# Conclusión: Reranking vale la pena!
Enter fullscreen mode Exit fullscreen mode

Para Monitoreo Continuo:

"Quiero alertas cuando calidad baja"

Solución:

# En Phoenix o monitoring system
if avg_relevance_last_24h < 0.80:
    send_alert("⚠️ RAG quality degraded! Avg relevance: 0.78")

if hallucination_rate_last_24h > 0.10:
    send_alert("🚨 High hallucination rate: 12%")
Enter fullscreen mode Exit fullscreen mode

🚀 El Impacto Transformador

Antes de Evaluación Automática:

  • 🤷 Quality unknown: "Parece que funciona bien..."
  • 🐛 Reactive debugging: Usuarios reportan problemas
  • 📊 No metrics: Sin datos para optimizar
  • 🎲 Blind optimization: Cambios sin medir impacto
  • ⏱️ Manual testing: Probar 10-20 queries manualmente

Después de Evaluación Automática:

  • 📊 Quality measured: "89% relevance, 5% hallucination"
  • 🔍 Proactive debugging: Identificar problemas antes que usuarios
  • 📈 Data-driven: Optimizar basado en métricas reales
  • 🎯 A/B testing: Comparar configuraciones objetivamente
  • Continuous evaluation: 100% de queries evaluadas automáticamente

Métricas de Mejora:

Aspecto Sin Evaluación Con Evaluación Mejora
Quality visibility 0% 100% +∞
Problem detection Días (user reports) Minutos (automated) -99%
Evaluation coverage 1-5% (manual) 100% (automated) +2000%
Optimization confidence Baja (guessing) Alta (data-driven) N/A
Time to fix issues Días Horas -90%

💡 Lecciones Aprendidas

1. Async Evaluation es Crítico

Evaluación no debe agregar latencia a la respuesta del usuario. Queue + ThreadPoolExecutor = perfecto.

2. gpt-4o-mini es Ideal para Evals

  • GPT-4: Muy caro para evaluar cada query ($30/1M tokens)
  • GPT-3.5-turbo: Calidad inconsistente
  • gpt-4o-mini: Balance perfecto ($0.15/1M tokens, buena calidad)

3. Phoenix Templates > Custom

Los templates de Phoenix (HALLUCINATION, RAG_RELEVANCY) están battle-tested. Úsalos.

4. Parallel Evaluations = 3x Faster

asyncio.gather ejecuta 3 evals en paralelo → 2.5s en lugar de 7.5s

5. Ground Truth es Gold

Tener un dataset de 50-100 query-answer pairs validados permite comparaciones objetivas.

6. Monitorea Tendencias, No Puntos

Un query malo no importa. 10% de queries malos sí importa. Phoenix dashboards muestran trends.

🎯 El Propósito Más Grande

Evaluación y métricas no son "nice to have" - son el sistema de calidad que garantiza que el RAG entrega valor real. Sin evaluación:

  • No sabes si está funcionando
  • No puedes optimizar
  • No detectas regresiones
  • No justificas inversión

Con evaluación automática continua:

  • Visibility: Calidad medida en cada query
  • Confidence: Cambios basados en datos
  • Quality assurance: Detección temprana de problemas
  • Continuous improvement: Optimización basada en métricas
  • Stakeholder reporting: Dashboards para el negocio
  • Cost optimization: Identificar oportunidades de ahorro

Estamos convirtiendo el RAG de una "caja negra" a un sistema medido, monitoreado y optimizable, donde cada respuesta contribuye a mejorar el siguiente millón de respuestas.


🔗 Recursos y Enlaces

Repositorio del Proyecto

Documentación Técnica

Recursos Externos


🎊 Fin de la Serie LLPY

Hemos completado un viaje de 14 posts construyendo un sistema RAG de clase mundial:

Serie Completa:

  1. LLPY-01: Introducción al Sistema RAG
  2. LLPY-02: Configuración con UV
  3. LLPY-03: Extracción de Datos Legales
  4. LLPY-04: Vectorización y Embeddings
  5. LLPY-05: Qdrant Base de Datos Vectorial
  6. LLPY-06: FastAPI API REST Robusta
  7. LLPY-07: Integración LLMs (OpenAI + Gemini)
  8. LLPY-08: Reranking para Precisión
  9. LLPY-09: Phoenix y OpenTelemetry
  10. LLPY-10: Autenticación JWT con RSA
  11. LLPY-11: Terraform - Infraestructura como Código
  12. LLPY-12: Docker y Containerización
  13. LLPY-13: CI/CD con GitHub Actions
  14. LLPY-14: Evaluación y Métricas de Calidad ← ESTE POST

Lo que Construimos:

✅ Sistema RAG completo end-to-end

✅ 413 artículos legales vectorizados

✅ API REST producción-ready

✅ Multi-provider LLM (OpenAI + Gemini)

✅ Observabilidad completa (Phoenix + OpenTelemetry)

✅ Infraestructura como código (Terraform)

✅ CI/CD automatizado (GitHub Actions)

✅ Evaluación continua (LLM-as-a-judge)

Métricas del Sistema (Observables con Phoenix):

Métrica Objetivo Medición
Latency (p50) <2s ✅ Phoenix tracking
Latency (p95) <5s ✅ Phoenix tracking
Relevance (avg) >0.8 ✅ LLM-as-a-Judge
Hallucination (avg) <0.1 ✅ LLM-as-a-Judge
Toxicity (avg) <0.05 ✅ LLM-as-a-Judge
Overall Quality >0.85 ✅ Weighted average
Cost per query <$0.01 ✅ Token tracking

Nota: Las métricas se recopilan automáticamente en Phoenix con cada query evaluada. Los valores "Objetivo" representan benchmarks típicos de RAG de producción, no mediciones actuales.

Impacto Transformador Final:

De 0 a sistema RAG de producción en 14 posts:

  • 🚀 Performance: Retrieval optimizado (gRPC), generación async
  • 🎯 Quality: LLM-as-a-Judge evaluando relevance, hallucination, toxicity
  • 💰 Cost: gpt-4o-mini para evals ($0.15/1M tokens), escalable
  • 🔒 Security: JWT RSA auth, GCP Secret Manager
  • 📊 Observability: Phoenix + OpenTelemetry tracking completo
  • 🔄 Deployment: CI/CD automático con GitHub Actions
  • 📈 Evaluation: 100% de queries evaluadas asíncronamente (non-blocking)

Gracias por acompañarnos en este viaje! 🎉

Si tienes preguntas o quieres contribuir al proyecto, visita el repositorio en GitHub.

Top comments (0)