Una implementación de codificación para automatizar el control de calidad de LLM con DeepEval, recuperadores personalizados y métricas de LLM como juez

Iniciamos este tutorial configurando un entorno de evaluación de alto rendimiento, específicamente enfocado en integrar el marco DeepEval para brindar rigor de pruebas unitarias a nuestras aplicaciones LLM. Al cerrar la brecha entre la recuperación sin procesar y la generación final, implementamos un sistema que trata los resultados del modelo como código comprobable y utiliza métricas de LLM como juez para cuantificar el rendimiento. Vamos más allá de la inspección manual al construir un proceso estructurado en el que cada consulta, contexto recuperado y respuesta generada se valida con métricas rigurosas de estándares académicos. Consulta los CÓDIGOS COMPLETOS aquí.

import sys, os, textwrap, json, math, re from getpass import getpass print(“🔧 Entorno de refuerzo (evita la corrupción numpy común de Colab/py3.12)…”) !pip -q uninstall -y numpy || verdadero !pip -q install –no-cache-dir –force-reinstall “numpy==1.26.4” !pip -q install -U deepeval openai scikit-learn pandas tqdm print(“✅ Paquetes instalados.”) importar numpy como np importar pandas como pd desde tqdm.auto importar tqdm desde sklearn.feature_extraction.text importar TfidfVectorizer de sklearn.metrics.pairwise importar cosine_similarity de deepeval importar evaluar de deepeval.test_case importar LLMTestCase, LLMTestCaseParams de deepeval.metrics importar (AnswerRelevancyMetric, FaithfulnessMetric, ContextualRelevancyMetric, ContextualPrecisionMetric, ContextualRecallMetric, GEval,) print(“✅ Importaciones cargadas exitosamente.”) OPENAI_API_KEY = getpass(“🔑 Ingrese OPENAI_API_KEY (déjelo vacío para ejecutar sin OpenAI): “).strip() openai_enabled = bool(OPENAI_API_KEY) si openai_enabled: os.environ[“OPENAI_API_KEY”] = OPENAI_API_KEY print(f”🔌 OpenAI habilitado: {openai_enabled}”)

Inicializamos nuestro entorno estabilizando las dependencias centrales e instalando el marco de evaluación profunda para garantizar un proceso de pruebas sólido. A continuación, importamos métricas especializadas como fidelidad y recuerdo contextual mientras configuramos nuestras credenciales API para permitir una evaluación automatizada y de alta fidelidad de nuestras respuestas de LLM. Consulta los CÓDIGOS COMPLETOS aquí.

DOCUMENTOS = [
{
“id”: “doc_01”,
“title”: “DeepEval Overview”,
“text”: (
“DeepEval is an open-source LLM evaluation framework for unit testing LLM apps. ”
“It supports LLM-as-a-judge metrics, custom metrics like G-Eval, and RAG metrics ”
“such as contextual precision and faithfulness.”
),
},
{
“id”: “doc_02”,
“title”: “RAG Evaluation: Why Faithfulness Matters”,
“text”: (
“Faithfulness checks whether the answer is supported by retrieved context. ”
“In RAG, hallucinations occur when the model states claims not grounded in context.”
),
},
{
“id”: “doc_03”,
“title”: “Contextual Precision”,
“text”: (
“Contextual precision evaluates how well retrieved chunks are ranked by relevance ”
“to a query. High precision means relevant chunks appear earlier in the ranked list.”
),
},
{
“id”: “doc_04”,
“title”: “Contextual Recall”,
“text”: (
“Contextual recall measures whether the retriever returns enough relevant context ”
“to answer the query. Low recall means key information was missed in retrieval.”
),
},
{
“id”: “doc_05”,
“title”: “Answer Relevancy”,
“text”: (
“Answer relevancy measures whether the generated answer addresses the user’s query. ”
“Even grounded answers can be irrelevant if they don’t respond to the question.”
),
},
{
“id”: “doc_06”,
“title”: “G-Eval (GEval) Custom Rubrics”,
“text”: (
“G-Eval lets you define evaluation criteria in natural language. ”
“It uses an LLM judge to score outputs against your rubric (e.g., correctness, tone, policy).”
),
},
{
“id”: “doc_07”,
“title”: “What a DeepEval Test Case Contains”,
“text”: (
“A test case typically includes input (query), actual_output (model answer), ”
“expected_output (gold answer), and retrieval_context (ranked retrieved passages) for RAG.”
),
},
{
“id”: “doc_08”,
“title”: “Common Pitfall: Missing expected_output”,
“text”: (
“Some RAG metrics require expected_output in addition to input and retrieval_context. ”
“If expected_output is None, evaluation fails for metrics like contextual precision/recall.”
),
},
]

