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

Anatomía de una traza: trace, span, observation, session

Objetivo de maestría

modelarás la jerarquía trace → span → observation → session y sabrás qué representa cada nivel en un sistema LLM. Con ese modelo mental, podrás mapear cualquier request del agente a una estructura que se inspecciona paso a paso.


2.1El problema

En la lección anterior viste el tuit: el agente de Aurora le prometió a un cliente un reembolso que no existe. Abriste el código y no tenías datos, tenías vibes.

Ahora ya sabes que necesitas una traza. Pero esa conversación no fue un paso: fue cuatro pasos internos encadenados.

text
1Cliente: "Mi pedido AUR-7782 no llega y quiero el reembolso, ¿procede?"
2
3  1. consultar_politica("reembolsos")   → recupera de la KB
4  2. (llamada al LLM con esa política)   → genera el plan
5  3. buscar_pedido("AUR-7782")           → estado real del envío
6  4. (llamada al LLM)                    → redacta la respuesta final

El fallo pudo estar en cualquiera de los cuatro. ¿Recuperó la política equivocada? ¿El modelo ignoró la política correcta? ¿Inventó el estado del pedido en lugar de usar el de buscar_pedido?

Un único registro plano ("entró X, salió Y") no te deja distinguirlos. Necesitas representar esos cuatro pasos como una estructura inspeccionable, paso a paso. Esa estructura tiene un nombre y una anatomía. Vamos a diseccionarla.


2.2Qué vas a poder hacer

Al terminar esta lección podrás:

  • Nombrar los cuatro niveles del modelo: trace, span, observation y session.
  • Distinguir qué representa cada uno en una app LLM (no en una web normal).
  • Dibujar la jerarquía de una request del agente de Aurora y asignar el padre correcto a cada span.

Necesitas saber antes:

  • Qué es una traza "explotable" y por qué la observabilidad va primero — lo viste en N0·L1 De vibes a datos.
  • El agente de Aurora y sus tres herramientas (buscar_pedido, consultar_politica, escalar_a_humano) — el sistema que instrumentas en todo el nivel.

No instrumentamos nada todavía. Esta lección construye el modelo mental; el código llega en L3.


2.3Recupera

Antes de seguir, responde para ti (en voz alta o por escrito). Si una pregunta te cuesta, es justo lo que esta lección viene a fijar.

  1. Si has tocado tracing en sistemas normales (APM, Application Performance Monitoring: las herramientas que cronometran una petición web a través de sus servicios), ¿qué es un span ahí? ¿Qué información lleva?
  2. En una petición web típica, un span suele medir una llamada a un servicio (a la base de datos, a otra API). ¿Bastaría con eso para entender por qué el modelo de Aurora eligió una política y no otra?
  3. ¿Qué pregunta concreta sobre el fallo del reembolso querrías poder responder mirando la traza?

Pista para la 2: el tracing clásico te dice cuánto tardó y si hubo error. Rara vez te dice qué texto entró al modelo ni qué documentos se recuperaron. Ese es su límite, y el motivo de la pieza nueva de esta lección: la observation.


2.4El concepto

Vamos a definir los cuatro términos de menor a mayor, anclados al ejemplo del reembolso. Cada uno es un contenedor del anterior.

Span — la unidad de trabajo

Un span es la unidad mínima trazable: representa una operación con principio y fin. Lleva un span_id propio, el trace_id al que pertenece, un parent_span_id (quién lo contiene), marcas de tiempo, estado, atributos y eventos. La fuente: Datadog LLM Observability — terms.

Analogía: un span es como una línea con sangría en un recibo desglosado. Dice qué se hizo, cuánto duró y bajo qué partida cuelga. La analogía falla en un punto: un recibo es plano, y los spans se anidan en un árbol de profundidad arbitraria.

En el ejemplo de Aurora, "recuperar la política de reembolsos" es un span. "Llamar al LLM" es otro. Cada uno tiene su propia duración y su propio resultado.

parent_span_id — el campo que construye el árbol

El campo que define la jerarquía es parent_span_id. Apunta al span que contiene a este. El span sin padre (parent_span_id vacío) es el span raíz: el punto de entrada, la request entera. Todos los demás cuelgan de él, directa o indirectamente. (Datadog LLM Observability — terms.)

Recuerda este campo. En L5 verás el fallo número uno al instrumentar a mano: un span con padre nulo se cuelga de la raíz y rompe el árbol. Por ahora te vale con saber qué campo manda.

Trace — el ciclo de vida de una request

