Prueba de regresión de tu servicio RAG con DeepEval — y resuelve un debate sobre modelos con datos
Nuestro análisis de economía de tokens de GPT-5.6 terminó con un desafío: las promesas de referencia son una hipótesis, no una razón — ejecuta la evaluación en tu carga de trabajo antes de que creas en un reclamo de "menos tokens por tarea". Este tutorial es nosotros siguiendo nuestro propio consejo.
Tomamos el asistente de documentos RAG de parte 5,
lo envolvemos en una suite de regresión DeepEval — en contenedor, juzgada por gemma4 en la API de
Inferencia WEC, sin dependencia de OpenAI — y luego usamos esa suite para responder a una pregunta genuinamente abierta:
¿deberíamos cambiar el modelo de generación del servicio? Qwen2.5-3B-Instruct (actual) vs
gemma4 (candidato), tasa de aprobación y tokens por tarea, medidos cara a cara.
Spoiler: la evaluación nos salvó de una migración innecesaria. Y en el camino encontramos cuatro fallos reales de producción: un espiral mortal por disco lleno, una condición de carrera en el arranque, una trampa de variable de Docker Compose que evaluó silenciosamente el modelo incorrecto, y la solución que hace que esa clase de error sea imposible. Cada comando, número y error a continuación proviene de una ejecución real.
Lo que estamos construyendo
Tres decisiones de diseño desde el principio:
- El ejecutor de evaluación es un contenedor, no un venv. La suite que ejecutas localmente es byte por byte
el artefacto que ejecuta CI —
docker compose run evalen ambos lugares. (Nuestro primer intento fue un venv. Murió por un disco lleno antes de instalar nada; consulta Solución de problemas.) - El juez es gemma4 en WEC. DeepEval asume OpenAI por defecto; le damos una clase de modelo personalizada de 30 líneas en su lugar. Las preguntas, respuestas y veredictos nunca salen de nuestra infraestructura.
- Reutilizamos el conjunto dorado de la parte 5 sin cambios. El
tests.csvde promptfoo incrusta cada hecho de referencia dentro de una cadenallm-rubric— una expresión regular lo extrae. Tus datos de evaluación sobreviven a tu marco de evaluación.
Requisitos previos
- El servicio RAG en funcionamiento de parte 5
(
rag-apirespondiendo en:8000,eval/tests.csvpresente). - Una clave de API de Inferencia WEC (Inferencia → Claves de API).
- ~2 GB de disco libre. En serio — verifica
df -h /ahora. El nuestro leía 100% y la historia de por qué está en Solución de problemas.
Paso 0 — prueba de humo del sistema bajo prueba
Nunca evalúes un servicio que no hayas probado manualmente primero:
curl -s -X POST localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"question": "¿Cómo creo una instancia de cómputo en WEC?"}' | python3 -m json.tool
Figura 1. El sistema bajo prueba, vivo: /ask responde del corpus de documentos de WEC y cita
sus fuentes. Nunca evalúes un servicio que no hayas probado manualmente primero.
Paso 1 — el ejecutor de evaluación en contenedor
eval/Dockerfile:
FROM python:3.12-slim
WORKDIR /eval
RUN pip install --no-cache-dir deepeval==3.*
# El código de la suite se monta en tiempo de ejecución para que las ediciones no necesiten una reconstrucción
CMD ["deepeval", "test", "run", "test_rag_regression.py"]
Agrega el servicio a docker-compose.yml dentro del bloque services: (el archivo de la parte 5 termina
con una sección volumes: de nivel superior — agregar ciegamente coloca tu servicio en el bloque incorrecto;
docker compose config --quiet lo detecta):
eval:
build: ./eval
profiles: ["eval"]
env_file: .env
environment:
RAG_API_URL: http://rag-api:8000
volumes:
- ./eval:/eval
depends_on:
- rag-api
profiles: ["eval"] lo mantiene fuera de un docker compose up normal — solo se ejecuta cuando lo pides.
Construyelo:
docker compose --profile eval build eval
Figura 2. El ejecutor de evaluación se construye en ~42s: python:3.12-slim más DeepEval 3.x. Esta imagen exacta
es la que CI ejecuta más tarde — sin venv, sin desviaciones.
Paso 2 — un juez en tu propia infraestructura
Las métricas de LLM-como-juez de DeepEval quieren una clave de OpenAI. No tenemos una y no queremos una.
eval/wec_judge.py:
"""Juez personalizado de DeepEval respaldado por WiLine Inference (compatible con OpenAI)."""
import os
from openai import OpenAI
from deepeval.models.base_model import DeepEvalBaseLLM
WEC_BASE_URL = os.getenv("WEC_BASE_URL", "https://inference.wiline.com/v1")
class WECJudge(DeepEvalBaseLLM):
def __init__(self, model: str | None = None):
self.model_name = model or os.getenv("JUDGE_MODEL", "gemma4")
self.client = OpenAI(
base_url=WEC_BASE_URL,
api_key=os.environ["WEC_API_KEY"],
)
def load_model(self):
return self.client
def generate(self, prompt: str) -> str:
response = self.client.chat.completions.create(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
return response.choices[0].message.content
async def a_generate(self, prompt: str) -> str:
return self.generate(prompt)
def get_model_name(self):
return f"WEC/{self.model_name}"
Prueba que responde desde dentro de la red de compose antes de construir nada sobre ello:
docker compose --profile eval run --rm eval \
python -c "from wec_judge import WECJudge; j = WECJudge(); print('el juez dice:', j.generate('Responde exactamente: juez WEC en línea'))"
el juez dice: juez WEC en línea
Figura 3. "juez WEC en línea" — gemma4 respondiendo a través del contenedor, en la misma red de compose que el servicio que juzgará. Sin clave de OpenAI en ninguna parte de la pila.
Una advertencia honesta que llevamos a través del resto de este tutorial: en la comparación de modelos más adelante, gemma4 es tanto juez como concursante. La mitigación es estructural: el juez solo compara respuesta frente a hecho de referencia, nunca "qué modelo escribió esto" — y observamos sus veredictos en los casos conocidos difíciles para favoritismo. (No mostró ninguno: cada caso en el que falló gemma4 es un caso en el que también falla Qwen.)
Puedes apuntar las métricas juzgadas por LLM de DeepEval a cualquier punto final compatible con OpenAI. Tus veredictos de evaluación — preguntas, respuestas, razonamiento — nunca salen de tu infraestructura.
Paso 3 — expón lo que quieres medir
El /ask de la parte 5 devolvió answer y sources — y desechó el uso de tokens que venía con cada finalización. Para la economía de tokens lo necesitamos, y para el A/B necesitamos que el modelo sea intercambiable. Tres variables de entorno y dos campos de respuesta en app/main.py (montado en volumen; uvicorn
recarga en caliente, sin reconstrucción):
# El modelo de generación es configurable para que la suite de evaluación pueda A/B modelos.
# Los valores predeterminados preservan la configuración del tutorial-5.
GEN_MODEL = os.getenv("GEN_MODEL", "Qwen2.5-3B-Instruct")
GEN_BASE_URL = os.getenv("GEN_BASE_URL", "https://inference.wiline.com/v1")
GEN_API_KEY = os.getenv("GEN_API_KEY", os.environ["WEC_API_KEY"])
generate() ahora también devuelve resp.usage, y /ask informa ambos:
{
"answer": "Para ver tus estados de cuenta ...",
"sources": ["..."],
"model": "Qwen2.5-3B-Instruct",
"usage": {
"prompt_tokens": 1238,
"completion_tokens": 122,
"total_tokens": 1360
}
}
Figura 4. Los nuevos campos model y usage: 1,238 tokens de prompt + 122 tokens de finalización para una
pregunta de facturación. Este es el número del debate sobre economía de tokens.
Ese bloque usage es el número del marketing de GPT-5.6 — medido en tu carga de trabajo,
por solicitud. Pasa la variable a través de compose (GEN_MODEL: ${GEN_MODEL:-Qwen2.5-3B-Instruct}
bajo rag-api's environment:) y recuerda esta línea — vuelve a morder en
Solución de problemas.
Paso 4 — la suite de regresión
eval/test_rag_regression.py. Lee el tests.csv de la parte 5, extrae el hecho de referencia de
cada rúbrica de promptfoo, pregunta al servicio en vivo, registra el uso de tokens y afirma una métrica de corrección
de GEval juzgada por WECJudge:
"""Suite de regresión DeepEval para el servicio RAG de documentos WEC.
Reutiliza el conjunto dorado de promptfoo (tests.csv) del tutorial anterior:
el hecho de referencia se analiza de cada cadena llm-rubric.
"""
import csv, json, os, re
import pytest
import requests
from deepeval import assert_test
from deepeval.metrics import GEval
from deepeval.test_case import LLMTestCase, LLMTestCaseParams
from wec_judge import WECJudge
RAG_API_URL = os.getenv("RAG_API_URL", "http://rag-api:8000")
USAGE_LOG = os.getenv("USAGE_LOG", "usage_log.jsonl")
MAX_CASES = int(os.getenv("MAX_CASES", "0")) # 0 = todos
FACT_RE = re.compile(r'Hecho de referencia de los documentos oficiales: "(.*?)"')
def load_cases():
with open("tests.csv", newline="") as f:
for row in csv.DictReader(f):
m = FACT_RE.search(row["__expected"])
yield row["question"], (m.group(1) if m else "")
CASES = list(load_cases())
if MAX_CASES:
CASES = CASES[:MAX_CASES]
judge = WECJudge()
correctness = GEval(
name="Correctness",
criteria=(
"La salida real debe abordar correctamente la pregunta de entrada y no debe "
"contradecir el hecho de referencia dado como salida esperada. Diferente redacción "
"está bien. Fallar solo si la respuesta es incorrecta, contradice el hecho de referencia, "
"o no responde a la pregunta."
),
evaluation_params=[
LLMTestCaseParams.INPUT,
LLMTestCaseParams.ACTUAL_OUTPUT,
LLMTestCaseParams.EXPECTED_OUTPUT,
],
model=judge,
threshold=0.5,
)
@pytest.mark.parametrize(
"question,reference", CASES, ids=[q[:50] for q, _ in CASES]
)
def test_rag_regression(question, reference):
r = requests.post(f"{RAG_API_URL}/ask", json={"question": question}, timeout=120)
r.raise_for_status()
data = r.json()
expect = os.getenv("EXPECT_MODEL")
assert not expect or data["model"] == expect, (
f"la suite esperaba {expect} pero el servicio está ejecutando {data['model']}"
)
with open(USAGE_LOG, "a") as f:
f.write(json.dumps({
"model": data.get("model"),
"question": question,
**data.get("usage", {}),
}) + "\n")
assert_test(
LLMTestCase(
input=question,
actual_output=data["answer"],
expected_output=reference,
),
[correctness],
)
(Esa afirmación EXPECT_MODEL no estaba en la primera versión. Existe debido a un error que encontrarás en un momento.)
Primera ejecución, limitada a 5 casos para fallar rápido:
docker compose --profile eval run --rm \
-e MAX_CASES=5 -e DEEPEVAL_TELEMETRY_OPT_OUT=YES \
eval
4 de 5 pasaron en 2:37 — y tanto los pases como la falla valen la pena leer. La falla ("¿Cuál es el punto de partida para la facturación?", puntuación 0.3) no es un error del servicio: la respuesta era factualmente correcta, pero la etiqueta dorada es un título de sección ("Cómo Funciona la Facturación") y el juez quería que se nombrara. Recuerda este caso — parpadea entre pasar y fallar durante todo el tutorial, y ese parpadeo se convierte en un hallazgo. Mientras tanto, en "número total de eventos" el juez puntuó un 0.6, un pase límite con el razonamiento exactamente correcto (el servicio describió la característica en lugar de dar pasos). Veredictos matizados de un juez autoalojado — la cosa que la gente asume que necesitas modelos de clase GPT-4 para.
Figura 5. Primera ejecución: 4 de 5 pasaron en 2:37, cada veredicto con razonamiento escrito del
juez gemma4 — incluyendo la única falla, un veredicto estricto sobre una etiqueta de título de sección
en lugar de un error del servicio.
Nota la línea de resumen de DeepEval: costo de token: Ninguno. No puede calcular el precio de un juez autoalojado — que es
exactamente por qué la suite escribe su propio usage_log.jsonl.
Figura 6. La contabilidad propia de la suite: una línea JSON por pregunta — modelo, prompt, finalización,
total — los datos en bruto de cada comparación en este tutorial se construyen a partir de esto.
Tienes una suite de regresión que califica un servicio RAG en vivo — reutilizando el conjunto dorado del último tutorial sin cambios, juzgada por un modelo autoalojado, con contabilidad de tokens por pregunta que el marco mismo no puede proporcionarte.
Paso 5 — el experimento: Qwen2.5-3B vs gemma4
La pregunta que la suite existe para responder: nuestro servicio genera con Qwen2.5-3B-Instruct — ¿debería hacerlo? gemma4 es más nuevo, bien considerado, ya está en el catálogo de WEC. Se siente como una mejora. Se siente. Vamos a medir.
Línea base, todos los 16 casos:
docker compose --profile eval run --rm \
-e DEEPEVAL_TELEMETRY_OPT_OUT=YES \
-e USAGE_LOG=usage_qwen.jsonl \
eval
14/16 (87.5%) en 8:50. Las dos fallas son artefactos de etiqueta, no errores de RAG: los hechos dorados
de la parte 5 son títulos de sección ("Cómo Funciona la Facturación"), y el juez penalizó respuestas que eran
factualmente correctas pero no repetían el título; y mp3 frente a la etiqueta audio/mpeg — el mismo hecho en
dos niveles de abstracción, dictaminado como una contradicción. Lee tus fallas antes de confiar en tu tasa de aprobación. Dejamos ambas: son una deuda de etiqueta honesta, y se convierten en nuestro canario de favoritismo del juez en el A/B.
Figura 7. La línea base: 87.5% en 8:50. Ambas fallas son artefactos de etiqueta, no errores de RAG —
lee tus fallas antes de confiar en tu tasa de aprobación.
Desafiante — misma suite, una variable de entorno (prefíjela en cada comando de compose; Solución de problemas explica por qué en sangre):
GEN_MODEL=gemma4 docker compose up -d rag-api
until curl -sf localhost:8000/health > /dev/null; do echo "esperando..."; sleep 3; done
curl -s -X POST localhost:8000/ask -H "Content-Type: application/json" -d '{"question": "ping"}' \
| python3 -c "import json,sys; print('>>> el modelo es:', json.load(sys.stdin)['model'])"
GEN_MODEL=gemma4 docker compose --profile eval run --rm \
-e DEEPEVAL_TELEMETRY_OPT_OUT=YES \
-e USAGE_LOG=usage_gemma4.jsonl \
-e EXPECT_MODEL=gemma4 \
eval
Y verifica el registro, porque las etiquetas de ejecución mienten:
sort <(cat eval/usage_gemma4.jsonl | python3 -c "import json,sys; [print(json.loads(l)['model']) for l in sys.stdin]") | uniq -c
# 16 gemma4
Figura 8. Cambiando el desafiante: GEN_MODEL=gemma4, espera por /health, luego confirma
>>> el modelo es: gemma4 desde la propia respuesta — antes de gastar un solo minuto de evaluación.
Figura 9. La ejecución de gemma4: 81.25% en ~13 minutos — y sus tres fallas son los mismos
casos de artefactos de etiqueta en los que Qwen parpadea. Sin favoritismo del juez gemma4 hacia el concursante gemma4.
La tabla de veredictos
Un script sobre los registros de uso:
| ejecución | tasa de aprobación | tokens promedio de prompt | tokens promedio de finalización | tokens totales |
|---|---|---|---|---|
| Qwen2.5-3B (ejecución 1) | 87.5% | 1,126 | 75 | 19,225 |
| Qwen2.5-3B (ejecución 2) | 81.25% | 1,126 | 75 | 19,225 |
| gemma4 | 81.25% | 1,199 | 415 | 25,834 |
Figura 10. El veredicto en cinco líneas: misma tasa de aprobación, 5.5× los tokens de finalización. La evaluación
justo previno una migración innecesaria.
Dos hallazgos, uno esperado y uno no:
El veredicto: no cambies. Misma calidad efectiva, mismos casos de falla — pero gemma4 usa
5.5× los tokens de finalización (415 frente a 75 por respuesta) y se ejecuta ~45% más lento por pregunta. En una
carga de trabajo RAG donde el contexto recuperado de ~1,100 tokens domina el prompt, la columna de finalización es toda la diferencia de costo. "Modelo más nuevo" perdió ante "mídelo" en una tarde. Esta es precisamente la evaluación que el post de noticias de GPT-5.6 dijo que ejecutarías — y es la misma evaluación que ejecutarías
contra GPT-5.6 mismo: cambia GEN_MODEL, GEN_BASE_URL, y GEN_API_KEY, nada más.
El juez tiene un umbral de ruido. Mira las dos filas de Qwen: cuentas de tokens idénticas — 19,225 en ambas ejecuciones, porque la generación con temperatura 0 es determinista — sin embargo 87.5% frente a 81.25%. Las respuestas no cambiaron; solo los veredictos del juez lo hicieron (un límite de 0.6 se convirtió en un 0.3). Eso aísla limpiamente la no determinación del juez del comportamiento del modelo, y establece tu umbral de CI: una puerta en "≥ 87.5%" fallaría. El nuestro tolera un veredicto cambiado y falla en dos o más regresiones reales.
Figura 11. Ejecución 2 de Qwen: las mismas 16 respuestas deterministas, 81.25% esta vez — el juez, no
el modelo, es donde vive la variación.
Puedes resolver un debate sobre el cambio de modelo con datos medidos — tasa de aprobación y tokens por tarea — y conoces el umbral de ruido de tu juez, así que puedes establecer un umbral de CI que no falle.
Paso 6 — la puerta de CI
La recompensa de contenerizar todo: el trabajo de CI es el comando que has estado ejecutando todo
el tiempo. .github/workflows/eval.yml:
name: rag-regression-eval
on:
pull_request:
paths: ["app/**", "eval/**"]
workflow_dispatch:
jobs:
eval:
runs-on: [self-hosted, wec] # corredor con acceso a inference.wiline.com
steps:
- uses: actions/checkout@v4
- name: Ejecutar suite de regresión
env:
WEC_API_KEY: ${{ secrets.WEC_API_KEY }}
run: |
docker compose up -d rag-api
docker compose --profile eval run --rm \
-e DEEPEVAL_TELEMETRY_OPT_OUT=YES \
-e EXPECT_MODEL=Qwen2.5-3B-Instruct \
eval
- name: Desmontar
if: always()
run: docker compose down
Cualquier PR que toque la aplicación o la evaluación debe sobrevivir a 16 preguntas juzgadas contra el servicio en vivo — con la identidad del modelo afirmada — antes de que se fusione.
Tu evaluación es una puerta de CI. El contenedor que ejecutaste localmente es byte por byte lo que CI ejecuta — sin "funciona en mi máquina" brecha entre desarrollo y la tubería.
Solución de problemas — las cuatro fallas reales
[Errno 28] No hay espacio en el dispositivo — y volvió
Nuestra primera pip install deepeval falló: disco 100% lleno, 58G de 58G. Liberamos 2 GB de registros de diario
y caché de construcción… y estaba lleno de nuevo minutos después. El culpable:
7.9G /var/lib/docker/containers/10281d.../10281d...-json.log
langfuse-clickhouse-1 estaba en un bucle de error, rociando trazas de pila en un registro json de Docker con
sin rotación configurada — un espiral mortal, porque un disco lleno causa más errores que causa más
registros. Había estado "no saludable" durante 8 días. La solución, en orden (el orden importa — truncar mientras
el contenedor se ejecuta simplemente vuelve a llenar):
docker stop langfuse-clickhouse-1
sudo sh -c "truncate -s 0 /var/lib/docker/containers/10281d*/*-json.log"
Luego hazlo permanente — rotación de registros en la sobrecarga de compose, aplicada a cada servicio:
x-logging: &default-logging
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
services:
clickhouse:
<<: *default-logging
# ... cada otro servicio
docker compose up -d para recrear. ClickHouse se volvió saludable por primera vez en 8 días.
El controlador json-file de Docker no rota por defecto; en una caja que ejecuta infraestructura LLM
que registra entusiastamente, los registros de contenedor sin límites son una bomba de tiempo.
13 pruebas fallan con Conexión rechazada, luego 3 pasan
Cambiar GEN_MODEL recrea rag-api, y la aplicación tarda ~3 minutos en arrancar (Chroma +
carga del modelo de incrustación). Nuestra suite comenzó a golpear /ask inmediatamente: 13 fallas
de conexión rechazadas seguidas, luego la API cobró vida a mitad de ejecución y las últimas 3 pasaron. Flaky de la peor manera — la
falla depende del tiempo de reloj de pared.
Soluciona esto en la suite, no en la disciplina humana — eval/conftest.py:
"""Rechaza iniciar la suite hasta que la API RAG esté saludable."""
import os, time
import requests
RAG_API_URL = os.getenv("RAG_API_URL", "http://rag-api:8000")
READY_TIMEOUT = int(os.getenv("READY_TIMEOUT", "300"))
def pytest_configure(config):
deadline = time.time() + READY_TIMEOUT
while time.time() < deadline:
try:
requests.get(f"{RAG_API_URL}/health", timeout=3).raise_for_status()
return
except requests.RequestException:
time.sleep(3)
raise RuntimeError(f"rag-api no saludable después de {READY_TIMEOUT}s — abortando evaluación")
La trampa de variable de compose: evaluamos el modelo incorrecto. Dos veces.
La más desagradable, porque nada falló. ${GEN_MODEL:-Qwen…} en compose se interpola
desde tu shell en cada invocación. Comenzamos el servicio con
GEN_MODEL=gemma4 docker compose up -d — correcto. Luego ejecutamos la evaluación sin el prefijo.
Compose resolvió la variable de nuevo al valor predeterminado de Qwen, vio el contenedor en ejecución como un desvío de configuración
y — a través de depends_on — silenciosamente recreó rag-api con Qwen. La ejecución se completó,
el resumen decía "gemma4" en cada veredicto (el juez era gemma4), y cada respuesta había sido
escrita por Qwen.
Solo lo atrapamos porque la suite registra el modelo desde la respuesta:
sort <(... usage_gemma4.jsonl ...) | uniq -c
# 16 Qwen2.5-3B-Instruct ← la ejecución "gemma4"
Nunca confíes en la etiqueta de una ejecución; verifica desde los datos que el sistema bajo prueba informa sobre sí mismo.
La solución permanente es la afirmación EXPECT_MODEL en la suite (Paso 4). Prueba que funciona — la
prueba de fallo deliberado:
FAILED ... AssertionError: la suite esperaba gemma4 pero el servicio está ejecutando Qwen2.5-3B-Instruct
1 falló en 2.44s
Menos de tres segundos, antes de gastar una sola llamada de juez — frente a los 10+ minutos que cada ejecución mal etiquetada nos costó.
Figura 12. La guardia en acción: modelo incorrecto → fallo ruidoso en menos de tres segundos, antes de que se gaste una
sola llamada de juez. Compara con los 10+ minutos que cada ejecución mal etiquetada nos costó.
sleep 5 no es una verificación de disponibilidad
Pequeño bono: después de recrear el contenedor, canalizamos la salida de /ask a un analizador JSON 5
segundos después y obtuvimos json.decoder.JSONDecodeError: Esperando valor: línea 1 columna 1 — el
servidor no estaba arriba, curl devolvió vacío, el analizador se ahogó en nada. Sondea /health en un bucle;
nunca duermas un número adivinado.
Lo que construiste
- Una suite de regresión DeepEval en contenedor — misma imagen localmente y en CI
- Un juez personalizado en WEC Inference (
gemma4+ GEval): veredictos con razonamiento, sin clave de OpenAI - Contabilidad de tokens por pregunta que el marco mismo no podía darte
- Una decisión de modelo medida: gemma4 ≈ Qwen en calidad, 5.5× los tokens de finalización — mantén Qwen
- Una medición del umbral de ruido del juez (respuestas deterministas, un cambio de tasa de aprobación de 6.25 puntos) que establece tu umbral de CI de manera honesta
- Una guardia
EXPECT_MODELque convierte la evaluación silenciosa del modelo incorrecto en un fallo ruidoso en menos de tres segundos - Una puerta de GitHub Actions que ejecuta el contenedor idéntico en cada PR
Qué sigue
La suite juzga respuestas. Las trazas de Langfuse de la parte 4 saben por qué — qué fragmentos fueron recuperados, cuánto costó el rango de generación, dónde se oculta la latencia. Conectar fallas de evaluación de vuelta a sus trazas (ID de prueba de DeepEval ↔ ID de traza de Langfuse) convierte "la prueba falló" en "aquí está la recuperación que lo causó." Esa es la historia de depuración a nivel de componente, y es hacia donde va esta serie a continuación.