EVAL_QUERIES = [
{
“query”: “What is DeepEval used for?”,
“expected”: “DeepEval is used to evaluate and unit test LLM applications using metrics like LLM-as-a-judge, G-Eval, and RAG metrics.”,
},
{
“query”: “What does faithfulness measure in a RAG system?”,
“expected”: “Faithfulness measures whether the generated answer is supported by the retrieved context and avoids hallucinations not grounded in that context.”,
},
{
“query”: “What does contextual precision mean?”,
“expected”: “Contextual precision evaluates whether relevant retrieved chunks are ranked higher than irrelevant ones for a given query.”,
},
{
“query”: “What does contextual recall mean in retrieval?”,
“expected”: “Contextual recall measures whether the retriever returns enough relevant context to answer the query, capturing key missing information issues.”,
},
{
“query”: “Why might an answer be relevant but still low quality in RAG?”,
“expected”: “An answer can address the question (relevant) but still be low quality if it is not grounded in retrieved context or misses important details.”,
},
]

Definimos una base de conocimiento estructurada que consta de fragmentos de documentación que sirven como nuestro contexto de verdad para el sistema RAG. También establecemos un conjunto de consultas de evaluación y los resultados esperados correspondientes para crear un “conjunto de datos de oro”, que nos permite evaluar con qué precisión nuestro modelo recupera información y genera respuestas fundamentadas. Consulta los CÓDIGOS COMPLETOS aquí.

clase TfidfRetriever: def __init__(self, docs): self.docs = docs self.texts = [f”{d[‘title’]}\Dakota del Norte[‘text’]}” para d en documentos]self.vectorizer = TfidfVectorizer(stop_words=”english”, ngram_range=(1, 2)) self.matrix = self.vectorizer.fit_transform(self.texts) def retrieve(self, query, k=4): qv = self.vectorizer.transform([query]) sims = coseno_similaridad(qv, self.matrix).flatten() top_idx = np.argsort(-sims)[:k]
resultados = []
para i en top_idx: resultados.append( { “id”: self.docs[i][“id”]”puntuación”: float(sims[i]), “texto”: self.textos[i]} ) devuelve el recuperador de resultados = TfidfRetriever(DOCS)

Implementamos una clase TF-IDF Retriever personalizada que transforma nuestra documentación en un espacio vectorial con capacidad de búsqueda utilizando la vectorización TF-IDF compatible con bigramas. Esto nos permite realizar búsquedas de similitud de cosenos en la base de conocimientos, lo que garantiza que podamos recuperar mediante programación los k fragmentos de texto más relevantes para cualquier consulta determinada. Consulta los CÓDIGOS COMPLETOS aquí.

