Langfuse WorkshopClickHouse Workshops

02 Tracing

Este es el punto inicial del tracing: el mismo código que checkpoint/01-base-app, todavía sin integración con Langfuse. Los paquetes ya están en package.json — ejecuta npm inst...

El material del workshop se mantiene en el repositorio público langfuse/langfuse-workshop. Usa el repositorio para ejecutar la aplicación, acceder a las ramas de checkpoint y realizar la configuración local.

Ver este archivo Markdown

Punto de partida

git checkout checkpoint/02-tracing

Este es el punto inicial del tracing: el mismo código que checkpoint/01-base-app, todavía sin integración con Langfuse. Los paquetes ya están en package.json; ejecuta npm install si no lo has hecho. Comprueba que .env contiene tu OPENAI_API_KEY y las claves de Langfuse.

Por qué hacemos tracing

El tracing registra cada paso del agente —llamadas al modelo, herramientas invocadas, entradas y salidas— en el orden en que sucedieron. Convierte el agente de una caja negra en algo que puedes examinar después; si una respuesta es incorrecta, señalas el paso exacto en vez de adivinar.

Para conocer la motivación general, consulta la lección de Langfuse Academy sobre tracing. Para los detalles técnicos —opciones del SDK, funcionamiento interno de OpenTelemetry y atributos de spans— consulta la documentación de tracing.

Objetivo

Cuando el padre pregunta "¿Cómo activo el Bluetooth?", el agente no llama a OpenAI una sola vez. Pregunta qué hacer, llama a get_support_context para obtener la configuración del iPhone, vuelve a preguntar, llama a search_help_library para recuperar los pasos de Bluetooth y pregunta una vez más para producir la respuesta numerada. Nada de eso es visible hoy.

El objetivo es hacer visibles todos esos pasos en Langfuse: un turno de chat se convierte en un trace anidado con la ejecución del agente, las generations de OpenAI y las dos llamadas de herramientas registradas en orden.

Proceso paso a paso de Specs

Construiremos el trace en tres pasos que reflejan la estructura del agente:

  1. Primer trace — registrar las generations de OpenAI.
  2. Traces anidados — agrupar las generations bajo una ejecución del agente por turno.
  3. Registro de herramientas — convertir cada invocación en una observación.

Paso 1 — Primer trace

Queremos observar las llamadas a OpenAI para ver entradas, salidas, coste, tokens y tiempo. Bastan dos cambios.

src/server/index.ts

Inicia el procesador de spans de Langfuse cerca del principio del archivo:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

new NodeSDK({ spanProcessors: [new LangfuseSpanProcessor()] }).start();

El procesador lee LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY y LANGFUSE_BASE_URL del entorno del proceso Node. El servidor carga el .env del repositorio antes que el SDK; edita .env en vez de depender de valores exportados en el shell.

Nota: si el último trace aparece tarde al detener o reiniciar npm run dev, vuelve a index.ts y convierte la línea anterior en variables langfuseSpanProcessor y sdk, para que shutdown() pueda hacer flush:

const langfuseSpanProcessor = new LangfuseSpanProcessor();
const sdk = new NodeSDK({ spanProcessors: [langfuseSpanProcessor] });
sdk.start();

async function shutdown() {
  server.close();
  await langfuseSpanProcessor.forceFlush();
  await sdk.shutdown();
}

No es necesario para entender tracing, pero evita la confusión de perder el último trace en desarrollo local.

src/server/support-agent.ts

Añade el import:

import { observeOpenAI } from "@langfuse/openai";

Después, envuelve el cliente OpenAI donde lo creas. Busca esta línea en runSupportConversation:

const openai = new OpenAI({ apiKey: env.openaiApiKey });

y cámbiala por:

const openai = observeOpenAI(new OpenAI({ apiKey: env.openaiApiKey }));

Ese es todo el cambio. Sin factory ni cliente sin envolver separado: observeOpenAI envuelve el cliente inline y openai.chat.completions.create(...) pasa a emitir un trace por llamada.

Verifica: ejecuta npm run dev, haz una pregunta y actualiza Langfuse. Verás una generation por llamada, con prompt, respuesta, tokens y latencia. Cada generation sigue siendo un trace de nivel superior; lo corregiremos a continuación.

Vista Traces después del Paso 1: cada turno aparece como generations openai-chat-completion independientes.

Paso 2 — Traces anidados

Para contextualizar las generations, las agruparemos bajo una ejecución del agente por turno. Son tres cambios en src/server/support-agent.ts, sin modificar el cuerpo de la función.

1. Añade el import:

import { observe } from "@langfuse/tracing";

2. Degrada la función existente. Busca:

export async function runSupportConversation(request: ChatRequest): Promise<ChatResponse> {

Quita export y cambia el nombre:

async function runSupportConversationInner(request: ChatRequest): Promise<ChatResponse> {

El cuerpo permanece igual.

3. Añade el export envuelto al final del archivo:

export const runSupportConversation = observe(runSupportConversationInner, {
  name: "dad-it-support-chat-turn",
  asType: "agent"
});

index.ts sigue importando runSupportConversation de la misma forma. observe(...) captura automáticamente el argumento como entrada del trace y el valor devuelto como salida.

Verifica: un turno debe aparecer ahora como una única observación dad-it-support-chat-turn con la generation de OpenAI anidada.

Árbol de trace tras el Paso 2: una raíz de agente dad-it-support-chat-turn con la generation de OpenAI como hija.

Paso 3 — Registrar llamadas de herramientas

La generation ya menciona las herramientas en su salida tool_calls, pero no hay una observación de la ejecución real: no vemos la entrada ni la salida. El mismo patrón observe(...) puede envolver cada herramienta.

src/server/tools.ts

Añade el import y los dos helpers observados encima de executeTool. Después, sustituye el executeTool existente por la versión siguiente, para que el switch llame a los helpers. No cambies TOOL_DEFINITIONS.

import { observe } from "@langfuse/tracing";

const getSupportContextTool = observe(
  async () => {
    const context = getSupportContext();

    return {
      ok: true,
      context: {
        id: context.id,
        label: context.label,
        devices: context.devices,
        deviceSummary: context.deviceSummary,
        responseStyle: context.responseStyle,
        scopeHighlights: context.scopeHighlights,
        notableApps: context.notableApps
      }
    };
  },
  { name: "get_support_context", asType: "tool" }
);

const searchHelpLibraryTool = observe(
  async (input: { question: string }) => {
    const guides = searchGuides(input.question);

    return {
      ok: true,
      results: guides.map((guide) => ({
        id: guide.id,
        title: guide.title,
        summary: guide.summary,
        steps: guide.steps,
        caution: guide.caution ?? null
      }))
    };
  },
  { name: "search_help_library", asType: "tool" }
);

export async function executeTool(name: string, input: Record<string, unknown>): Promise<ToolResult> {
  switch (name) {
    case "get_support_context":
      return getSupportContextTool();

    case "search_help_library":
      return searchHelpLibraryTool({ question: String(input.question ?? "") });

    default:
      return { ok: false, error: `Unsupported tool: ${name}` };
  }
}

Si npm run dev termina con Multiple exports with the same name "executeTool", el executeTool original sigue más abajo. Elimínalo y conserva solo la versión anterior.

Trace completo tras el Paso 3: dad-it-support-chat-turn con la generation de OpenAI y las observaciones get_support_context + search_help_library debajo.

Cómo verificar que has terminado

  • Un turno de usuario crea un trace en Langfuse.
  • Observación raíz: dad-it-support-chat-turn (tipo agent).
  • Generation hija de observeOpenAI(...) con prompt, respuesta, tokens y latencia.
  • Observaciones hijas de herramientas: get_support_context, search_help_library.
  • La entrada raíz es la solicitud del chat y la salida raíz, la respuesta.

Cierre

Mismo patrón, distintos tipos de observación: observe(fn, { asType }) envuelve una función y emite un span con el nombre y tipo indicados. observeOpenAI(client) es la versión especializada para el SDK de OpenAI.

Una forma más directa de añadir tracing completo según las prácticas recomendadas es la skill de Langfuse (/langfuse). Aplica los patrones a tu código sin que implementes cada wrapper a mano. Este recorrido muestra lo que sucede por debajo.

observeOpenAI envuelve el SDK oficial de OpenAI; por debajo equivale a la instrumentación automática para OpenAI JS. Si utilizas otro SDK —Anthropic, Vercel AI SDK o tu propio cliente HTTP—, el catálogo de integraciones contiene el wrapper o la guía equivalente.

Apéndice/Extra — IDs de usuario y sesión

El recorrido anterior produce un trace limpio con forma padre → generation → herramienta. Lo siguiente que suelen querer los equipos es segmentar por usuario y sesión para ver "todos los turnos de este usuario" o "la sesión completa de ayer". Para simplificar la presentación en directo, omitimos este paso, pero los checkpoints posteriores lo incluyen para que los capítulos siguientes utilicen esas vistas sin más código. Consulta Sessions y Users en la documentación.

En resumen:

import { propagateAttributes } from "@langfuse/tracing";

return propagateAttributes(
  {
    userId: request.userId ?? `workshop-${context.id}`,
    sessionId: request.sessionId,
    tags: ["langfuse-workshop", "dad-it-support"]
  },
  async () => {
    // ...the same tool-calling loop...
  }
);

Todo lo que queda dentro de propagateAttributes(...) —incluidos los spans hijos emitidos por observeOpenAI— recibe automáticamente userId, sessionId y los tags. Las vistas Users, Sessions y los filtros de tags de Langfuse se activan en cuanto existen esos atributos.

Estado final

Esta aplicación terminada con tracing es el punto de partida para 03-prompt-management y 04-monitoring.

En esta página

¿Quieres seguir tu progreso?

Opcional. Enviaremos un enlace por correo para confirmar tu dirección; el progreso se registrará cuando lo abras.

Usa tu correo de trabajo, no uno personal.

Para seguir el progreso también debes aceptar los Términos del servicio actuales en la Configuración de privacidad.

ES