Un trace (traza) es el ciclo de vida completo de una request: un trace_id y el contenedor de todos sus spans. (Datadog LLM Observability — terms.)

En Aurora, una pregunta del cliente — "¿procede el reembolso de AUR-7782?" — genera un trace. Dentro viven los cuatro spans del paso a paso. El trace es el árbol; los spans son sus nodos.

Observation — el span, pero con sabor LLM (término Langfuse)

Aquí aparece la pieza que el tracing clásico no tiene. En Langfuse, una observation es el término que engloba las operaciones de un trace, y se especializa en tres tipos según qué operación sea:

  • generation — una llamada al LLM (lleva prompt, respuesta, tokens, modelo).
  • tool — una llamada a herramienta (lleva nombre, argumentos, resultado).
  • retrieval — una recuperación de la base de conocimiento (lleva la query y los documentos).

Las observations son anidables, igual que los spans. Fuente: Langfuse — data model.

Por qué importa la distinción: el span de APM clásico te da duración y estado. La observation de tipo generation te da, además, el prompt exacto y la respuesta del modelo. La de tipo retrieval te da los documentos recuperados. Sin ese contenido, "el modelo recuperó la política equivocada" es invisible. Con él, lo ves.

Trata "observation" y "span con tipo LLM" como sinónimos prácticos en este curso. Langfuse usa "observation"; OTel y la mayoría de herramientas APM usan "span" como término base, y cada plataforma añade sus propios tipos de span encima. La idea es la misma: la unidad de trabajo, enriquecida para sistemas LLM.

Session — la conversación entera

Un session (sesión) agrupa varios traces de una misma conversación multi-turno. Si el cliente escribe tres mensajes seguidos al agente, son tres traces distintos, pero una sola sesión. Fuente: getmaxim.ai, corroborado por la jerarquía de Langfuse.

La jerarquía completa, de mayor a menor:

text
1Session   →  toda la conversación (varios turnos)
2  Trace   →  una request (un turno: "¿procede el reembolso?")
3    Span / observation  →  un paso dentro del turno (retrieval, LLM, tool)

Léelo así: Sessions > Traces > Spans.

Un matiz de OpenTelemetry: session no siempre es una entidad

Cuidado con un detalle que confunde al saltar entre herramientas. En OpenTelemetry (OTel, el estándar abierto de telemetría) puro, la sesión no es una entidad de primer nivel como el trace. Es un atributo: gen_ai.conversation.id, una etiqueta que pones en los spans para marcar a qué conversación pertenecen. Fuente: OpenTelemetry — GenAI semantic conventions.

La diferencia práctica: en Langfuse navegas "la sesión" como un objeto. En OTel puro, "la sesión" es el conjunto de spans que comparten el mismo gen_ai.conversation.id. Mismo concepto, distinta mecánica. Lo retomas en L4 al mapear los atributos gen_ai.*.


2.5Míralo funcionar

Vamos a dibujar el trace completo de la request del reembolso de Aurora. Antes de leer la anotación línea a línea, mira el árbol entero: fíjate en la sangría, que es la jerarquía hecha visible.

text
1[TRACE] reembolso AUR-7782                 ← span RAÍZ (parent: ninguno)
2│  trace_id: t-9f3a   ·  session_id: s-aurora-0412
3│  input: "Mi pedido AUR-7782 no llega y quiero el reembolso"
4│  output: "Tu pedido salió ayer; aún no procede reembolso, pero…"
5│  duración: 4.1s
6
7├─ [retrieval] consultar_politica("reembolsos")   ← parent: el RAÍZ
8│     query: "política de reembolsos"
9│     docs: ["politica_devoluciones_v3 #chunk2 (score 0.81)"]
10│     duración: 0.3s
11
12├─ [generation] LLM: planificar respuesta        ← parent: el RAÍZ
13│     model: claude · input_tokens: 1240 · output_tokens: 86
14│     finish_reason: tool_use   (el modelo pide llamar a una tool)
15│     duración: 1.2s
16
17├─ [tool] buscar_pedido("AUR-7782")              ← parent: el RAÍZ
18│     args: {"order_id": "AUR-7782"}
19│     result: {"estado": "en_transito", "envio": "2026-06-10"}
20│     duración: 0.2s
21
22└─ [generation] LLM: redactar respuesta final    ← parent: el RAÍZ
23      model: claude · input_tokens: 1410 · output_tokens: 132
24      finish_reason: end_turn
25      duración: 1.9s