def extractive_baseline_answer(query, retrieved_contexts): “”” Alternativa sin conexión: creamos una respuesta corta extrayendo las oraciones más relevantes. Esto mantiene el cuaderno ejecutable incluso sin OpenAI. “”” join = “\n”.join(retried_contexts) sents = re.split(r”(?<=[.!?])\s+", unido) palabras clave = [w.lower() for w in re.findall(r"[a-zA-Z]{4,}", consulta)]puntuado = [] para s en envíos: s_l = s.lower() puntuación = suma(1 para k en palabras clave si k en s_l) si len(s.strip()) > 20: scoring.append((score, s.strip())) scoring.sort(key=lambda x: (-x[0]-len(x[1]))) mejor = [s for sc, s in scored[:3] si sc > 0]si no es mejor: mejor = [s.strip() for s in sents[:2] if len(s.strip()) > 20]ans = ” “.join(best).strip() if not ans: ans = “No pude encontrar suficiente contexto para responder con confianza”. return ans def openai_answer(query, retrieved_contexts, model=”gpt-4.1-mini”): “”” Solicitud RAG simple para demostración. Las métricas de DeepEval aún se pueden evaluar incluso si su solicitud de generación difiere; la clave es que almacenamos retrieval_context por separado. “”” from openai import OpenAI client = OpenAI() context_block = “\n\n”.join([f”[CTX {i+1}]\n{c}” for i, c in enumerate(retried_contexts)]) Prompt = f”””Eres un asistente técnico conciso. Utilice SÓLO el contexto proporcionado para responder la consulta. Si la respuesta no está en contexto, diga que no lo sabe. Consulta: {consulta} Contexto: {context_block} Respuesta:””” resp = client.chat.completions.create( modelo=modelo, mensajes=[{“role”: “user”, “content”: prompt}]temperatura = 0,2, ) devolver opciones respectivas[0].message.content.strip() def rag_answer(consulta, contextos_recuperados): si openai_enabled: intente: devolver openai_answer(consulta, contextos_recuperados) excepto Excepción como e: print(f”⚠️ Falló la generación de OpenAI, volviendo a la línea de base extractiva. Error: {e}”) devuelve extractive_baseline_answer(consulta, contextos_recuperados) más: devuelve extractive_baseline_answer(consulta, contextos_recuperados)

Implementamos un mecanismo de respuesta híbrido que prioriza la generación de alta fidelidad a través de OpenAI mientras mantenemos una línea de base extractiva basada en palabras clave como respaldo confiable. Al aislar el contexto de recuperación de la generación final, garantizamos que nuestros casos de prueba de DeepEval sigan siendo consistentes independientemente de si la respuesta es sintetizada por un LLM o extraída mediante programación. Consulta los CÓDIGOS COMPLETOS aquí.

print(“\n🚀 Ejecutando RAG para crear casos de prueba…”) test_cases = []
K = 4 para el artículo en tqdm(EVAL_QUERIES): q = artículo[“query”]
esperado = artículo[“expected”]

recuperado = recuperador.retrieve(q, k=K) contexto_recuperación = [r[“text”] para r en recuperado]actual = rag_answer(q, retrieval_context) tc = LLMTestCase( input=q, actual_output=actual, expected_output=expected, retrieval_context=retrieval_context, ) test_cases.append(tc) print(f”✅ Construido {len(test_cases)} objetos LLMTestCase.”) print(“\n✅ Métricas configurado.”) métricas = [
AnswerRelevancyMetric(threshold=0.5, model=”gpt-4.1″, include_reason=True, async_mode=True),
FaithfulnessMetric(threshold=0.5, model=”gpt-4.1″, include_reason=True, async_mode=True),
ContextualRelevancyMetric(threshold=0.5, model=”gpt-4.1″, include_reason=True, async_mode=True),
ContextualPrecisionMetric(threshold=0.5, model=”gpt-4.1″, include_reason=True, async_mode=True),
ContextualRecallMetric(threshold=0.5, model=”gpt-4.1″, include_reason=True, async_mode=True),

GEval(
name=”RAG Correctness Rubric (GEval)”,
criteria=(
“Score the answer for correctness and usefulness. ”
“The answer must directly address the query, must not invent facts not supported by context, ”
“and should be concise but complete.”
),
evaluation_params=[
LLMTestCaseParams.INPUT,
LLMTestCaseParams.ACTUAL_OUTPUT,
LLMTestCaseParams.EXPECTED_OUTPUT,
LLMTestCaseParams.RETRIEVAL_CONTEXT,
]model=”gpt-4.1″, umbral=0.5, async_mode=True, ), ]si no es openai_enabled: print(“\n⚠️ NO proporcionó una clave API de OpenAI.”) print(“Las métricas de LLM como juez de DeepEval (AnswerRelevancy/Faithfulness/Contextual* y GEval) requieren un juez de LLM.”) print(“Vuelva a ejecutar esta celda y proporcione OPENAI_API_KEY para ejecutar las métricas de DeepEval.”) print(“\n✅ Sin embargo, la construcción de su canalización RAG + caso de prueba fue exitosa de un extremo a otro.”) filas = []
para i, tc en enumerar (test_cases): filas.append({ “id”: i, “query”: tc.input, “actual_output”: tc.actual_output[:220] + (“…” si len(tc.actual_output) > 220 else “”), “expected_output”: tc.expected_output[:220] + (“…” si len(tc.expected_output) > 220 else “”), “contextos”: len(tc.retrieval_context o []), }) display(pd.DataFrame(rows)) rise SystemExit(“Detenido antes de la evaluación (sin clave OpenAI)”).

Ejecutamos la canalización RAG para generar objetos LLMTestCase emparejando nuestro contexto recuperado con respuestas generadas por modelos y expectativas de verdad sobre el terreno. Luego configuramos un conjunto completo de métricas de DeepEval, incluidos G-Eval e indicadores RAG especializados, para evaluar el rendimiento del sistema utilizando un enfoque de LLM como juez. Consulta los CÓDIGOS COMPLETOS aquí.

print(“\n🧪 Ejecutando DeepEval evaluar(…) …”) resultados = evaluar(test_cases=test_cases, metrics=metrics) resumen_rows = []
para idx, tc en enumerate(test_cases): fila = { “case_id”: idx, “query”: tc.input, “actual_output”: tc.actual_output[:200] + (“…” si len(tc.actual_output) > 200 else “”), } para m en métricas: fila[m.__class__.__name__ if hasattr(m, “__class__”) else str(m)] = Ninguno resumen_rows.append(fila) def try_extract_case_metrics(results_obj): extraído = []
candidatos = []
para atributo en [“test_results”, “results”, “evaluations”]: if hasattr(results_obj, attr): candidatos = getattr(results_obj, attr) break si no son candidatos y isinstance(results_obj, lista): candidatos = results_obj para case_i, case_result en enumerate(candidatos o []): item = {“case_id”: case_i} metrics_list = Ninguno para el atributo [“metrics_data”, “metrics”, “metric_results”]: if hasattr(case_result, attr): metrics_list = getattr(case_result, attr) break if isinstance(metrics_list, dict): for k, v in metrics_list.items(): elemento[f”{k}_score”] = getattr(v, “score”, Ninguno) si v no es Ninguno más Ninguno elemento[f”{k}_reason”] = getattr(v, “razón”, Ninguno) si v no es Ninguno más Ninguno más: para mr en metrics_list o []: nombre = getattr(señor, “nombre”, Ninguno) o getattr(getattr(señor, “métrico”, Ninguno), “nombre”, Ninguno) si no es nombre: nombre = señor.__clase__.__nombre__ elemento[f”{name}_score”] = getattr(señor, “puntuación”, Ninguno) elemento[f”{name}_reason”] = getattr(señor, “razón”, Ninguno) extraído.append(elemento) devuelve case_metrics extraído = try_extract_case_metrics(resultados) df_base = pd.DataFrame([{
“case_id”: i,
“query”: tc.input,
“actual_output”: tc.actual_output,
“expected_output”: tc.expected_output,
} for i, tc in enumerate(test_cases)]) df_metrics = pd.DataFrame(case_metrics) si case_metrics en caso contrario pd.DataFrame([]) df = df_base.merge(df_metrics, on=”case_id”, how=”left”) score_cols = [c for c in df.columns if c.endswith(“_score”)]
compacto = df[[“case_id”, “query”] + score_cols].copy() print(“\n📊 Tabla de puntuación compacta:”) display(compact) print(“\n🧾 Detalles completos (incluye motivos):”) display(df) print(“\n✅ Listo. Consejo: si la precisión/recordación contextual es baja, mejore la clasificación/cobertura del retriever; si la fidelidad es baja, ajuste la generación para usar solo el contexto.”)

Finalizamos el flujo de trabajo ejecutando la función de evaluación, que activa el proceso de LLM como juez para calificar cada caso de prueba según nuestras métricas definidas. Luego agregamos estos puntajes y su razonamiento cualitativo correspondiente en un DataFrame centralizado, brindando una vista granular de dónde sobresale el proceso RAG o dónde requiere una mayor optimización en la recuperación y generación.

Por último, concluimos ejecutando nuestro conjunto de evaluación integral, en el que DeepEval transforma resultados lingüísticos complejos en datos procesables utilizando métricas como Fidelidad, Precisión Contextual y la rúbrica G-Eval. Este enfoque sistemático nos permite diagnosticar “fallos silenciosos” en la recuperación y alucinaciones en la generación con precisión quirúrgica, proporcionando el razonamiento necesario para justificar los cambios arquitectónicos. Con estos resultados, pasamos de la creación de prototipos experimentales a un sistema RAG listo para producción respaldado por una red de seguridad verificable basada en métricas.

Consulta los CÓDIGOS COMPLETOS aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 100.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.