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

Qué capturar y por qué (y qué no)

Objetivo de maestría

decidirás qué registrar en cada span de un sistema LLM (tokens, coste, latencia, TTFT, finish_reason, errores, metadata) y qué dejar fuera por privacidad y coste. Sin los campos correctos, una traza es bonita pero inútil: no responde "cuánto costó" ni "por qué se cortó".


4.1El problema

Vuelve el fallo de Aurora. Un cliente escribe al agente de soporte y la respuesta llega a medias: el texto se corta en mitad de una frase sobre devoluciones. En L3 instrumentaste responder() con Langfuse, así que esta vez sí tienes una traza. Abres la traza en la UI, esperando una respuesta.

No la tienes. Ves que hubo una llamada al modelo y que produjo texto. Pero no puedes contestar las dos preguntas que importan:

  • ¿Cuánto costó esta request? No hay tokens ni coste registrados.
  • ¿Por qué se cortó la respuesta? No hay finish_reason. ¿Llegó al límite de tokens? ¿Fue un error? ¿El modelo decidió parar? La traza calla.

Capturaste que algo pasó, pero no qué. Tienes un span vacío de significado. El problema de esta lección es ese: una traza solo es explotable si lleva los campos correctos. Esos campos convierten "ocurrió una llamada" en "ocurrió esta llamada, costó esto, terminó así".


4.2Qué vas a poder hacer

Al terminar podrás:

  • Enumerar los campos mínimos de un span LLM y de un span de tool, y explicar qué pregunta responde cada uno.
  • Enriquecer un span pobre con tokens, coste, latencia, TTFT, finish_reason, errores y metadata (user_id, release).
  • Mapear atributos de las convenciones GenAI de OpenTelemetry (gen_ai.*) a esos campos, y reconocer cuáles son Opt-In.
  • Justificar qué NO capturar —contenido de prompts y respuestas— por privacidad y coste, sin dejar la traza ciega.

Necesitas saber antes: instrumentar una función con el SDK de Langfuse y @observe (N0·L3), y el modelo trace → span → observation → session (N0·L2). Si esos no están sólidos, repásalos antes de seguir.


4.3Recupera

Antes de leer, responde de memoria. En N0·L3 instrumentaste responder() con el decorador @observe().

  1. ¿Qué capturó @observe automáticamente, sin que tú pasaras nada a mano?
  2. De esos campos automáticos, ¿alguno te dice cuánto costó la llamada o por qué terminó?
  3. En N0·L2 listaste los campos que definen un span. ¿Cuántos puedes nombrar ahora?

Pista para la 1: según el corpus de L3, @observe() captura input, output, timings y errores vía context propagation de OpenTelemetry. Eso es la base. Para la 2, fíjate en que ni "coste" ni "razón de terminación" aparecen en esa lista. Ahí está el hueco que esta lección cierra.


4.4El concepto

Los campos mínimos de un span LLM

Empecemos por lo concreto: la lista de campos que una traza necesita para ser explotable. Según la documentación de Langfuse, un span de generación —la unidad que representa una llamada al modelo— captura como mínimo estos campos:

CampoQué pregunta responde
input¿Qué se le mandó al modelo?
output¿Qué devolvió?
tokens in / tokens out¿Cuánto consumió de entrada y de salida?
coste¿Cuánto costó esta llamada?
latencia¿Cuánto tardó la llamada entera?
modelo + versión¿Qué modelo y qué versión la atendió?
finish_reason¿Por qué terminó? (fin natural, límite, error)
errores¿Falló? ¿Con qué?
session_id¿A qué conversación pertenece este span?

Para un span de tool —el que representa una llamada a una herramienta de Aurora— el corpus de Langfuse lista: nombre, args, resultado, latencia y error. Y como contexto de toda la traza: user_id, session_id y metadata (entorno, release, tags).

Cada campo gana su sitio respondiendo una pregunta de diagnóstico. Si un campo no responde ninguna, no lo captures.

TTFT no es lo mismo que latencia total

Dos campos se confunden, y la diferencia importa. La latencia total es lo que tarda la llamada entera, de que la envías a que recibes el último token. El TTFT (time-to-first-token, tiempo hasta el primer token) es lo que tarda en llegar el primer fragmento de la respuesta.

Según la guía de Braintrust, el TTFT se mide aparte de la latencia total, y es clave en streaming. La analogía: la latencia total es cuánto tarda en llegar el paquete completo; el TTFT es cuánto tardas en oír el primer "tic" del cartero. El cliente de Aurora percibe el TTFT —cuándo empieza a ver texto—, no solo el total. La analogía falla en un punto: si no hay streaming, no hay "primer token" separable y TTFT y latencia casi coinciden.