Una nota sobre el nombre: finish_reason es el campo en la API de Anthropic/OpenAI; en OpenTelemetry el atributo equivalente es gen_ai.response.finish_reasons (lo verás en L4).

Léelo de arriba abajo. Cada decisión del diseño responde a una pregunta del análisis:

  • El span raíz es la request entera. Su input/output es lo que ve el cliente. Su session_id (s-aurora-0412) conecta este turno con los demás de la misma conversación. Ese session_id no aparece solo: lo pasa el desarrollador al instrumentar (lo verás en L3). Por ahora basta con saber qué agrupa. Pregúntate: ¿por qué el coste y la duración total cuelgan aquí y no en un span hijo? Porque la raíz agrega; los hijos detallan.
  • El retrieval va primero y guarda los docs con su score. Aquí verías si recuperó politica_devoluciones_v3 (correcta) o una de envíos (incorrecta). Sin ese campo, el fallo "recuperó la política equivocada" sería invisible.
  • El primer generation tiene finish_reason: tool_use. Eso significa que el modelo no cerró la respuesta: pidió ejecutar una herramienta. Por eso el árbol continúa con un span tool.
  • El tool muestra estado: en_transito. El pedido existe y va en camino. Si la respuesta final hubiera prometido un reembolso, sabrías que el fallo está en el último generation, no en los datos: el modelo tuvo el estado correcto y aun así redactó mal.

Eso es lo que un registro plano nunca te daría: localizar el paso culpable. Aquí los cuatro spans son hermanos, todos hijos directos de la raíz. En L5 verás que el anidamiento puede ser más profundo, pero la lógica del parent_span_id no cambia.


2.6Hazlo tú

Practica con andamiaje decreciente. Primero un caso casi resuelto; luego uno desde cero.

Ejercicio A (guiado) — completa la jerarquía

Un cliente pregunta: "¿Cuánto tarda una devolución?". El agente solo consulta la KB y responde, sin buscar pedido ni escalar. Completa los parent que faltan.

text
1[TRACE] consulta-devoluciones          ← span raíz, parent: ________
2├─ [retrieval] consultar_politica(...)  ← parent: ________
3└─ [generation] LLM: responder          ← parent: ________

Antes de mirar la solución, responde con tus palabras. ¿Por qué el retrieval y el generation comparten el mismo padre en lugar de colgar uno del otro? Justifícalo, no lo memorices.

Solución y razonamiento
  • Raíz → parent: ninguno (es el punto de entrada).
  • retrievalparent: el span raíz.
  • generationparent: el span raíz.

Comparten padre porque son dos pasos secuenciales del mismo turno, no uno dentro de otro. La recuperación termina y después arranca la generación; ninguno ocurre dentro del otro. Anidar el generation dentro del retrieval afirmaría que generar es un sub-paso de recuperar, lo cual es falso. La jerarquía codifica contención, no orden temporal: el orden lo dan las marcas de tiempo.

Ejercicio B (autónomo) — dibuja el trace desde cero

El cliente escribe: "Llevo tres correos sin respuesta, esto es indignante". El agente recupera la política de incidencias, llama al LLM (que pide escalar), ejecuta escalar_a_humano("cliente insatisfecho, 3 contactos") y el LLM redacta la disculpa final.

Dibuja el trace en formato árbol como el de la sección 2.5. Para cada nodo indica: tipo (retrieval / generation / tool), qué representa y su parent. Marca también qué sería el session_id y si este turno y el del reembolso podrían compartir sesión.

Solución
text
1[TRACE] incidencia-sin-respuesta        ← raíz, parent: ninguno
2├─ [retrieval] consultar_politica("incidencias")   ← parent: raíz
3├─ [generation] LLM: evaluar → pide escalar         ← parent: raíz (finish_reason: tool_use)
4├─ [tool] escalar_a_humano("cliente insatisfecho…") ← parent: raíz
5└─ [generation] LLM: redactar disculpa final        ← parent: raíz (finish_reason: end_turn)

session_id: identifica al cliente en esta conversación. Compartiría sesión con el turno del reembolso solo si es el mismo cliente en la misma conversación continuada. Cliente o conversación distintos → sesión distinta. La sesión agrupa turnos de una conversación, no de un cliente a lo largo del tiempo.


2.7Comprueba

