SEXTANTEcursos técnicos de IA
métodobackward-design
árbitroel dato
Entrar
N5 · Producción y experimentación/L2

Evals online con Langfuse

Objetivo de maestría

configurarás evals online en Langfuse sobre el tráfico real de Aurora, a nivel de observación y con sampling, controlando el coste. Sin esto, tu juez del N2 solo ve el dataset, nunca el mundo que de verdad llega al agente.


5.1El juez que te arruina

Aurora lleva dos semanas en producción. Tu gate de CI del N4 está verde y tu juez del N2 detecta alucinaciones con TPR y TNR sobre el dataset. Quieres lo siguiente obvio: saber si Aurora alucina en producción, no solo en tu dataset.

La idea inmediata es correr ese juez sobre cada conversación real. Haces la cuenta.

Aurora recibe 40.000 conversaciones al día. Tu juez es un LLM grande con prompt largo y chain-of-thought —así lo diseñaste en el N2, porque un juez efectivo suele usar más cómputo que el sistema evaluado—. Evaluar las 40.000 multiplica tu factura de inferencia por 40.000 llamadas extra al día.

Tienes dos salidas malas. Evalúas el 100% del tráfico y la factura del juez supera a la del propio agente. O no evalúas nada en producción y vuelves a las vibes del N0·L1: te enteras de los fallos por el tuit, no por el dashboard.

Esta lección construye la tercera salida.


5.2Qué vas a poder hacer

Al terminar sabrás:

  • Configurar un eval online en Langfuse: tu juez del N2 corriendo sobre trazas de producción en vivo, sin bloquear la respuesta al usuario.
  • Apuntar el eval a una observación concreta —el span de generación, el de retrieval— y no solo a la traza entera.
  • Aplicar sampling para que el coste del juez sea una fracción del coste de evaluarlo todo.
  • Verificar las dos condiciones que un juez debe cumplir antes de soltarlo en producción.

Necesitas saber antes:

  • El juez LLM-as-judge del N2, validado contra humanos con TPR/TNR.
  • Qué es una observación frente a una traza, del N0·L2 (jerarquía traza → observaciones).
  • La instrumentación de Aurora con Langfuse del N0·L3/L4 (spans de retrieval, generación y tool calls).

Esta lección no rehace el juez ni la instrumentación. Los reutiliza. Aquí los conectas con el tráfico real.


5.3Recupera

Antes de seguir, responde mentalmente. No mires lo de abajo hasta tener tu respuesta.

  1. En el N2 validaste tu juez antes de confiar en él. ¿Qué dos métricas usaste y contra qué los comparaste?
  2. Del N0·L2: ¿qué diferencia hay entre una traza y una observación del agente de Aurora?
  3. Una conversación de Aurora deja una traza con un span de retrieval (consultar_politica) y un span de generación (la respuesta de Claude). Si el agente alucina por recuperar la política equivocada, ¿en qué span está la evidencia del fallo?

La respuesta a la 1: TPR (tasa de verdaderos positivos: de los fallos reales, cuántos detecta el juez) y TNR (tasa de verdaderos negativos: de los aciertos reales, cuántos deja pasar). Los comparaste contra etiquetas humanas, no contra accuracy cruda, porque el dataset estaba desbalanceado.

La 3 anticipa el corazón de la lección: la evidencia está en el span de retrieval —recuperó el chunk equivocado—, aunque el síntoma aparezca en la generación. Localizar el fallo exige mirar la observación correcta, no solo el resultado final.


5.4El concepto: evaluar lo que de verdad llega

Esta sección introduce tres piezas que encajan entre sí. Lee la definición de cada una y para un segundo antes de la siguiente; encadenan.

Online vs offline: dos relojes distintos

Hasta el N4 evaluaste offline: tu suite corría contra un dataset versionado, antes de desplegar. Es un examen con respuestas conocidas.

