SEXTANTEcursos técnicos de IA
métodobackward-design
árbitroel dato
Entrar
N0 · Observabilidad/L6

De trazas a preguntas: tu dataset listo para el análisis

Objetivo de maestría

producir un dataset de ≥100 trazas reales del agente de Aurora, navegable y reproducible — el entregable del checkpoint C0. Importa porque sin volumen no ves patrones, y sin patrones no hay error analysis.


6.1El problema: tres ejemplos no son un patrón

Instrumentaste el agente de Aurora en las cinco lecciones anteriores. Cada request produce ahora una traza con sus spans anidados: retrieval, llamada al LLM, tool call. Abres Langfuse y miras tus trazas. Tienes tres.

Tres trazas te muestran tres historias. No te muestran un patrón.

Aquí está el dolor concreto. El agente de Aurora a veces inventa el estado de un pedido sin llamar a buscar_pedido. A veces recupera la política equivocada. A veces escala de más. Con tres trazas, ¿cuál de esos fallos es el más frecuente? ¿Cuál cuesta más dinero? No lo sabes. Estás otra vez en territorio de vibes, solo que con mejores herramientas.

El error analysis del próximo nivel necesita una entrada concreta: un conjunto de trazas lo bastante grande para que los modos de fallo se vuelvan visibles por repetición. Hamel Husain, coautor del libro de O'Reilly sobre evals, lo formula como instrucción operacional: revisa 50–100 conversaciones reales para encontrar los modos de fallo más comunes. Tres no bastan. Necesitas volumen.

Esta lección cierra N0 generando ese volumen. No vas a aprender un concepto nuevo grande. Vas a consolidar lo que ya sabes hasta convertirlo en un dataset que el resto del curso explotará.


6.2Qué vas a poder hacer

Al terminar esta lección podrás:

  • Explicar qué hace que un dataset de trazas sea "explotable" para el error analysis: volumen, sesiones, campos críticos y reproducibilidad.
  • Generar tráfico de prueba contra el agente de Aurora con un script que deja ≥100 trazas en Langfuse.
  • Verificar que el dataset es navegable: que puedes reconstruir qué pasó en cualquier request.
  • Aprobar el checkpoint C0 evaluando tu propio dataset contra su rúbrica de cinco dimensiones.

Necesitas saber antes (de este mismo nivel):

  • El agente de Aurora instrumentado con spans anidados, de N0·L5. Sin esa instrumentación, no hay trazas que generar.
  • La jerarquía trace → span → observation → session, de N0·L2.
  • Los campos críticos de un span (tokens, coste, latencia, finish_reason, errores), de N0·L4.
  • El decorador @observe de Langfuse y las sesiones, de N0·L3 y N0·L5.

6.3Recupera

Antes de generar el dataset, recupera lo que sostiene esta lección. Responde mentalmente antes de leer la pista; recuperar de memoria fija el conocimiento mejor que releer.

Pregunta 1 (jerarquía). El agente de Aurora atiende una conversación de tres turnos del mismo cliente. ¿Qué entidad agrupa esos tres turnos como una sola conversación?

La session. Agrupa varias traces de una conversación multi-turno: Sessions > Traces > Spans (corpus B.1). En OTel puro, session no es una entidad, sino el atributo gen_ai.conversation.id.

Pregunta 2 (span y padre). En una traza, un tool call cuelga directamente de la raíz en vez de colgar del span del LLM que lo invocó. ¿Qué campo está mal?

El parent_span_id (en Langfuse, parentObservationId). Cuando vale null, la observación se adjunta a la raíz y rompe la jerarquía — el bug del span huérfano de N0·L5 (corpus B.1, B.6).

Pregunta 3 (campos críticos). Vuelve el fallo del pedido inventado. Quieres saber cuánto costó esa respuesta y por qué se cortó. ¿Qué dos campos del span LLM consultas?

El coste (derivado de tokens in/out) y el finish_reason. Son campos mínimos de un span LLM junto a input, output, latencia, modelo+versión y errores (corpus B.1).

Si fallaste alguna, vuelve a la lección indicada antes de seguir. Es común confundir trace con session: la diferencia clave es que una session contiene varias traces, una por turno de la conversación.


6.4El concepto: qué hace un dataset "explotable"