Mini-quiz sin pistas. Apunta a acertar 4 de 5 antes de pasar a L3.

  1. ¿Qué campo de un span determina su lugar en la jerarquía?
  2. Verdadero o falso: en OpenTelemetry puro, una sesión es una entidad de primer nivel como el trace.
  3. El cliente envía dos mensajes seguidos a Aurora. ¿Cuántos traces y cuántas sessions hay (como mínimo)?
  4. Tienes un span con finish_reason: tool_use. ¿Qué esperas encontrar como span siguiente y por qué?
  5. ¿Qué tipo de observation guarda los documentos recuperados con su score, y por qué ese campo es decisivo para el error analysis?
Respuestas y feedback
  1. parent_span_id. Si dijiste trace_id, ojo: el trace_id dice a qué request pertenece el span, no quién lo contiene. La jerarquía la construye parent_span_id.
  2. Falso. En OTel puro la sesión es un atributo (gen_ai.conversation.id), no una entidad. En Langfuse sí es un objeto navegable. Es la confusión más común al cambiar de herramienta — tenerla clara te ahorra horas.
  3. Dos traces (uno por turno) y una session (la conversación los agrupa). Si contaste dos sessions, recuerda: Sessions > Traces, no al revés.
  4. Un span de tipo tool. finish_reason: tool_use significa que el modelo no cerró el turno, pidió ejecutar una herramienta; el siguiente paso ejecuta esa tool y devuelve el resultado al modelo.
  5. La de tipo retrieval. Guarda la query y los documentos con su score. Es decisivo porque sin él no puedes distinguir "recuperó el contexto equivocado" de "tuvo el contexto correcto pero el modelo lo ignoró" — dos fallos distintos con arreglos distintos.

Si fallaste la 1 o la 4, vuelve a 2.4 y 2.5: son el núcleo que L3 y L5 dan por sabido. Si fallaste solo la 2 o la 3, tienes el modelo; te faltó el matiz de la sesión, que reforzarás en L4.


2.8Conecta

Ya tienes el modelo mental que el resto del nivel da por hecho:

  • En L3 instrumentas tu primera observation con el SDK de Langfuse: el decorador @observe() captura input, output y tiempos sin que los pases a mano. Lo que ahí veas en la UI es exactamente el árbol que acabas de dibujar.
  • En L5 anidas el agente entero con el parent_span_id correcto y enfrentas el bug del span huérfano. El campo que hoy aprendiste a respetar es el que ahí no se rompe.
  • En el checkpoint C0, una de las cinco dimensiones de la rúbrica es jerarquía correcta, cero spans huérfanos. Hoy aprendiste a leerla; en L5 la produces.

En tu trabajo: este modelo aplica a cualquier app LLM que mantengas, no solo a Aurora. Un chatbot de RAG, un agente con tools, un pipeline multi-paso — todos se representan como trace → spans/observations, agrupados en sessions. La próxima vez que un sistema tuyo "responda raro", la primera pregunta es: ¿puedo ver el árbol de esa request?

Y volviendo al tuit de Aurora: el fallo del reembolso ya no es un misterio opaco. Es un árbol de cuatro nodos, y sabes en cuál mirar primero.


2.9Reflexiona

Cierra con tres preguntas. No las contestes rápido; escribir la respuesta consolida el aprendizaje más que releer.

  • ¿Qué representa cada nivel — session, trace, span/observation — con tus propias palabras y sin mirar la lección?
  • ¿Qué te sigue costando: distinguir tipos de observation, asignar el padre correcto, o el matiz de la sesión en OTel? Nómbralo: eso dirige tu repaso.
  • Antes de esta lección, ¿cómo habrías intentado depurar el fallo del reembolso? ¿Qué harías distinto ahora?

Referencia rápida

text
1JERARQUÍA          Sessions > Traces > Spans/Observations
2
3session   conversación multi-turno
4          · Langfuse: entidad navegable
5          · OTel: atributo gen_ai.conversation.id (NO entidad)
6trace     ciclo de vida de UNA request · trace_id · contiene spans
7span      unidad de trabajo · span_id, trace_id, parent_span_id,
8          timestamps, status, attributes · raíz = parent vacío
9observation  (término Langfuse) span con tipo LLM. Tipos:
10          · generation  → llamada al LLM (prompt, respuesta, tokens, modelo)
11          · tool        → llamada a herramienta (nombre, args, resultado)
12          · retrieval   → recuperación KB (query, docs + score)
13
14CAMPO QUE MANDA   parent_span_id  → construye el árbol
15                  finish_reason: tool_use  → viene un span tool después

Fuentes: Datadog LLM Observability (trace/span/parent_span_id); Langfuse data model (observation: generation/tool/retrieval; Sessions > Traces > Spans); OpenTelemetry GenAI semantic conventions (gen_ai.conversation.id como atributo, no entidad).