Las convenciones GenAI de OpenTelemetry

Hasta aquí, nombres de Langfuse. Pero ¿hay un estándar? Sí, en construcción. Las convenciones semánticas GenAI de OpenTelemetry (OTel) son un intento de dar nombres comunes a los atributos de un span de IA, bajo el prefijo gen_ai.*. OpenTelemetry es el estándar abierto de telemetría; "semantic conventions" significa "nombres acordados para que dos herramientas distintas hablen el mismo idioma".

Un dato que debes interiorizar y que cambia cómo los usas. A junio de 2026, TODOS los atributos GenAI de OTel están en status Development —ni Stable ni Experimental—. No existe ningún gen_ai.* estable, y no hay fecha comprometida de estabilización (fuente: OpenTelemetry GenAI semantic conventions). Traducción práctica: son útiles, pero pueden cambiar de nombre o forma. No construyas nada que se rompa si un atributo cambia.

OTel clasifica cada atributo por nivel de requisito. Estos son los tres que verás:

  • Required — obligatorio en ese tipo de span. En un span de inferencia, el corpus lista como Required gen_ai.operation.name y gen_ai.provider.name.
  • Recommended — deberías capturarlo. El uso de tokens entra aquí: gen_ai.usage.input_tokens y gen_ai.usage.output_tokens (entre otros del corpus).
  • Opt-In — solo si lo activas explícitamente. Aquí está el contenido: gen_ai.input.messages, gen_ai.output.messages. OTel lo marca Opt-In por privacidad y coste.

Hay un caso intermedio útil: gen_ai.conversation.id es Conditionally Required y agrupa los spans de una conversación. Es el equivalente OTel a tu session_id —el campo que en N0·L2 usabas para enlazar los turnos de una misma conversación—.

Por qué el contenido es Opt-In (el objetivo O0.4)

Aquí está la decisión de diseño de toda la lección. Capturar el contenido completo —los prompts que envías y las respuestas íntegras del modelo— es la opción más golosa: lo ves todo. Y por eso OTel lo deja fuera por defecto. Es Opt-In por dos razones:

  • Privacidad. Los prompts de Aurora llevan datos del cliente: nombre, número de pedido, a veces dirección. Volcar eso a tu sistema de trazas lo convierte en un almacén de datos personales, con todas las obligaciones que eso implica.
  • Coste. El contenido es voluminoso. Guardar cada prompt y cada respuesta de cada request multiplica el almacenamiento y el coste de la plataforma de observabilidad.

Es común pensar que "más captura es siempre mejor". No lo es. La diferencia clave: capturar metadata estructurada (tokens, coste, finish_reason, latencia) es barato y casi nunca sensible; capturar contenido crudo es caro y casi siempre sensible. El criterio del objetivo O0.4 de este curso: registra lo que te deja diagnosticar sin inflar coste ni exponer datos. El contenido se activa con intención —por ejemplo, en un entorno de desarrollo o con un muestreo pequeño—, no por defecto en producción.


4.5Míralo funcionar

Volvemos a la traza vacía de Aurora. La vamos a enriquecer. Este código asume que ya tienes la llamada al modelo instrumentada de L3; aquí añadimos los campos que faltaban.

Antes de leer línea a línea: el patrón es siempre el mismo —tomas el objeto observación actual y le adjuntas datos con update—. Lee el bloque entero una vez, luego volvemos sobre los puntos clave.

python
1from langfuse import observe, get_client, propagate_attributes
2
3langfuse = get_client()
4
5@observe(as_type="generation")
6def llamar_modelo(mensajes: list[dict]) -> dict:
7    # 1. Llamada real al modelo (Anthropic SDK), en streaming para medir TTFT.
8    respuesta = client.messages.create(
9        model="claude-...",          # modelo + versión: responde "¿quién la atendió?"
10        messages=mensajes,
11    )
12
13    # 2. Enriquecer el span con los campos que faltaban en la traza vacía.
14    langfuse.update_current_generation(
15        usage_details={
16            "input_tokens": respuesta.usage.input_tokens,    # tokens in
17            "output_tokens": respuesta.usage.output_tokens,  # tokens out
18        },
19        # 'metadata' es dict[str, str]; cada valor ≤ 200 chars (límite del SDK).
20        metadata={
21            "finish_reason": respuesta.stop_reason,   # por qué terminó: el campo que faltaba
22            "release": "aurora-2026.06",              # qué versión del agente respondió
23        },
24    )
25
26    return respuesta
27
28# 3. Contexto de la traza: a quién pertenece y a qué sesión. Se propaga
29#    envolviendo la llamada con el context manager, no dentro del span.
30def responder(mensajes: list[dict], user_id: str, session_id: str) -> dict:
31    with propagate_attributes(user_id=user_id, session_id=session_id):
32        return llamar_modelo(mensajes)

