Saltar al contenido principal
avanzadoPart 4

Captura lo que tus pruebas no ven: observa y evalúa tu aplicación WEC en producción con Langfuse

· 20 min de lectura
Rafael Fernandes
Ingeniero de PLN y Redactor Técnico en WiLine
Share:
Langfuse+
0/6
🎯 Skill path0/6 earned
AI evals & observability

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

La pila de Langfuse en funcionamiento — seis servicios saludables Figura 1. docker compose ps — los seis servicios (web, trabajador, ClickHouse, Postgres, Redis, MinIO) en funcionamiento y saludables.

Desplegando junto a otros servicios

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"
app.py
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.

Un solo rastreo — entrada, salida, latencia y conteos de tokens 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.

Establecer un tiempo de espera

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:

pipeline.py
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_ticketticket-classifydraft-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).

Habilidad desbloqueada 🏅

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:

replay.py
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".

El tablero de Langfuse poblado con tráfico reproducido 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/v1crí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
  • Crear conexión, luego selecciona el proveedor wec-judge y el modelo Qwen3.5:9BGuardar.

El diálogo de Nueva Conexión LLM — URL base de WEC, modelos predeterminados apagados, un modelo WEC personalizado agregado 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.

La configuración del evaluador LLM-como-juez 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:

  1. 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 se estaba activando, su llamada LLM simplemente nunca regresó.
  2. 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 a Qwen3.5:9B, que responde a una llamada directa a la herramienta en ~3s. Aún así, se agotó el tiempo.
  3. Sospecha de la red. curl desde el host: 200 en medio segundo. La misma solicitud de estilo juez desde dentro del contenedor trabajador: ~3s. Modelo, red, contenedor — todo bien.
  4. 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:

docker-compose.override.yml
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:

La puntuación de Correctitud adjunta a un rastreo real 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.

Habilidad desbloqueada 🏅

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.

Dos restricciones a tener en cuenta

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.

Atajo tentador que no funciona: un modelo pequeño como juez

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:

Profundizando en un rastreo — el árbol de spans localiza cada paso Figura 7. El árbol handle_tickethandle_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-classifycategorí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.

Habilidad desbloqueada 🏅

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:

Agregando un rastreo de vuelta al conjunto de evaluación 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.

Habilidad desbloqueada 🏅

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-Instruct para 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. Aumenta LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS (Paso 4) y confirma con docker compose exec langfuse-worker env | grep TIMEOUT. Si curl simple 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).

Finished this tutorial?
Mark it complete to earn Observe & score production on your skill path.

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.

Lectura adicional