Captura lo que tus pruebas no ven: observa y evalúa tu aplicación WEC en producción con Langfuse
- 1Prove a model works
- 2Trustworthy JSON
- 3Real test data at scale
- 4Observe & score production
- 5RAG, end to end
- 🏆Catch regressions in CI
Un cliente dice que tu bot de soporte les prometió una política de reembolso que no existe. Tu función hizo dos llamadas LLM — clasificar, luego responder. ¿Cuál de ellas la inventó? Si no puedes responder eso, tu aplicación es una caja negra — y esta guía soluciona exactamente eso.
Ahora puedes probar que un modelo funciona (parte 1), hacer que su salida sea confiable para máquinas (parte 2), y generar un conjunto de pruebas real para verificarlo (parte 3). Pero todo eso se ejecuta fuera de línea, en CI, con entradas que elegiste. La producción no juega de la misma manera.
Esta guía cierra la brecha. Autoalojaremos Langfuse — la alternativa de código abierto y autoalojable a LangSmith — rastrearemos cada llamada real, evaluaremos automáticamente el tráfico en vivo con un juez LLM, profundizaremos en el paso exacto que falla, y retroalimentaremos las fallas para que tu conjunto de datos de la parte 3 se vuelva más fuerte. La evaluación fuera de línea te dice que funcionó en tu conjunto de pruebas; esto te dice que funciona en el mundo real.
La habilidad que esto añade
Al final podrás tomar cualquier función WEC y: ver dentro de cada llamada, evaluar la calidad en tráfico real automáticamente, encontrar la llamada exacta que falló y por qué, y convertir fallas en producción en nuevos casos de prueba. Esa es la diferencia entre "pasó CI" y "se mantiene en producción."
Mantendremos el ejemplo en ejecución de la parte 3 — un clasificador de tickets de soporte en la API de Inferencia WEC — y lo pondremos bajo observación.
Lo que estamos construyendo
Tu aplicación llama a la API WEC como de costumbre; Langfuse (autoalojado) captura silenciosamente cada llamada como un rastreo, un juez LLM evalúa cada una, y las fallas fluyen de vuelta a tu conjunto de datos de la parte 3 — para que la producción siga fortaleciendo tus evaluaciones:
Por qué Langfuse (y cuándo elegir algo más ligero)
Usamos Langfuse porque es el líder de código abierto en observabilidad LLM (con licencia MIT, autoalojable), su SDK es compatible con OpenAI por lo que apunta a la API WEC sin cambios, y — la razón por la que encaja en esta serie — tiene conjuntos de datos, evaluación y evaluadores LLM-como-juez integrados. Eso permite que tu conjunto de datos de la parte 3 y el bucle de "evaluar producción, retroalimentar fallas" sean de primera clase en lugar de añadidos.
El compromiso: la pila de producción es pesada (Postgres + ClickHouse). Si no necesitas las características de evaluación y quieres algo más ligero, alternativas razonables son Arize Phoenix (nativa de OpenTelemetry, fuerte para evaluación fuera de línea), Helicone (un proxy de una línea — pero sin evaluaciones), o OpenLIT (un solo binario, nativa de OTEL). Para un flujo de trabajo impulsado por evaluaciones como este, los contenedores adicionales valen la pena; para un simple rastreo, uno de esos puede ser más adecuado para ti.
Paso 1 — Autoalojar Langfuse en una Instancia WEC
La pila autoalojada de Langfuse consta de seis servicios (web + trabajador, ClickHouse, Postgres, Redis, almacenamiento de objetos), así que dale una Instancia WEC con ~15 GB de disco y 4 GB+ de RAM — no lo metas en una máquina que ya esté ocupada.
git clone https://github.com/langfuse/langfuse.git && cd langfuse
Genera tres secretos y colócalos en un .env (la ENCRYPTION_KEY debe ser hex, o el contenedor web no arrancará):
cat > .env <<EOF
NEXTAUTH_SECRET=$(openssl rand -base64 32)
SALT=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
NEXTAUTH_URL=http://<tu-ip-de-instancia>:3000
EOF
docker compose up -d
docker compose ps
Figura 1. docker compose ps — los seis servicios (web, trabajador, ClickHouse, Postgres, Redis, MinIO) en funcionamiento y saludables.
Si la máquina ya ejecuta algo en el puerto 3000 o 5432, Langfuse no arrancará ("dirección ya en uso"). Las bases de datos de respaldo no necesitan puertos de host en absoluto — edita docker-compose.yml para mover langfuse-web a un puerto de host libre (por ejemplo, 3001:3000) y elimina el mapeo del host de Postgres. Mantener tus bases de datos fuera de la red del host es una buena práctica de todos modos. Consulta Solución de problemas.
Abre http://<ip-de-instancia>:3000, crea una cuenta (el primer usuario es el propietario), luego una
Organización → Proyecto, y bajo Configuración → Claves API crea un par de claves
(pk-lf-… / sk-lf-…).
Paso 2 — Instrumentar el clasificador para que cada llamada sea rastreada
Apunta el SDK de Langfuse a tu instancia y usa su OpenAI drop-in — rastrea automáticamente cualquier llamada compatible con OpenAI, incluida la API de Inferencia WEC, sin código adicional alrededor de cada solicitud:
python3 -m venv ~/lf-env && source ~/lf-env/bin/activate
pip install langfuse openai
export LANGFUSE_PUBLIC_KEY="pk-lf-…"
export LANGFUSE_SECRET_KEY="sk-lf-…"
export LANGFUSE_HOST="http://<ip-de-instancia>:3000" # usa el puerto que expusiste — 3001 si lo remapeaste en el Paso 1
export WEC_API_KEY="sk-tu-clave-wec"
import os
from langfuse.openai import openai # drop-in: rastrea automáticamente cada llamada
from langfuse import get_client
client = openai.OpenAI(
base_url="https://inference.wiline.com/v1",
api_key=os.environ["WEC_API_KEY"],
timeout=30, # SIEMPRE establece esto — un backend lento cuelga para siempre de lo contrario
)
def classify(ticket: str) -> str:
resp = client.chat.completions.create(
model="Qwen2.5-3B-Instruct",
name="ticket-classify", # nombra el rastreo en Langfuse
messages=[{"role": "user", "content":
"Clasifica este ticket de soporte. Devuelve SOLO JSON con claves "
"categoría (facturación/técnico/cuenta/otro), prioridad (baja/media/alta), "
f"resumen. Ticket: {ticket}"}],
)
return resp.choices[0].message.content
if __name__ == "__main__":
print(classify("Me cobraron dos veces este mes."))
get_client().flush() # empuja los rastros antes de que el proceso termine
Ejecuta el código, luego abre Rastreo — la llamada aparecerá en segundos con entrada, salida, latencia, conteos de tokens, y (una vez que agreguemos precios) costo.
Figura 2. Una llamada ticket-classify en Langfuse: el prompt, la salida JSON, latencia (~1.2s),
y conteos de tokens — capturada sin código adicional alrededor de la solicitud.
El cliente de OpenAI no tiene un tiempo de espera predeterminado. Si el modelo es lento o el backend se detiene, tu
aplicación se cuelga indefinidamente — sin error, sin rastreo. timeout=30 convierte eso en un fallo rápido y visible.
De una llamada a un pipeline real — el árbol de spans
Una llamada es fácil de registrar; no necesitas Langfuse para eso. El rastreo se justifica en funciones de múltiples pasos,
donde muestra toda la cadena como un solo árbol para que puedas ver qué paso falló. Hagamos
que la función sea realista — clasifica el ticket, luego redacta una respuesta de esa categoría (dos llamadas WEC) —
y agrúpalas en un solo rastreo con el decorador @observe:
from langfuse import observe, get_client
from app import client, classify # reutiliza el cliente WEC rastreado + clasificador
def draft_reply(ticket: str, category: str) -> str:
resp = client.chat.completions.create(
model="Qwen2.5-3B-Instruct",
name="draft-reply",
messages=[{"role": "user", "content":
f"Un cliente envió este ticket de {category}. Escribe una respuesta corta y útil.\nTicket: {ticket}"}],
)
return resp.choices[0].message.content
@observe() # agrupa todo lo que está debajo en UN solo rastreo
def handle_ticket(ticket: str) -> str:
category = classify(ticket) # child span 1
reply = draft_reply(ticket, category) # child span 2
return reply
if __name__ == "__main__":
print(handle_ticket("Me cobraron dos veces este mes."))
get_client().flush()
Abre ese rastreo y obtendrás un árbol: handle_ticket → ticket-classify → draft-reply, cada
hijo con su propia entrada, salida, latencia y tokens. Ahora un fallo tiene una dirección — puedes
señalar el paso exacto que falló en lugar de adivinar (usamos esto en el Paso 5).
Ahora puedes ver dentro de cualquier llamada LLM en producción — entrada, salida, latencia, tokens — sin escribir una línea de código de registro.
Paso 3 — Reproducir tu conjunto de datos de la parte 3 a través de ello
Para obtener una señal real (no una llamada), empuja tu conjunto de datos de la parte 3 a través del clasificador para que el tablero tenga tráfico para analizar:
import os, json
from langfuse.openai import openai
from langfuse import get_client
from app import classify # reutiliza la función rastreada
for line in open(os.path.expanduser("~/tickets_dataset.jsonl")):
if line.strip():
classify(json.loads(line)["ticket"])
get_client().flush()
Ahora el Tablero de inicio muestra rastreos a lo largo del tiempo, una distribución de latencia (p50/p95), tokens por modelo, y un total de costos — la vista de "observa en producción".
Figura 3. Después de reproducir el conjunto de datos de la parte 3: rastreos a lo largo del tiempo, una distribución de latencia, tokens
por modelo, y un total de costos — la vista de "observa en producción".
Hacer que el costo sea real para los modelos WEC
Langfuse calcula automáticamente el costo para los modelos que conoce (OpenAI, Anthropic), pero la API de Inferencia WEC
no devuelve costo en dólares y Langfuse no conoce los precios de los modelos de WEC — así que el costo muestra $0. Esta es
la misma brecha que encontramos en parte 1. Soluciona esto una vez:
Configuración → Modelos → agregar un modelo que coincida con Qwen2.5-3B-Instruct con tus precios por token de entrada/salida.
Ahora cada rastreo y el tablero muestran el gasto real — el seguimiento de costos que Promptfoo
no pudo darnos.
Paso 4 — Evaluar automáticamente el tráfico en vivo con un juez LLM
La evaluación de CI verifica un conjunto fijo. Aquí evaluamos cada llamada real automáticamente con un LLM-como-juez. Este paso parece pura configuración — también es donde encontramos y solucionamos el mejor error de la serie.
Conectar al juez
Crea el evaluador a través de Evaluadores → + Nuevo evaluador. Es un asistente de 3 pasos:
1. Seleccionar Evaluador — elige la plantilla de Correctitud LLM-como-juez.
2. Configurar conexión LLM — el juez necesita un modelo para llamar, y aún no hay ninguno, así que haz clic + Agregar conexión LLM. El diálogo tiene más campos de los que esperarías — aquí está exactamente lo que necesita cada uno:
- Adaptador LLM:
openai - Nombre del proveedor: cualquier etiqueta sin dos puntos (por ejemplo,
wec-judge) — solo el nombre de la conexión en Langfuse - Clave API: tu clave WEC
- Haz clic en Mostrar configuraciones avanzadas:
- URL base de la API:
https://inference.wiline.com/v1— crítico; si se deja en blanco, golpea a OpenAI real - Usar API de Respuestas: deja apagado (WEC habla Completions de Chat)
- Encabezados adicionales: ninguno — omitir
- Habilitar modelos predeterminados: desactivar (no quieres la lista de modelos de OpenAI aquí)
- Modelos personalizados → Agregar nombre de modelo personalizado:
Qwen3.5:9B— la elección del modelo importa mucho aquí, como verás
- URL base de la API:
- Crear conexión, luego selecciona el proveedor
wec-judgey el modeloQwen3.5:9B→ Guardar.
Figura 4. El diálogo de Nueva Conexión LLM: adaptador openai, la URL base de la API WEC, modelos predeterminados apagados, y un modelo personalizado agregado — esto es lo que apunta al juez a WEC en lugar de
OpenAI real.
3. Ejecutar Evaluador — agrega un filtro Nombre = ticket-classify, activa Ejecutar en observaciones entrantes en vivo para que evalúe el nuevo tráfico, y Ejecutar.
Figura 5. El evaluador de Correctitud — nombre de la evaluación, el filtro ticket-classify, y "ejecutar en
observaciones entrantes en vivo."
Luego: sin puntuaciones. Depurando el tiempo de espera de 120 segundos
Configuramos todo, enviamos tráfico fresco… y nada. Sin puntuación en ningún rastreo. Así es como lo rastreamos — puedes encontrar la misma pared:
- Verifica el registro del evaluador. Evaluadores → tu evaluador → Registros tenía el error real:
Request timed out after 120000ms. Cada ejecución, exactamente 120s — así que el juez sí se estaba activando, su llamada LLM simplemente nunca regresó. - Sospecha del modelo. Nuestro primer juez,
Qwen2.5-3B-Instruct, rechazó las llamadas a herramientas de inmediato (400 instantáneo — una configuración vLLM faltante en el backend; más sobre eso a continuación). Cambiamos aQwen3.5:9B, que responde a una llamada directa a la herramienta en ~3s. Aún así, se agotó el tiempo. - Sospecha de la red.
curldesde el host: 200 en medio segundo. La misma solicitud de estilo juez desde dentro del contenedor trabajador: ~3s. Modelo, red, contenedor — todo bien. - Reproduce lo que Langfuse realmente envía. El juez no llama a la API como nuestro curl — utiliza salida estructurada (
response_format) para que la puntuación regrese en un formato legible por máquina. Reproducir esa forma exacta de llamada expuso el problema: en este backend, las solicitudes de estilo juez al modelo de razonamiento son increíblemente lentas e impredecibles — la solicitud idéntica varió de 3 a 95 segundos en diferentes ejecuciones (el modelo "piensa" durante mucho tiempo, y el modo JSON añade sobrecarga). Con el prompt más largo del juez, las ejecuciones superaron el tiempo de espera predeterminado de 120s. Nada estaba roto — simplemente era más lento que el tiempo de espera.
La solución son dos líneas de configuración. Langfuse lee su tiempo de espera LLM de
LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS (predeterminado 120000). Dale espacio:
services:
langfuse-worker:
environment:
LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS: "600000"
langfuse-web:
environment:
LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS: "600000"
docker compose up -d langfuse-worker langfuse-web
# verifica que se haya aplicado dentro del contenedor:
docker compose exec langfuse-worker env | grep TIMEOUT
Envía un nuevo rastreo a través de app.py, espera un par de minutos, y:
Figura 6. Ahí está — Correctitud: 1.00 adjunta a un rastreo ticket-classify en vivo, con
el razonamiento del juez a un clic de distancia. Cada nueva llamada ahora se evalúa automáticamente.
Ahora puedes evaluar automáticamente el tráfico real en producción — y depurar un evaluador que produce silencio: verifica sus Registros, aísla modelo vs. red vs. contenedor, ajusta el tiempo de espera a la realidad.
El juez necesita llamadas a herramientas. Si tu modelo devuelve un 400 en cualquier solicitud de tools, eso
generalmente es la configuración de servicio, no el modelo — en vLLM son las banderas --enable-auto-tool-choice y
--tool-call-parser. Ese fue nuestro caso: lo informamos, el equipo de la plataforma habilitó las
banderas en Qwen2.5-3B-Instruct el mismo día, y el 400 desapareció. Solo la conexión del juez se ve afectada — tu aplicación mantiene su modelo.
La puntuación es lenta (~2 min/puntuación) y eso es el backend, no tú. El modelo de razonamiento "piensa" durante mucho tiempo en los prompts del juez — vale la pena informarlo a tu equipo de plataforma (límites de presupuesto de razonamiento, o un backend de decodificación guiada como xgrammar en vLLM). El tiempo de espera elevado mantiene la puntuación confiable mientras tanto.
Una vez que se habilitó la llamada a herramientas en Qwen2.5-3B, lo intentamos como juez — las puntuaciones regresaron en
2–10 segundos en lugar de ~2 minutos. Pero eran basura: el mismo tipo de respuesta correcta obtuvo
1, 1, 0, y 0.05 en cuatro ejecuciones, con racionales casi idénticos adjuntos a puntuaciones opuestas. El modelo pequeño llena perfectamente el formato de puntuación; simplemente no puede evaluar realmente.
Imagen espejo del hallazgo de parte 3: el modelo pequeño gana en clasificación y pierde en evaluación — la evaluación es una tarea de razonamiento. Mantén el juez en Qwen3.5:9B: lento pero coherente.
Paso 5 — Encontrar la falla que tus pruebas pasaron por alto
Aquí es donde el árbol de spans da sus frutos. Abre un rastreo handle_ticket y no obtienes un solo blob opaco — obtienes la cadena completa como un árbol, cada paso con su propia entrada, salida, latencia y
tokens:
Figura 7. El árbol handle_ticket — handle_ticket → ticket-classify → draft-reply — con
latencia y tokens por paso. Cada paso tiene una dirección que puedes inspeccionar.
Esa estructura es lo que hace que las fallas en producción sean depurables. Supongamos que un cliente recibe una respuesta sin sentido. Sin rastreo solo sabrías "la respuesta fue mala." Con el árbol lo expandes y lees cada paso — por ejemplo:
ticket-classify→categoría: "facturación"✅ correcto — así que el clasificador no es el problema.draft-reply→ inventó una política de reembolso que no existe ❌ — el error está en el paso 2.
Has localizado la falla en el paso exacto, entrada y salida — deberías corregir el prompt de draft-reply,
no perder tiempo en el clasificador. La evaluación fuera de línea te dice que la tasa de aprobación cayó; el rastreo
te dice qué llamada, con qué entrada, produjo qué salida incorrecta, en qué paso. Y ahora que el Paso 4 evalúa cada llamada, ni siquiera tienes que buscar: filtra el Rastreo por puntuación baja y los rastreos sospechosos emergen por sí mismos — a menudo una redacción que tu conjunto de datos de la parte 3 nunca cubrió, que es exactamente lo que capturas de vuelta a continuación.
Ahora puedes asignar una falla en producción al paso exacto, entrada y salida — no más adivinanzas sobre qué llamada inventó esa política de reembolso.
Paso 6 — Cerrar el ciclo: producción → mejores evaluaciones
Una falla en producción es un caso de prueba faltante — y Langfuse hace que capturarlo sea un clic. Crea
el conjunto de datos una vez (Conjuntos de datos → + Nuevo conjunto de datos, por ejemplo, fallas-en-producción), luego en cualquier rastreo haz clic en + Agregar a conjuntos de datos y selecciónalo. La entrada y salida del rastreo se completan automáticamente como un nuevo ítem:
Figura 8. "Agregar a conjuntos de datos" convierte un rastreo de producción en un nuevo caso de prueba — entrada y salida esperada completadas automáticamente — alimentándolo directamente de vuelta a tu conjunto de datos de la parte 3.
Para automatizarlo, extrae los rastreos que has marcado o agregado a un conjunto de datos a través del SDK de Langfuse, escribe
filas {ticket, categoría, prioridad} en tickets_dataset.jsonl, y vuelve a ejecutar la evaluación de Promptfoo de la parte 1/parte 3 — ahora cubre el caso del mundo real.
Ahora tu evaluación de CI es más fuerte porque la producción encontró un hueco en ella. Ese es el ciclo: evaluación fuera de línea → enviar → observar y evaluar tráfico real → capturar fallas → la evaluación fuera de línea se vuelve más fuerte.
Has cerrado el ciclo: las fallas en producción ahora hacen que tus evaluaciones sean más fuertes — el ciclo que mantiene una función de IA buena después de que se envía.
Lo que ahora puedes hacer
- Ver dentro de cada llamada WEC en producción (entrada/salida/latencia/costo).
- Evaluar tráfico real automáticamente con un juez LLM — y depurar el evaluador mismo cuando se queda en silencio.
- Depurar una falla específica hasta el paso exacto, no solo una métrica agregada.
- Convertir fallas en producción en casos de prueba, para que tus evaluaciones sigan mejorando.
Esa es la escalera de "mantenerlo bien después del envío". La evaluación fuera de línea (partes 1–3) prueba que funciona antes del lanzamiento; esto prueba — y mantiene — que sigue funcionando después.
Solución de problemas
"dirección ya en uso" en docker compose up
Otro servicio ocupa el puerto 3000 (web) o 5432 (Postgres). Las bases de datos no necesitan puertos de host —
en docker-compose.yml, remapea langfuse-web a un puerto libre (3001:3000) y elimina el mapeo del host de Postgres. Compose fusiona las listas de ports, así que un archivo de anulación con ports: [] no las eliminará; edita el compose directamente.
El contenedor web no arranca
Casi siempre ENCRYPTION_KEY — debe ser openssl rand -hex 32 (64 caracteres hexadecimales), no base64.
Verifica docker compose logs langfuse-web.
El costo muestra $0
Langfuse no tiene precio para los modelos de WEC por defecto. Agrega el modelo + precios por token bajo Configuración → Modelos (Paso 3).
No aparecen puntuaciones en nuevos rastreos
Verifica Evaluadores → tu evaluador → Registros primero — un error allí te dice que está ejecutándose y fallando, no inactivo. Luego empareja el error:
- Un 400 mencionando llamadas a herramientas /
tool_choice— la llamada a herramientas no está habilitada para ese modelo en el backend de servicio (en vLLM: las banderas--enable-auto-tool-choice/--tool-call-parser). Dos formas de salir: apunta la Conexión LLM del juez a un modelo donde funcione (Qwen3.5:9B), o pide a tu equipo de plataforma que habilite las banderas — eso es lo que solucionóQwen2.5-3B-Instructpara nosotros. De cualquier manera, solo cambia el juez — tu aplicación mantiene su modelo. Request timed out after 120000ms— la llamada de salida estructurada del juez es más lenta que el tiempo de espera predeterminado de Langfuse. AumentaLANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS(Paso 4) y confirma condocker compose exec langfuse-worker env | grep TIMEOUT. Sicurlsimple es rápido pero la puntuación aún se agota, no persigas la red — la ruta de salida estructurada es lo que es lento, y eso es una solución del backend.
Las puntuaciones tardan ~2 minutos cada una
Eso no es un colapso — es el modelo de razonamiento generando una larga cadena de pensamiento oculta en los prompts del juez (altamente variable: la misma solicitud puede tardar 3s o 95s), además de la sobrecarga de salida estructurada. Aumentar el tiempo de espera hace que la puntuación sea confiable pero no rápida — la verdadera solución está del lado de la plataforma (límites de presupuesto de razonamiento, backend de decodificación guiada).
Qué sigue
Ahora puedes construir, probar, observar y mejorar una sola función WEC. La última escalera es el verdadero patrón de aplicación: RAG — recuperación + generación — con todo de las partes 1–4 integrado (salida verificada por esquema, un conjunto de datos de evaluación, y rastreo de Langfuse sobre todo el pipeline). Ahí es donde todo se une.