Un eval online es tu juez corriendo sobre trazas de producción en vivo, en ingest time —cuando la traza llega a Langfuse—. Se ejecuta de forma asíncrona, sin bloquear la respuesta al usuario (Langfuse, core-concepts, jun 2026). El usuario recibe su respuesta a la velocidad de siempre; el juez puntúa después, en segundo plano.

La analogía: el eval offline es un simulacro de examen antes del lanzamiento; el online es un inspector que audita pedidos reales según salen por la puerta. El límite de la analogía: el inspector no audita todos los pedidos —ahí entra el sampling—.

No compiten. Se complementan. El offline protege el deploy de regresiones conocidas (N4). El online te dice qué hace el agente ante inputs que tu dataset nunca vio. El ciclo, como adelantó el N5·L1: offline pre-deploy → detectas problemas en prod → añades casos al dataset → nueva iteración offline (Langfuse, core-concepts).

Observation-level evals: localizar, no solo detectar

Hasta febrero de 2026, los evaluadores online de Langfuse solo podían puntuar trazas completas: lentos, del orden de minutos, y a grano grueso. Sabías que la conversación falló, no dónde.

Desde feb 2026, Langfuse soporta observation-level evals: evaluar una observación concreta —una generación, un retrieval, un tool call— y no solo la traza entera (Langfuse, changelog observation-level evals, 13 feb 2026). La ejecución pasa de minutos a segundos.

Esto encaja con la localización del fallo del N3. Recuerda la pregunta del recall: si Aurora alucina por mala recuperación, la evidencia está en el span de retrieval. Un eval a nivel de traza te da un veredicto único sobre toda la conversación. Un eval a nivel de observación puntúa la generación por separado del retrieval. Cuando la groundedness baja, sabes si apuntar al retriever o al modelo.

Un matiz de versión: observation-level evals requiere el SDK de Python v3+ (en 2026, v4) o el de JS/TS v4+. No funciona con SDKs v2 legados (Langfuse, changelog).

Sampling: comprar solo la señal que necesitas

Aquí está la respuesta al hook. No evalúas el 100% del tráfico. Configuras un sampling: una fracción de las observaciones —por ejemplo el 5%— que el evaluador puntúa; el resto pasa sin score (Langfuse, llm-as-a-judge, jun 2026).

Es muestreo estadístico clásico. Para estimar la tasa de alucinación de Aurora no necesitas auditar las 40.000 conversaciones diarias; una muestra representativa te da el dato con un coste proporcional. Evaluar al 5% cuesta una vigésima parte de evaluar al 100%.

Dos detalles que el corpus fija y conviene no inventar:

  • El sampling se configura en el evaluador, dentro de Langfuse, no en el código Python de tu agente. No existe una API del SDK para fijar el sampling rate desde la instrumentación (Langfuse, llm-as-a-judge, jun 2026).
  • La metadata del span sirve para filtrar qué observaciones entran al evaluador (por ejemplo env="prod"), no para controlar el sampling rate (Langfuse, llm-as-a-judge).

El requisito de calidad: no sueltes un juez sin validar

Es común querer activar el juez online cuanto antes. Resiste la tentación. Un juez mal calibrado puntuando producción genera una métrica que parece dato y es ruido —exactamente lo que el curso combate—.

Langfuse recomienda lo mismo que ya hiciste en el N2: validar el evaluador con TPR y TNR superando el 90% antes de activarlo en producción (Langfuse, automated-evaluations, 12 sep 2025). Y el modelo del juez debe soportar structured output, porque cada eval emite un score estructurado por observación (Langfuse, llm-as-a-judge). Si tu juez del N2 ya pasó ese umbral, está listo. Si no, vuelve al N2 antes de seguir.


5.5Míralo funcionar: groundedness online sobre Aurora

Vamos a montar un eval online de groundedness —¿la respuesta de Aurora se apoya en el contexto recuperado?— sobre el span de generación, con sampling al 5% y filtro por metadata.

