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

Instrumentar con Langfuse: del cero al primer trace

Objetivo de maestría

instrumentarás una llamada LLM real con el SDK de Langfuse y verás el primer trace en la UI. Es el primer giro del flywheel: sin captura no hay datos que analizar.


3.1El problema

Tienes el agente de soporte de Aurora corriendo en tu máquina. Responde a clientes, recupera políticas, busca pedidos. También tienes Langfuse self-hosted levantado, esperando trazas.

Entre las dos piezas hay un hueco. El agente produce respuestas; Langfuse no recibe nada. La UI está vacía.

Lo que falta no es una refactorización ni una librería nueva. Faltan unas pocas líneas que conecten la ejecución del agente con el almacén de trazas. Esta lección las escribe contigo.

Al terminar, una request del agente de Aurora aparecerá en Langfuse como un trace que puedes abrir, inspeccionar y leer. Eso es el primer dato explotable del curso.


3.2Qué vas a poder hacer

Al final de esta lección podrás:

  • Instrumentar una función Python que llama a un LLM usando el decorador @observe() del SDK de Langfuse.
  • Ejecutar esa función y localizar su trace en la UI de Langfuse.
  • Nombrar los campos que el SDK captura sin que los pases a mano (input, output, tiempos, errores).
  • Explicar por qué @observe captura entrada y salida sin que tú las registres explícitamente.

Necesitas saber antes:

  • La jerarquía trace → span → observation → session y qué representa cada nivel (N0·L2).
  • Qué campos definen un span (input, output, timestamps, status) — los recuperarás en 3.3 (N0·L2).
  • Python a nivel de funciones y decoradores básicos, y haber llamado alguna vez a una API de LLM (prereq externo del nivel).

No necesitas saber OpenTelemetry por dentro. Lo que importe lo introducimos aquí.


3.3Recupera

Antes de instrumentar, recupera de memoria lo de N0·L2. Responde antes de seguir leyendo; el esfuerzo de recordar fija el aprendizaje.

  1. En N0·L2 definiste un span. Nombra tres campos que lo describen.
  2. ¿Qué relación hay entre un trace y un span? ¿Cuál contiene a cuál?
  3. En Langfuse, ¿qué término engloba las operaciones de un trace y se especializa en generations (LLM), tool calls y retrieval?
Comprueba tu respuesta
  1. Un span lleva, entre otros: span_id, trace_id, parent_span_id, timestamps, status, atributos. Cualquier tres valen.
  2. El trace es el ciclo de vida completo de una request y contiene los spans. El span es la unidad mínima trazable dentro del trace.
  3. Observation. Es el término de Langfuse para las operaciones de un trace; se especializa en generations, tool calls y retrieval, y son anidables.

Si fallaste la 1 o la 3, vuelve a N0·L2 antes de seguir. El resto de la lección asume esos términos.


3.4El concepto

Ya tienes el modelo mental de una traza. Ahora necesitas una herramienta que las produzca y las almacene. En este curso usamos Langfuse.

Langfuse es una plataforma de observabilidad para aplicaciones LLM: captura trazas, las almacena y te las muestra en una UI navegable. Su núcleo (tracing, prompts, evals, playground) está bajo licencia MIT. Puedes ejecutarlo en tu propia infraestructura sin coste de licencia.

Analogía: Langfuse es para un agente LLM lo que una caja negra de vuelo es para un avión. Registra qué pasó en cada momento para que después puedas reconstruirlo. El límite de la analogía: la caja negra solo se lee tras un accidente. Tú leerás las trazas de Langfuse de forma continua, no solo cuando algo falla.

Self-host: dónde viven tus trazas

Langfuse se despliega self-hosted con Docker. La serie v3 corre como seis servicios coordinados: una capa web (puerto :3000), un worker, PostgreSQL, ClickHouse (la base analítica donde viven las trazas), Redis y un almacén de blobs tipo MinIO/S3.

No vas a montar esa arquitectura en esta lección. Para el curso, asume que ya está levantada (la receta de Docker es material aparte). Lo que importa aquí: tienes una URL de Langfuse y un par de claves de API.

El foco: el SDK de Python v4

La pieza que tocas desde tu código es el SDK. Trabajamos con el SDK de Python v4 (2026) de Langfuse.

Una advertencia que vas a agradecer luego. Las versiones de patch cambian a diario. El día que se verificó el corpus, la serie del servidor pasó de un patch al siguiente en una jornada. Por eso aprendes la serie mayor (SDK Python v4), no un número exacto. Fija mentalmente "v4", no "v4.x.y". Tu instalación traerá el patch más reciente de la serie.