Tres puntos merecen self-explanation. Respóndelos antes de seguir.

  • ¿Por qué usage_details con tokens in y out por separado? Porque el coste no es un número que llega del modelo: se deriva de los tokens y de la tarifa del modelo. Con tokens in/out y el nombre del modelo, la plataforma calcula el coste. Sin tokens, la pregunta "¿cuánto costó?" no tiene respuesta.
  • ¿Por qué finish_reason resuelve el fallo de apertura? Porque distingue causas que se ven iguales en el output. Una respuesta cortada por límite de tokens, una cortada por error y una que terminó de forma natural producen texto incompleto similar, pero finish_reason te dice cuál fue. Ese era el agujero del span vacío.
  • ¿Por qué user_id y session_id se propagan con propagate_attributes, fuera del span? Porque pertenecen a la request entera, no a una llamada concreta. En N0·L2 viste la jerarquía: lo que aplica a toda la conversación vive arriba. Por eso el context manager envuelve la llamada y los inyecta en la traza, no en la generación. El SDK limita user_id y session_id a 200 caracteres.

Fíjate en lo que NO está en el código: el contenido crudo del prompt del cliente. Pasamos mensajes al modelo, pero no lo volcamos a la traza como contenido capturado. Esa es la decisión O0.4 en acción.

El trade-off de capturar prompts completos

Imagina que añades el contenido íntegro. Ganas poder leer la conversación exacta al depurar. Pagas tres cosas:

  • Privacidad: cada traza pasa a contener datos personales del cliente de Aurora.
  • Coste: el volumen de almacenamiento por traza se dispara.
  • Ruido: las trazas pesan más y la UI carga más lento.

OTel resuelve este trade-off por ti con el status Opt-In: no captura contenido salvo que lo pidas. La pregunta no es "¿capturo contenido?" sino "¿en qué entorno y a qué tasa de muestreo me compensa el riesgo?". En desarrollo, con datos de prueba: sí. En producción, con datos reales y a escala: rara vez por defecto.


4.6Hazlo tú

Práctica con andamiaje decreciente. Trabajas sobre un span pobre de Aurora —solo input y output, como la traza de apertura—.

Parte A — guiada (rellena 2 huecos)

Aurora tiene dos fallos que diagnosticar. Para cada uno, di qué campo añadirías al span y por qué ese y no otro.

  • Fallo 1: "El coste mensual del agente se ha disparado y no sé qué requests lo causan." Campo a añadir: ________. Por qué: ________.
  • Fallo 2: "Algunas respuestas llegan truncadas; necesito saber si es límite de tokens o error del proveedor." Campo a añadir: ________. Por qué: ________.

Antes de mirar la respuesta, responde esta elaborative interrogation: ¿por qué finish_reason distingue dos causas que el output por sí solo no distingue?

Respuesta
  • Fallo 1 → tokens in/out (de los que se deriva el coste). Con tokens por request y el modelo, la plataforma calcula el coste de cada una; puedes ordenar y encontrar las caras. El coste no llega del modelo, se calcula desde los tokens.
  • Fallo 2 → finish_reason. Es el único campo que separa "se cortó por límite" de "se cortó por error" cuando el output truncado se ve igual en ambos casos. La interrogación: el output solo te muestra el resultado, no la causa; finish_reason es el campo de causa.

Parte B — autónoma

Estás a punto de instrumentar Aurora en producción con tráfico de clientes reales. Tu jefe pide "captúralo todo, incluidos los prompts completos, por si acaso".

Escribe 3-4 frases que justifiquen qué NO capturar y por qué, citando privacidad y coste. Propón una alternativa concreta para no quedarte ciego.

Una respuesta defendible

No capturaría el contenido crudo de prompts y respuestas por defecto en producción. Razón: los prompts de Aurora contienen datos personales del cliente (privacidad) y el contenido infla el almacenamiento de cada traza (coste); por eso OTel lo marca Opt-In. Alternativa: capturo metadata estructurada en el 100% de las trazas —tokens, coste, finish_reason, latencia, user_id—. Es barata y casi nunca sensible. Activo la captura de contenido solo en un muestreo pequeño o en desarrollo. Así diagnostico sin convertir el sistema de trazas en un almacén de datos personales.