El montaje tiene dos mitades. Primero, código de instrumentación en tu agente: marca cada traza con los atributos por los que el evaluador filtrará. Segundo, configuración en la UI de Langfuse: ahí se elige el target, el sampling y el mapeo. Lee primero ambas mitades enteras; luego las desmenuzamos.

Mitad 1 — instrumentar para que el filtro funcione

python
1from langfuse import get_client, propagate_attributes
2
3langfuse = get_client()  # lee LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_HOST
4
5def responder_en_produccion(mensaje, user_id, session_id):
6    with langfuse.start_as_current_observation(
7        as_type="span", name="soporte-aurora", input=mensaje
8    ) as root:
9        # Marca la traza con atributos que el evaluador usará para filtrar.
10        with propagate_attributes(
11            user_id=user_id,
12            session_id=session_id,
13            metadata={"env": "prod"},
14            tags=["produccion"],
15            version="1.2.0",
16        ):
17            # El span de retrieval: consultar_politica (RAG).
18            with langfuse.start_as_current_observation(
19                as_type="retriever", name="consultar_politica", input=mensaje
20            ) as ret:
21                contexto = consultar_politica(mensaje)
22                ret.update(output=contexto)
23
24            # El span de generación: aquí evaluará el juez de groundedness.
25            with langfuse.start_as_current_observation(
26                as_type="generation", name="claude", model="claude-sonnet-4-6"
27            ) as gen:
28                resp, n_in, n_out = llamar_a_claude(mensaje, contexto)
29                gen.update(
30                    output=resp,
31                    usage_details={"input_tokens": n_in, "output_tokens": n_out},
32                )
33        root.update(output=resp)
34    langfuse.flush()
35    return resp

Nota: claude-sonnet-4-6 es el ID vigente en los ejemplos del curso. Ajústalo al modelo de tu cuenta. consultar_politica y llamar_a_claude son las funciones de Aurora que ya instrumentaste en el N0.

Self-explanation. Antes de seguir, responde: ¿por qué propagate_attributes va envolviendo los spans, y no después de crearlos?

Porque su docstring lo fija: solo el span activo y los creados después de entrar en ese contexto reciben los atributos; los spans preexistentes no se actualizan retroactivamente (Langfuse, propagation.py). Si lo pusieras al final, el filtro del evaluador por metadata/tags/userId no aplicaría a tus observaciones. La doc lo dice sin rodeos: para filtrar observaciones por atributos de traza, usa propagate_attributes() en tu instrumentación (Langfuse, llm-as-a-judge).

Ese es el único código Python que esta lección necesita de tu lado. El sampling y el prompt del juez no se configuran aquí.

Mitad 2 — configurar el evaluador en la UI

En Langfuse, el evaluador se monta en la interfaz. Los pasos (Langfuse, llm-as-a-judge, jun 2026):

  1. Evaluators → + Set up Evaluator.
  2. Default Model: una LLM Connection que soporte structured output (tu juez del N2).
  3. Tipo: plantilla Managed o Custom (tu prompt de groundedness con {{variables}}).
  4. Evaluation Target: elige Live Observations (recomendado), no Live Traces (legacy). Aquí decides observation-level.
  5. Filters: filtra por observation type = generation y metadata.env = prod. Solo las generaciones de producción entran.
  6. Sampling: pon 5%. El evaluador puntuará 1 de cada 20 observaciones que pasen el filtro.
  7. Variable Mapping: conecta los campos de la observación a los placeholders del prompt (input, output, contexto recuperado) con JSONPath.
  8. Preview: valida contra las trazas de las últimas 24 h antes de activar.

Tras activarlo, los scores aparecen en cada observación de generación muestreada, no solo en la traza. Abres una conversación, despliegas el span claude, y ves su score de groundedness junto a su latencia y tokens.