El SDK v4 requiere Pydantic v2. Ten esa dependencia en cuenta al preparar el entorno.

El decorador @observe()

La forma más directa de instrumentar una función es el decorador @observe(). Lo pones encima de una función y el SDK envuelve esa ejecución en una observation. Esa observation captura su input, su output, sus tiempos y sus errores.

Decorador, en Python, es una función que envuelve a otra para añadirle comportamiento sin tocar su cuerpo. @observe añade el comportamiento "captura esta ejecución como una observation de Langfuse".

En el SDK v4 conviven dos formas de instrumentar. El decorador @observe() es la vía declarativa, función a función. Junto a él existe langfuse.start_as_current_observation(as_type=...), una vía explícita para casos donde necesitas controlar el inicio y el fin de la observation a mano. Las dos coexisten en v4; en esta lección usamos @observe(). La forma explícita la necesitarás más adelante, al anidar el agente entero (N0·L5).

Qué NO es @observe: no es un sistema de logging. Un log es una línea de texto suelta; @observe produce una observation estructurada, con jerarquía y campos, que la UI sabe leer. Tampoco mide calidad: registra qué pasó, no si estuvo bien. Eso último llega en niveles posteriores.


3.5Míralo funcionar

Vamos a instrumentar la función responder() del agente de Aurora. Es el punto de entrada: recibe el mensaje del cliente y devuelve la respuesta del agente. Antes de tocar código, deja el entorno listo.

3.5.0Antes de instrumentar: deja Langfuse listo

Tres cosas tienen que existir antes de que @observe envíe nada. Prepáralas una vez y olvídate.

Primero, instala el SDK en tu entorno de Python:

bash
1pip install langfuse