4.7Comprueba

Sin pistas. Mapea cada atributo gen_ai.* a su campo y marca su nivel de requisito. Apruebas con 4 de 5.

Para cada fila, indica dos cosas. (a) A qué campo de la tabla de 4.4 corresponde. (b) Si es Required, Recommended u Opt-In según el corpus de OTel.

  1. gen_ai.usage.input_tokens
  2. gen_ai.output.messages
  3. gen_ai.operation.name
  4. gen_ai.usage.output_tokens
  5. gen_ai.conversation.id
Solución y feedback
  1. gen_ai.usage.input_tokenstokens in · Recommended.
  2. gen_ai.output.messagescontenido (output) · Opt-In (privacidad/coste).
  3. gen_ai.operation.name → identifica la operación del span · Required en inferencia.
  4. gen_ai.usage.output_tokenstokens out · Recommended.
  5. gen_ai.conversation.idsession_id · Conditionally Required.

Feedback formativo. Si acertaste el nivel de los de contenido (el 2) como Opt-In, has captado lo que más importa de la lección: esa marca es la que protege privacidad y coste, y es el corazón del objetivo O0.4. Brecha frecuente: marcar los tokens como Required. No lo son —son Recommended—; lo Required en inferencia es la operación y el proveedor. Siguiente paso: si fallaste el 5, vuelve a N0·L2 y enlaza session_id con gen_ai.conversation.id mentalmente; es el mismo concepto con dos nombres.

Recuerda el matiz de junio de 2026: aunque distingas Required de Opt-In, ninguno de estos atributos es Stable todavía. Todos están en status Development.


4.8Conecta

Acabas de cerrar el agujero de la traza de apertura. La respuesta cortada de Aurora ya no es un misterio: con finish_reason sabes si fue límite o error. Con tokens y coste sabes cuánto te cuesta cada conversación.

Con los campos correctos decididos, en N0·L5 instrumentas el agente de Aurora entero: no una llamada suelta, sino retrieval → LLM → tool anidados en una jerarquía que respeta la causalidad. Y en el Nivel 1 estos campos dejan de ser decoración. El error analysis se hace contando sobre ellos: cuántas trazas tienen finish_reason de error, cuánto cuesta el modo de fallo nº1. Son la materia prima del checkpoint C0, cuya rúbrica exige coste, latencia, tokens, finish_reason y errores presentes en ≥90% de los spans.

En tu trabajo: la próxima vez que instrumentes una app LLM, antes de capturar nada hazte la pregunta de esta lección. ¿Qué fallo concreto quiero poder diagnosticar? Captura los campos que lo responden. Y antes de volcar contenido crudo, pregunta por privacidad y coste.


4.9Reflexiona

Tómate un minuto, en serio. La metacognición es lo que convierte una lección leída en una capacidad tuya.

  • ¿Qué campo te costó más entender por qué importa? ¿finish_reason, TTFT, la diferencia entre Recommended y Opt-In?
  • El status Development de los gen_ai.* a junio de 2026: ¿cambia cómo confiarías en esos nombres en un proyecto real? ¿Cómo?
  • Si tu jefe insistiera en "captúralo todo", ¿qué argumento de esta lección usarías para defender el muestreo de contenido?

Esto requiere práctica. La intuición de "qué capturar" se afina mirando trazas reales y echando en falta campos —que es exactamente lo que harás a partir de L5—.


Referencia rápida

Campos mínimos de un span LLM (Langfuse): input · output · tokens in/out · coste · latencia · modelo+versión · finish_reason · errores. Span de tool: nombre · args · resultado · latencia · error. Contexto de traza: user_id · session_id · metadata (env/release/tags).

TTFT vs latencia total: TTFT = hasta el primer token (clave en streaming); latencia total = la llamada entera. Se miden aparte.

OTel GenAI (gen_ai.*), junio 2026 — TODOS en status Development (ninguno Stable):

  • Required (inferencia): gen_ai.operation.name, gen_ai.provider.name.
  • Recommended: gen_ai.usage.input_tokens, gen_ai.usage.output_tokens.
  • Conditionally Required: gen_ai.conversation.id (= session_id).
  • Opt-In (privacidad/coste): gen_ai.input.messages, gen_ai.output.messages (= contenido crudo).

Regla O0.4: captura metadata estructurada en el 100% (barata, no sensible); el contenido crudo, Opt-In y bajo muestreo.

Fuentes del corpus: OpenTelemetry GenAI semantic conventions · Langfuse (data-model, overview) · Braintrust (TTFT).