Si más adelante automatizas la creación de evaluadores por API: la API programática de evaluadores de Langfuse está marcada como [unstable] —bajo rediseño, en /api/unstable/evaluators—. La propia doc recomienda la UI. No construyas el lab del checkpoint sobre esa API.

Por qué observation-level localiza el fallo

Cierra el bucle con el recall. Imagina que el score de groundedness cae en una generación. Como el eval apunta a la observación de generación y no a la traza entera, sabes que el síntoma está en la respuesta. Despliegas el span hermano de retrieval en la misma traza y compruebas qué recuperó. Si trajo la política equivocada —el envios-12 del N0·L1—, el culpable es el retriever, no el modelo. Un eval a nivel de traza te habría dado un único "falló" sin decirte dónde mirar.


5.6Hazlo tú

Ejercicio 1 — andamiaje parcial: de 100% a 5%, de traza a observación

Un compañero dejó este evaluador configurado en Langfuse, en la UI:

  • Evaluation Target: Live Traces
  • Sampling: 100%
  • Filters: (ninguno)

Está puntuando cada traza de producción entera, sin filtro. La factura del juez se ha disparado y, cuando un score baja, nadie sabe si es culpa del retrieval o de la generación.

Reescribe la configuración para arreglar las dos cosas. Completa los huecos:

  • Evaluation Target: __________ (pista: localizar, no solo detectar).
  • Sampling: __________ (pista: una fracción, no todo).
  • Filters: observation type = __________ y metadata.____ = prod.
  • ¿Qué tienes que añadir en el código Python del agente para que ese filtro por metadata funcione sobre las observaciones?

Ejercicio 2 — autónomo: un eval online para "escaló de más"

El agente de Aurora a veces escala de más: abre un ticket humano para una pregunta trivial que la KB respondía. Quieres un eval online que detecte ese fallo en producción, desde cero.

Diseña la configuración. Sin mirar la solución, decide y justifica:

  • ¿A qué observación apuntas el evaluador? (¿la generación, el tool call de escalar_a_humano, la traza?)
  • ¿Qué sampling pones, y por qué ese y no 100%?
  • ¿Qué dos condiciones del juez verificas antes de activarlo?

Antes de seguir, una pregunta de interrogación elaborativa. Respóndela tú primero: ¿por qué el sampling al 5% es aceptable para estimar una tasa de fallo, pero NO sería aceptable para garantizar que ninguna conversación de alto riesgo se cuela sin revisar?

Porque el sampling estima una proporción poblacional —cuántas Auroras alucinan, de media—, y para eso una muestra representativa basta. Pero no cubre casos individuales: el 95% no muestreado pasa sin score. Para garantizar revisión de cada caso de alto riesgo, el muestreo no sirve; necesitas filtrar el 100% de esa categoría (por metadata) o derivarla a una cola de revisión humana. El sampling compra señal agregada barata, no cobertura individual.


5.7Comprueba

Sin pistas. Dos partes.

Parte A — coste. Aurora recibe 40.000 conversaciones al día. Tu juez cuesta, en redondo, 0,01 € por evaluación. Calcula el coste diario del juez evaluando al 100% y al 5%, y expresa cuántas veces más barato es el 5%.

Parte B — antes de soltarlo. Nombra las dos cosas que hay que verificar de tu juez antes de activarlo como eval online en producción.

Ver la respuesta razonada

Parte A.

  • Al 100%: 40.000 × 0,01 € = 400 € / día.
  • Al 5%: 40.000 × 0,05 × 0,01 € = 2.000 evals × 0,01 € = 20 € / día.
  • El 5% es 20 veces más barato (1/0,05 = 20). Tiene sentido: muestrear una vigésima parte cuesta una vigésima parte.

Parte B.

  1. Que TPR y TNR superan el 90% (validado contra humanos, como en el N2). Un juez por debajo de ese umbral produce una métrica de producción que parece dato y es ruido.
  2. Que el modelo del juez soporta structured output, porque cada eval emite un score estructurado por observación.