Ya sabes capturar una traza. La pregunta de esta lección es distinta: ¿qué convierte una pila de trazas en materia prima para el análisis?

Un dataset de trazas explotable es un conjunto de trazas reales con dos cualidades. De cada una reconstruyes, request a request, qué pasó por dentro. Y en conjunto tiene la masa suficiente para que los patrones de fallo emerjan. La analogía: es la diferencia entre un fotograma suelto y una grabación completa. Un fotograma te dice un instante; la grabación te deja ver el movimiento que se repite. La analogía falla en un punto: la grabación es continua, y tu dataset es discreto — son requests independientes que tú agrupas.

Cuatro propiedades lo hacen explotable. Las cuatro ya las trabajaste; aquí las unes.

Volumen. Necesitas ≥100 trazas. No es una cifra mágica: es el umbral por debajo del cual los fallos raros no aparecen y los frecuentes no se distinguen del ruido. Husain marca 50–100 conversaciones como punto de partida para el error analysis (corpus A.2). Con menos, confundes anécdota con tendencia.

Sesiones. Las trazas multi-turno se agrupan por session_id, con user_id para saber de quién es cada conversación. Sin sesión, ves requests sueltos y pierdes el hilo: un fallo del tercer turno suele explicarse por lo que pasó en el primero.

Campos críticos. Cada span lleva input, output, tokens, coste, latencia, modelo, finish_reason y errores (corpus B.1). Estos campos son las columnas por las que filtrarás y contarás en el análisis. Un span sin ellos es una traza que ves pero no puedes interrogar.

Reproducibilidad. El dataset se puede re-generar. Si tu colega corre tu script y obtiene un dataset equivalente, tienes un activo de ingeniería. Si nació de clics manuales irrepetibles, tienes una foto que no puedes rehacer cuando cambies el agente.

Contraejemplo — qué NO es un dataset explotable. Cincuenta trazas donde la mitad tiene tool calls colgando de la raíz (spans huérfanos) y ningún session_id. Tienes volumen, pero no causalidad ni hilo conversacional. Ves que algo falló, no por qué ni en qué turno. Volumen sin estructura no es explotable.

El puente a N1: "look at your data"

Para qué sirve todo esto. Husain y Shankar son tajantes sobre el orden de operaciones en evals: "You cannot know what to measure until you systematically find out how your product fails" (corpus A.2). El error analysis cualitativo precede a montar métricas. Empezar con métricas genéricas —hallucination, toxicity— produce evals que no correlacionan con tus problemas reales.

El acto fundacional es mirar tus datos: "Start by doing the error analysis yourself" (Husain, corpus A.2). Y para mirarlos, primero tienen que existir en cantidad. Eso es lo que produces hoy.

En N1 abrirás esas ≥100 trazas y las leerás una a una como un investigador: anotando el primer fallo de cada una. Esta lección no analiza nada — el análisis es N1. Aquí construyes el insumo. La regla del nivel se mantiene: en N0 hacemos los fallos visibles, no los diagnosticamos.


6.5Míralo funcionar: generar 100+ trazas

Vas a leer un script que corre N sesiones simuladas contra el agente de Aurora. Cada sesión es una conversación de varios turnos; cada turno genera una traza; el conjunto deja más de 100 trazas en Langfuse.

Prerequisito. El script usa get_client(), que lee las credenciales del entorno. Antes de correrlo, ten configuradas LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY y LANGFUSE_HOST, igual que en N0·L3. Sin ellas, el cliente no envía nada y la UI sigue vacía.

Esta sección tiene densidad: mezcla el bucle de sesiones, la propagación de session_id/user_id y el flush final. Lee el bloque entero una vez para captar la forma, y después la explicación línea a línea. No intentes entenderlo de una pasada.