Segundo, dale al SDK tres datos por variables de entorno. El SDK las lee al construir el cliente; no van en el código:

  • LANGFUSE_PUBLIC_KEY — la clave pública del proyecto. Identifica a qué proyecto de Langfuse mandas las trazas.
  • LANGFUSE_SECRET_KEY — la clave secreta del mismo proyecto. Autentica el envío; trátala como una contraseña.
  • LANGFUSE_HOST — la URL de tu Langfuse self-hosted (p. ej. http://localhost:3000). Le dice al SDK a dónde enviar.

Las dos claves salen de la UI de Langfuse: en la configuración del proyecto, sección de API keys. Exporta las tres en tu shell:

bash
1export LANGFUSE_PUBLIC_KEY="pk-lf-..."
2export LANGFUSE_SECRET_KEY="sk-lf-..."
3export LANGFUSE_HOST="http://localhost:3000"

O bien escríbelas en un archivo .env y cárgalo antes de ejecutar (con python-dotenv, direnv o tu gestor habitual):

bash
1# .env
2LANGFUSE_PUBLIC_KEY=pk-lf-...
3LANGFUSE_SECRET_KEY=sk-lf-...
4LANGFUSE_HOST=http://localhost:3000

Con el SDK instalado y las tres variables en el entorno, ya puedes instrumentar.

3.5.1El primer trace

Este bloque tiene tres partes (configuración del SDK, el agente, la ejecución). Lee el código entero una vez antes de la explicación línea a línea; así ves la forma general y luego encajas los detalles.

python
1# aurora_instrumentado.py — primer trace del agente de Aurora
2from anthropic import Anthropic
3from langfuse import observe, get_client
4
5client = Anthropic()  # lee ANTHROPIC_API_KEY del entorno
6
7# El SDK de Langfuse lee su URL y sus claves del entorno:
8#   LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY
9# get_client() devuelve el cliente ya configurado desde esas variables.
10langfuse = get_client()
11
12
13def buscar_pedido(order_id: str) -> dict: ...
14def consultar_politica(tema: str) -> str: ...   # paso RAG: recupera de la KB
15def escalar_a_humano(motivo: str) -> str: ...
16
17TOOLS = [...]  # esquemas de las 3 tools para tool-calling
18
19
20@observe()  # <-- estas son las líneas que faltaban
21def responder(mensaje: str, historial: list[dict]) -> str:
22    mensajes = historial + [{"role": "user", "content": mensaje}]
23    respuesta = client.messages.create(
24        model="claude-sonnet-4-6",  # ajusta al modelo de tu cuenta
25        max_tokens=1024,
26        tools=TOOLS,
27        messages=mensajes,
28    )
29    # (en el agente real, aquí va el bucle de tool-use; lo simplificamos
30    #  para ver UN trace limpio. El agente completo se traza en N0·L5.)
31    texto = respuesta.content[0].text
32    return texto
33
34
35if __name__ == "__main__":
36    salida = responder("¿Dónde está mi pedido A-4471?", historial=[])
37    print(salida)
38    langfuse.flush()  # fuerza el envío de las trazas pendientes antes de salir

Ahora línea a línea, en el orden en que importa:

  1. from langfuse import observe, get_client — importas el decorador y el accesor del cliente. El decorador instrumenta; get_client() te da el objeto con el que controlas el envío.

  2. langfuse = get_client() — el SDK construye el cliente leyendo LANGFUSE_HOST y las dos claves del entorno. No pasas la URL ni las claves en el código; viven en variables de entorno, como las claves de la API del modelo.

  3. @observe() sobre responder — esta es la línea que faltaba. A partir de aquí, cada llamada a responder(...) se captura como una observation: su input (los argumentos), su output (lo retornado), sus tiempos y, si lanza, su error.

  4. langfuse.flush() — el SDK acumula trazas y las envía en lotes, por eficiencia. En un script corto que termina enseguida, fuerzas el envío con flush() antes de salir. Sin él, el proceso puede morir con trazas todavía en cola.

Ejecuta:

bash
1python aurora_instrumentado.py

Abre tu Langfuse en el navegador, ve a la sección de Tracing y busca el trace más reciente. Verás una entrada con el nombre responder. Ábrela: dentro está el mensaje del cliente como input y la respuesta del agente como output, con su duración.

Si la UI sigue vacía tras 30 segundos, no es un fallo tuyo de concepto; es lo más común al empezar. Repasa 3.6 (errores frecuentes) antes de dudar de tu código.

Self-explanation

Para fijarlo, explica esto con tus palabras antes de leer la respuesta:

En ningún momento escribiste observation.input = mensaje ni observation.output = texto. ¿Por qué aparecen el input y el output en el trace?

Respuesta

Porque @observe envuelve la ejecución de la función. Captura los argumentos con los que se llamó (mensaje, historial) como input, y el valor de retorno como output. No hace falta que los pases a mano: el decorador los lee de la propia llamada.

El "cómo" por debajo es context propagation de OpenTelemetry (OTel). El SDK abre un contexto al entrar en la función decorada y lo cierra al salir. Ese contexto recoge entrada, salida y tiempos, y sabe a qué trace y a qué observation padre pertenece. Esa misma propagación de contexto, en N0·L5, hará que los spans anidados encuentren a su padre automáticamente.


3.6Hazlo tú

Andamiaje decreciente: primero completas una función casi lista, después instrumentas una desde cero. Antes, una pregunta para razonar.

Elaborative interrogation

Responde antes de mirar la respuesta:

¿Por qué el SDK manda las trazas en lotes en vez de una por una, en el momento? ¿Qué problema evita ese diseño?

Respuesta

Enviar una traza por la red en cada llamada añadiría latencia a cada respuesta del agente y multiplicaría las peticiones HTTP. El SDK las acumula y las envía agrupadas, de forma asíncrona, para no penalizar el camino caliente de la request. El precio de ese diseño: en un script corto debes flush() antes de salir, o el proceso muere con trazas en cola.

Ejercicio 1 — completa los huecos

consultar_politica es el paso RAG del agente: recupera una política de la base de conocimiento. Instrumenta esta función rellenando los dos huecos (___).

python
1from langfuse import ___, get_client   # hueco 1: ¿qué importas para decorar?
2
3langfuse = get_client()
4
5
6___                                    # hueco 2: la línea que instrumenta
7def consultar_politica(tema: str) -> str:
8    chunks = kb.buscar(tema)           # recupera de la KB (ya existe)
9    return "\n".join(chunks)
Solución
python
1from langfuse import observe, get_client   # hueco 1
2
3langfuse = get_client()
4
5
6@observe()                                  # hueco 2
7def consultar_politica(tema: str) -> str:
8    chunks = kb.buscar(tema)
9    return "\n".join(chunks)

El hueco 1 importa observe. El hueco 2 aplica @observe() sobre la función. Tras ejecutarla, su trace mostrará tema como input y el texto unido como output.

Ejercicio 2 — desde cero

Sin plantilla esta vez. Instrumenta buscar_pedido para que cada llamada produzca un trace. Escribe el archivo completo: imports, decorador, la función, y una llamada de prueba con flush().

python
1def buscar_pedido(order_id: str) -> dict:
2    # devuelve estado y tracking del pedido (ya existe)
3    return {"order_id": order_id, "estado": "en_reparto"}
Pauta de autoevaluación

Tu archivo debería tener: el import de observe y get_client; @observe() justo encima de def buscar_pedido; una llamada como buscar_pedido("A-4471"); y langfuse.flush() al final. Tras ejecutar, en la UI debe aparecer un trace buscar_pedido con order_id en el input y el dict en el output. Si el input aparece pero el output no, revisa que la función retorne el valor en vez de imprimirlo.

Errores frecuentes al empezar (normalízalos)

Estos fallos no son señal de que vayas mal. Le pasan a todo el mundo en su primer setup:

  • La UI está vacía. Casi siempre falta flush() en un script corto, o las variables LANGFUSE_HOST / claves no están en el entorno. Comprueba las dos cosas antes de tocar el código.
  • Aparece el trace pero sin output. La función imprime en vez de retornar. @observe captura el valor de retorno, no lo que mandas a stdout.
  • Error de Pydantic al importar. El SDK v4 requiere Pydantic v2. Si tu entorno tiene Pydantic v1, actualízalo.

3.7Comprueba

Sin pistas esta vez. El gate de maestría de esta lección: instrumentar y nombrar lo capturado.

  1. Toma esta función y hazla producir un trace en Langfuse. Escribe el código completo.

    python
    1def resumen_conversacion(historial: list[dict]) -> str:
    2    # condensa el historial en una frase (ya existe)
    3    return condensar(historial)
  2. Tras instrumentarla y ejecutarla, nombra tres campos que aparecerán en su trace en la UI.

  3. En una frase: ¿por qué no tuviste que escribir el input ni el output a mano?

Feedback formativo. Si tu código aplica @observe() sobre la función e incluye flush(), tienes lo esencial: la captura funciona y los datos llegan. La brecha frecuente está en la pregunta 2: nombrar "el nombre de la función" no es un campo capturado de su ejecución. Los tres campos sólidos son input (los argumentos), output (el retorno) y los tiempos (duración/timestamps); el status/error también cuenta. Si nombraste tokens o coste, ojo: esos no aparecen solos con @observe sobre una función cualquiera; decidir y añadir esos campos es justo el trabajo de N0·L4. Siguiente paso: si dudaste en la 3, relee la self-explanation de 3.5.

Gate: para dar por superada la lección, la 1 debe correr y producir trace, y debes nombrar tres campos correctos en la 2.


3.8Conecta

Acabas de cerrar el hueco del problema de apertura. El agente de Aurora y Langfuse ya no están desconectados: una request produce un trace que puedes abrir y leer. Ese es el primer dato explotable del curso, el primer giro del flywheel.

Pero capturaste lo que @observe da por defecto: input, output, tiempos, error. Cuando vuelva el fallo de Aurora, seguirás sin poder calcular cuánto costó la respuesta ni por qué se cortó. Faltan campos. N0·L4 decide qué capturar y por qué (tokens, coste, latencia, finish_reason) y qué dejar fuera por privacidad y coste.

Y por ahora trazaste funciones sueltas. El agente real encadena retrieval → LLM → tool en una jerarquía. N0·L5 anida el agente entero con la forma explícita langfuse.start_as_current_observation(as_type=...) que mencionamos en 3.4, para que la causalidad sea reconstruible sin spans huérfanos.

En tu trabajo: este patrón —decorar el punto de entrada y mirar el trace— es lo primero que harías al heredar cualquier app LLM sin observabilidad. Antes de optimizar nada, hazla visible.


3.9Reflexiona

Tómate dos minutos. La metacognición consolida lo aprendido más que releer.

  • ¿Qué hace exactamente @observe que tú no tuviste que escribir? Dilo en una frase.
  • ¿Qué parte del flujo (configuración del SDK, el decorador, el flush) te resultó menos clara? Es donde conviene volver.
  • Si mañana heredas un agente sin trazas, ¿cuál es el primer cambio que harías?

3.10Referencia rápida

python
1# Instrumentar una función con Langfuse (SDK Python v4)
2from langfuse import observe, get_client
3
4langfuse = get_client()        # lee LANGFUSE_HOST + claves del entorno
5
6@observe()                     # captura input, output, tiempos, errores
7def mi_funcion(x):
8    return trabajo(x)
9
10mi_funcion("...")
11langfuse.flush()               # fuerza el envío en scripts cortos
ElementoQué hace
@observe()Envuelve la función como una observation (vía declarativa).
start_as_current_observation(as_type=...)Control explícito inicio/fin (vía manual, para anidar; N0·L5).
get_client()Cliente Langfuse desde variables de entorno.
flush()Vacía la cola de trazas antes de salir del proceso.
Captura por defectoinput (args), output (retorno), tiempos, error/status.
VersiónSDK Python v4 (2026); requiere Pydantic v2. Aprende la serie, no el patch.
Licencia / despliegueCore MIT; self-host con Docker.

Fuente de los hechos: documentación de Langfuse (SDK Python, modelo de datos, self-hosting; corpus B.3).