Feedback formativo:

  • Si acertaste A y B con su justificación: dominas las dos decisiones que definen un eval online sano —coste por sampling y calibración del juez—. Las dos reaparecen en el checkpoint C5, donde montarás esto sobre tráfico simulado.
  • Si calculaste mal el coste (p. ej. 5% de 40.000 = 200 en vez de 2.000): el fallo está en el orden de magnitud, no en el método. 5% de 40.000 es 2.000, no 200. Repite la cuenta separando "cuántas evals" (40.000 × 0,05) de "cuánto cuesta cada una" (× 0,01 €).
  • Si en B dijiste solo TPR/TNR y olvidaste structured output: la mitad correcta es la de calidad estadística; la que falta es técnica. Sin structured output, el juez no puede emitir un score parseable por observación y el eval online no se ejecuta. Relee §5.4, "El requisito de calidad".

5.8Conecta

Vuelve a la cuenta del §5.1: 40.000 conversaciones, un juez que multiplicaba la factura por cada request. Ya tienes la tercera salida. Evalúas el tráfico real al 5%, a nivel de observación, con un juez validado. La factura del juez es una vigésima parte; cuando un score cae, sabes en qué span mirar.

Pero fíjate en lo que aún te falta. Mides solo lo que tú decides medir —groundedness, escalado de más, lo que configuraste—. Y mides solo el 5%; el otro 95% pasa sin tu mirada.

Hay una señal que llega gratis sobre ese 95%: la del propio usuario. Los clientes de Aurora ya te dicen si la respuesta sirvió —unos pulsan el pulgar abajo, muchos más reformulan la pregunta o abandonan—. El N5·L3 captura esa señal como scores: feedback explícito e implícito, con sus límites de ruido y reward hacking.

¿Dónde lo aplicarías en tu trabajo? Piensa en cualquier sistema LLM que hayas tocado. Si hoy quisieras saber su tasa de alucinación en producción sin duplicar tu factura de inferencia, ya tienes el patrón: un juez validado, apuntado a la observación que importa, muestreando una fracción del tráfico.


5.9Reflexiona

Tómate dos minutos. Estas preguntas consolidan más que releer.

  • Con tus palabras: ¿qué problema resuelve el sampling y qué problema NO resuelve?
  • ¿Por qué evaluar a nivel de observación, y no solo de traza, te ayuda a localizar el fallo del N3?
  • ¿Qué sigue sin estar claro? Si es "cómo decido el porcentaje de sampling exacto", es una pregunta justa: depende de tu volumen y tu presupuesto, y el corpus solo fija el 5% como ejemplo, no como ley.

Referencia rápida

  • Eval online: tu juez del N2 sobre trazas de producción en vivo, en ingest time, asíncrono, sin bloquear al usuario (Langfuse, core-concepts). Complementa al offline (N4), no lo sustituye.
  • Observation-level evals (feb 2026): evaluar una observación concreta (generación, retrieval, tool call), no solo la traza. Localiza el fallo; ejecución en segundos. Requiere SDK Py v4 / JS v4 (Langfuse, changelog, 13 feb 2026).
  • Sampling: fracción del tráfico que el evaluador puntúa (p. ej. 5%). Se configura en el evaluador (UI), no por SDK Python. La metadata del span sirve para filtrar, no para el sampling rate (Langfuse, llm-as-a-judge).
  • propagate_attributes(): obligatorio en la instrumentación para que los filtros del evaluador por userId/sessionId/tags/metadata apliquen a las observaciones. Envuelve los spans; no actualiza spans previos.
  • Antes de producción: TPR y TNR > 90% + juez con structured output (Langfuse, automated-evaluations, 12 sep 2025).
  • No construir sobre: la API programática de evaluadores (/api/unstable/evaluators) — marcada [unstable]. Usa la UI.