python
1# generar_dataset.py — produce >=100 trazas reales del agente de Aurora
2import uuid
3from langfuse import get_client, propagate_attributes
4from aurora_agent import responder   # ya instrumentado en N0·L5 con @observe
5
6langfuse = get_client()
7
8# Conversaciones de prueba: cada una es una lista de turnos del cliente.
9# Cubren los caminos del agente: pedido, política (RAG), escalado.
10CONVERSACIONES = [
11    ["¿Dónde está mi pedido AUR-1043?", "¿Y cuándo llega exactamente?"],
12    ["¿Puedo devolver unas zapatillas usadas una vez?", "¿Quién paga el envío de vuelta?"],
13    ["Llevo 3 emails sin respuesta, esto es indignante", "Quiero hablar con una persona"],
14    ["¿Hacéis envíos a Canarias?", "¿Cuánto tarda?"],
15    ["Mi pedido AUR-2210 llegó roto", "¿Me devolvéis el dinero o lo cambiáis?"],
16    # ... amplía hasta >=50 conversaciones (>=100 turnos => >=100 trazas)
17]
18
19def correr_sesion(turnos: list[str], user_id: str, session_id: str) -> None:
20    historial: list[dict] = []
21    for mensaje in turnos:
22        # Cada turno es su propia traza; propagate_attributes agrupa la conversación.
23        with langfuse.start_as_current_observation(as_type="span", name="turno", input=mensaje) as turno:
24            with propagate_attributes(user_id=user_id, session_id=session_id):
25                respuesta = responder(mensaje, historial)
26            turno.update(output=respuesta)
27        historial.append({"role": "user", "content": mensaje})
28        historial.append({"role": "assistant", "content": respuesta})
29
30def main() -> None:
31    total_trazas = 0
32    for i, turnos in enumerate(CONVERSACIONES):
33        user_id = f"cliente_{i:03d}"
34        session_id = str(uuid.uuid4())   # una session por conversación
35        correr_sesion(turnos, user_id, session_id)
36        total_trazas += len(turnos)
37    langfuse.flush()                     # vacía la cola antes de salir
38    print(f"Generadas {total_trazas} trazas en {len(CONVERSACIONES)} sesiones.")
39
40if __name__ == "__main__":
41    main()

Recórrelo por piezas:

  • CONVERSACIONES define el tráfico. Cada conversación cubre uno de los caminos del agente: estado de pedido (buscar_pedido), consulta de política (consultar_politica, el paso RAG) o cliente enfadado (escalar_a_humano). Variedad de caminos = variedad de fallos visibles después.

  • correr_sesion ejecuta una conversación turno a turno, arrastrando el historial. Reproduce lo que hace un cliente real: el segundo mensaje depende del primero.

  • start_as_current_observation(as_type="span", ...) abre el span del turno; propagate_attributes(user_id=..., session_id=...) es lo que da estructura al dataset. Sin ese context manager, Langfuse registraría trazas sueltas. Con él, los turnos de una misma conversación se agrupan en una session navegable.

Self-explanation — pregúntate y responde antes de seguir: ¿por qué session_id se genera una vez por conversación, fuera del bucle de turnos, y no dentro?

Porque el session_id identifica la conversación entera. Si se generara dentro del bucle, cada turno tendría un id distinto y Langfuse vería tres conversaciones de un turno en vez de una de tres. La session perdería su razón de ser: agrupar los turnos de un mismo cliente.

  • langfuse.flush() fuerza el envío de las trazas en cola antes de que el proceso termine. El SDK envía en lotes de forma asíncrona; sin flush(), un script corto puede salir dejando trazas sin enviar. Es el error nº1 al generar datasets en batch: el script imprime "100 trazas" pero en la UI aparecen 60.

Verificar la navegabilidad

Generar no es suficiente. Tienes que comprobar que el dataset es explotable. Abre Langfuse y verifica, no asumas:

  1. Cuenta. ¿Hay ≥100 trazas? El contador de la UI debe casar con lo que imprimió el script. Si no casa, sospecha del flush().
  2. Entra en una traza al azar. ¿Ves el árbol retrieval → LLM → tool, anidado, sin spans colgando de la raíz?
  3. Filtra por una session. ¿Aparecen los turnos de esa conversación juntos, en orden?
  4. Mira los campos de un span LLM. ¿Están coste, tokens, latencia y finish_reason?

Si las cuatro pasan, tu dataset es navegable. Acabas de producir el entregable del checkpoint C0.


6.6Hazlo tú: el checkpoint C0

Esta práctica es el checkpoint C0. No es un ejercicio de calentamiento: es la tarea auténtica que cierra el nivel.

Enunciado del checkpoint C0

Instrumenta el agente de Aurora con Langfuse de modo que cada request produzca una traza con spans anidados (retrieval → llamadas al LLM → tool calls), capturando los campos críticos, con user_id y session_id.

Entregable: un export de ≥100 trazas reales (sesiones simuladas) navegable, donde puedas reconstruir qué pasó en cualquier request.

El grueso de la instrumentación ya lo hiciste en N0·L1 a L5. Esta lección añade la última pieza: el volumen. Adapta el script de §6.5 a tu agente, amplía CONVERSACIONES hasta superar las 100 trazas, córrelo y verifica.

Elaborative interrogation

Antes de evaluarte, responde. Escribir la respuesta antes de leerla fija mejor el razonamiento. ¿Por qué la rúbrica exige cero spans huérfanos, y no se conforma con "que estén todos los spans"?

Porque un span presente pero huérfano destruye la causalidad. En el error analysis (N1) necesitas saber si el tool call vino antes o después del retrieval, y de qué llamada dependió. Un span que cuelga de la raíz existe, pero ha perdido su relación con los demás. La jerarquía no es decoración: es la información de qué causó qué (corpus B.6).

La rúbrica C0 — cinco dimensiones

Aprobado = ✔ en las dimensiones 1, 2 y 3 + al menos una de las dimensiones 4 o 5. Evalúa tu dataset contra cada una con honestidad.

1. Cobertura — Todos los pasos relevantes (retrieval, LLM, tools) tienen su span.

2. Jerarquía — Parent-child correcto; cero spans huérfanos en la raíz.

3. Campos críticos — Coste, latencia, tokens, finish_reason y errores presentes y correctos en ≥90% de los spans.

4. Sesiónuser_id y session_id presentes; las conversaciones multi-turno son reconstruibles.

5. Reproducibilidad — El dataset de trazas se puede re-generar.


6.7Comprueba: feedback formativo por dimensión

Evalúa tu entregable. Para cada dimensión, busca tu situación y aplica el feedback. Está escrito como te lo daría un mentor que revisa tu trabajo: qué hiciste bien y por qué importa, qué falta y cuál es el siguiente paso.

Dimensión 1 — Cobertura.

  • Si cada request muestra retrieval, LLM y tool con su span: has cubierto el camino completo del agente, y eso importa porque en N1 podrás localizar el fallo en el paso exacto donde ocurrió. La brecha a vigilar: revisa que el span de retrieval capture la query y los docs recuperados, no solo "que exista". Siguiente paso: abre tres trazas con tool call y confirma que el retrieval previo está registrado.
  • Si algún paso no tiene span: tu cobertura produce trazas con huecos donde el análisis quedará ciego. Compara una traza tuya con el árbol esperado de N0·L5 — ¿qué paso del bucle responder() no abre su span? Añade el @observe o el context manager que falta en esa función.

Dimensión 2 — Jerarquía.

  • Si no hay spans colgando de la raíz salvo el root: tu jerarquía preserva la causalidad, y eso es lo que hará legible el error analysis. Siguiente paso: confirma el caso multi-turno — que cada turno sea su propia traza dentro de la session.
  • Si encuentras tool calls colgando de la raíz: tu traza muestra los pasos pero no su relación; produce un árbol plano donde la esperada produce un árbol anidado. ¿Qué diferencia ves en el parentObservationId de ese span frente a uno bien anidado? Revisa el bug del span huérfano de N0·L5: el padre está en null.

Dimensión 3 — Campos críticos.

  • Si ≥90% de tus spans LLM llevan coste, tokens, latencia y finish_reason: tus trazas son interrogables, no solo visibles — podrás filtrar y contar por estos campos en N1. Siguiente paso: verifica que el coste no sea cero por una tabla de precios mal configurada.
  • Si faltan campos en muchos spans: tu dataset se ve pero no se puede interrogar por coste o por motivo de corte. Mira un span pobre frente a uno completo de N0·L4 — ¿qué campos enriqueció @observe allí que aquí no aparecen? Añade la captura que falta antes de re-generar.

Dimensión 4 — Sesión.

  • Si las conversaciones multi-turno se reconstruyen por session_id: preservas el hilo conversacional, clave porque muchos fallos del turno N nacen en el turno 1. Siguiente paso: filtra por un user_id y confirma que ves todas sus sesiones.
  • Si los turnos aparecen sueltos: tienes volumen pero pierdes el hilo de cada conversación. Revisa dónde generas el session_id: ¿está fuera del bucle de turnos, una vez por conversación? (ver §6.5).

Dimensión 5 — Reproducibilidad.

  • Si tu dataset nace de un script que cualquiera puede correr: tienes un activo de ingeniería, no una foto irrepetible — podrás re-generar trazas cuando cambies el agente en niveles posteriores. Siguiente paso: documenta en un README cómo correrlo y qué versión del agente usa.
  • Si lo generaste con clics manuales: tu dataset existe pero no se puede rehacer. El siguiente paso es portar esos pasos al script de §6.5; sin reproducibilidad, cada cambio del agente te obliga a empezar de cero.

Si no llegas al umbral (✔ en 1, 2 y 3 + una de 4/5), no es un fracaso: es información sobre qué pieza de N0 reforzar. Vuelve a la lección que cubre esa dimensión, corrige y re-genera. El gate mide tu dataset, no tu aptitud.


6.8Conecta: cierras N0, abres N1

Vuelve al tuit del cliente de Aurora que abrió este nivel en N0·L1. El bot le prometió un reembolso que no existía. Al abrir el código, no había forma de saber qué había pasado. Tenías vibes, no datos.

Ahora la situación es otra. Si ese fallo ocurriera hoy, tu agente instrumentado habría dejado una traza. Verías si recuperó la política equivocada, si el LLM inventó el reembolso, qué finish_reason tuvo, cuánto costó. Ya podrías diagnosticarlo. Eso es lo que cambia entre el inicio de N0 y este punto.

Y no tienes esa capacidad para una request, sino para ≥100. Eso te lleva al umbral del próximo nivel.

N1 empieza con una instrucción de tres palabras de Hamel Husain: "look at your data" (corpus A.2). Abrirás tu dataset y leerás las trazas una a una, anotando el primer fallo de cada una. De ese acto cualitativo —no de una métrica genérica— saldrán los modos de fallo reales del agente. El dataset que produces hoy es la entrada literal de C1.

¿Dónde aplicas esto en tu trabajo? Cualquier app LLM que ya tengas en producción es candidata. Antes de añadir una métrica de "calidad", instrumenta el tráfico real y junta ≥100 trazas. El orden importa: primero ves, luego mides.


6.9Reflexiona

Tómate dos minutos. La metacognición —pensar sobre cómo aprendes— tiene efecto sostenido en el rendimiento; no te saltes esto.

  • ¿Qué aprendiste? Resume en una frase qué hace que un dataset de trazas sea explotable frente a un montón de logs.
  • ¿Qué sigue sin estar claro? ¿Te costó la propagación del session_id? ¿El flush()? ¿La diferencia entre cobertura y jerarquía? Nómbralo: ese es tu punto de repaso antes de N1.
  • ¿Qué harías distinto? Si volvieras a instrumentar el agente desde cero, ¿qué campo capturarías desde el primer span en vez de añadirlo al final?

Referencia rápida

Dataset de trazas explotable — las 4 propiedades:

  1. Volumen — ≥100 trazas — p.ej. ≥50 conversaciones de 2 turnos. (Husain recomienda mirar 50–100 conversaciones como punto de partida del error analysis.)
  2. Sesionessession_id + user_id; multi-turno reconstruible.
  3. Campos críticos — input/output, tokens, coste, latencia, modelo, finish_reason, errores.
  4. Reproducibilidad — re-generable por script.

Generar el dataset (esqueleto):

python
1from langfuse import get_client, propagate_attributes
2langfuse = get_client()
3
4session_id = str(uuid.uuid4())          # una vez por conversación, FUERA del bucle
5with langfuse.start_as_current_observation(as_type="span", name="turno", input=mensaje) as turno:
6    with propagate_attributes(user_id=user_id, session_id=session_id):
7        respuesta = responder(mensaje, historial)
8    turno.update(output=respuesta)
9langfuse.flush()                        # imprescindible en scripts batch

Verificar navegabilidad: cuenta ≥100 · árbol anidado sin huérfanos · session agrupa turnos · span LLM con campos.

Rúbrica C0 (aprobado = 1+2+3 ✔ y una de 4/5): Cobertura · Jerarquía (0 huérfanos) · Campos (≥90%) · Sesión · Reproducibilidad.

Lo que viene en N1: "look at your data" — open coding sobre estas trazas, anotando el primer fallo de cada una.