Todos los recursos

Cuando una función de LLM falla a las 2am, "dio una respuesta rara" no te dice nada. Esta guía te muestra cómo instrumentar cada llamada al modelo como una traza estructurada (el prompt resuelto, la respuesta cruda, la secuencia de tool calls, tokens y latencia) para que puedas reconstruir con exactitud qué vio e hizo el modelo, y luego enmascarar lo sensible para que tu tabla de trazas no termine siendo tu próxima filtración de datos.

Observabilidad en apps de LLM: registrar lo que de verdad importa

En resumen

  • Una llamada al LLM sin traza es imposible de depurar. Registra cada llamada como un registro estructurado (nunca un print) con un trace id que te permita reconstruir una corrida completa.
  • Captura el prompt resuelto (el texto final que realmente se envió, no la plantilla), la respuesta cruda con tool calls y argumentos, el modelo y sus params, además de tokens y latencia.
  • En agentes, la secuencia de tool calls ES la historia. Poder reproducir "el planner buscó, no obtuvo nada y terminó alucinando un resultado" es lo que convierte un misterio en un arreglo de una sola línea.
  • Prompts y respuestas vienen cargados de datos personales. Enmascara o hashea los campos sensibles antes de que lleguen al log y define una ventana de retención, o tu tabla de observabilidad se convierte en tu peor filtración.
  • Muestrea por resultado, no a ciegas: guarda el 100% de errores, llamadas lentas y escaladas; muestrea el camino feliz. Envuelve el logger en un try/catch para que la instrumentación nunca tumbe el request.

La observabilidad tradicional asume determinismo: la misma entrada produce la misma salida, así que un stack trace más la entrada bastan para reproducir un bug. Las funciones de LLM rompen ese supuesto. El mismo prompt puede dar respuestas distintas, un tool call se dispara en una corrida y en la siguiente no, y el fallo casi nunca es una excepción. Es una respuesta segura y equivocada que se ve perfecta hasta que un usuario la nota. Esta guía te lleva paso a paso a instrumentar las llamadas al LLM para que, cuando algo salga mal, puedas reconstruir con precisión qué vio el modelo y qué hizo. Vas a salir con un esquema concreto, un logger que puedes meter en un codebase de TypeScript, un ejemplo real de cómo depurar un fallo de agente desde su traza, y los resguardos de privacidad que evitan que tu almacén de trazas se convierta en un pasivo.

Nota

A lo largo de esta guía, una traza es el registro completo de una operación lógica (un turno de chat, una corrida de agente), y un span es un paso dentro de ella (una sola llamada al modelo o la ejecución de una herramienta). Una traza, muchos spans, unidos por un trace id compartido. Si ya usaste tracing distribuido, es la misma idea acotada al trabajo con LLM.

01 · Prerrequisitos: lo que necesitas antes de instrumentar

No empieces a meterle logging a todo hasta tener esto resuelto. Cada cosa es barata y te ahorra tener que rehacer el trabajo después.

  • Un lugar donde escribir registros estructurados. Una sola tabla de Postgres o Supabase basta para arrancar. No necesitas un servicio de tracing dedicado desde el primer día. El requisito es que los registros se puedan consultar por campo, no sacar con grep de un archivo de log. Un JSON dentro de una columna de texto sirve, siempre que puedas filtrar y agregar sobre él.
  • Un trace id generado en el punto de entrada. Genera un UUID donde un request entra a tu sistema (la ruta de la API, el consumidor de la cola) y pásalo por cada llamada a modelo y a herramienta de esa operación. Sin él, una corrida de agente de varios pasos no es más que un montón de filas sueltas.
  • Un único punto de paso para las llamadas al modelo. Si toda llamada a Claude pasa por una función wrapper, instrumentas una sola vez. Si las llamadas están regadas por todo el codebase, arregla eso primero: centraliza y después instrumenta. Ese refactor se paga solo la primera vez que te toque depurar.
  • Un acuerdo sobre qué es sensible. Decide desde ya qué campos pueden contener datos personales (los prompts y las respuestas casi siempre los traen) para que el enmascarado venga integrado desde el primer registro y no haya que ponerlo a la carrera después de una filtración.

Importante

Genera el trace id justo en el borde de tu sistema y pásalo hacia abajo de forma explícita. No intentes reconstruirlo después a partir de timestamps o ids de usuario. Vas a terminar uniendo los spans equivocados y confiando en una historia que nunca pasó.

02 · Qué capturar en cada llamada

La meta es la reconstrucción: desde una sola fila deberías poder rearmar con exactitud qué se le pidió al modelo y cómo respondió. Captura como mínimo:

  • Un trace id y un índice de paso, para poder ordenar y agrupar los spans de una corrida.
  • El prompt resuelto que realmente se envió, no la plantilla, sino el texto final con todas las variables ya interpoladas. Este es el campo más importante y el que la gente termina haciendo mal. Los bugs se esconden en la interpolación: un undefined que se renderizó como el texto literal "undefined", un documento truncado, una instrucción de sistema que se cayó sin avisar. Si registras la plantilla, registras lo que estaba bien y te pierdes lo que se rompió.
  • Modelo, temperatura y demás params, incluyendo cualquier ajuste de thinking o effort. El comportamiento cambia cuando estos cambian, y quieres verlo en la traza en lugar de andar adivinando.
  • La respuesta cruda, con todos sus tool calls y sus argumentos exactos. No tu resumen ya parseado. La estructura cruda, para poder ver qué emitió realmente el modelo frente a lo que tu código hizo con eso.
  • Tokens y latencia (tokens de entrada, tokens de salida, milisegundos). Alimentan tus vistas de costo y velocidad, y delatan la llamada descontrolada antes que la factura.

Captura las entradas, no solo las salidas

Un atajo a medias muy común es registrar solo la respuesta. Cuando la respuesta está mal, te dice que está mal, no por qué. El porqué casi siempre está en la entrada: qué contexto se recuperó, qué decía realmente el prompt resuelto, qué herramientas se le ofrecieron. Registra las entradas con tanto cuidado como las salidas, como mínimo.

03 · Un logger que puedes copiar y pegar

Así se ve en TypeScript. Pasa cada llamada al modelo por una sola función para que la instrumentación viva en un único lugar. Fíjate en que el cronómetro envuelve la llamada, el logging es best-effort, y los campos sensibles pasan por el enmascarado antes de guardarse.

type Span = {
  traceId: string
  step: number
  model: string
  params: Record<string, unknown>
  resolvedPrompt: string
  raw: unknown
  toolCalls: { name: string; args: unknown }[]
  inputTokens: number
  outputTokens: number
  latencyMs: number
  outcome: "ok" | "error" | "escalated"
}

async function tracedCall(traceId: string, step: number, input: CallInput) {
  const start = performance.now()
  try {
    const res = await callModel(input)
    void logSpan({
      traceId,
      step,
      model: input.model,
      params: input.params,
      resolvedPrompt: redact(input.prompt),
      raw: redact(res.raw),
      toolCalls: res.toolCalls,
      inputTokens: res.usage.input,
      outputTokens: res.usage.output,
      latencyMs: performance.now() - start,
      outcome: "ok",
    })
    return res
  } catch (err) {
    void logSpan({ traceId, step, outcome: "error", latencyMs: performance.now() - start, error: String(err) } as Span)
    throw err
  }
}

Dos decisiones deliberadas: la llamada a logSpan es fire-and-forget (el void) y va envuelta para que nunca pueda lanzar una excepción hacia el camino del request. Un logger roto jamás debe romper una funcionalidad que sí funciona. Y el cronómetro envuelve solo la llamada al modelo, así tu número de latencia refleja el modelo y no tu propia serialización.

Atención

Nunca dejes que el logging bloquee ni tumbe el request. Si tu almacén de trazas está caído, el request del usuario igual debe completarse. Envuelve la escritura en su propio try/catch, prefiere un insert fire-and-forget o una cola en segundo plano, y asume que perder una traza es un costo aceptable. Un logger que tumba producción es peor que no tener logger.

04 · En agentes, la secuencia de tool calls es la historia

Una sola llamada al modelo es una oración; una corrida de agente es un párrafo, y la trama es el orden de sus tool calls. Lo más valioso que te dan tus trazas es poder reproducir esa secuencia: a qué herramienta recurrió el agente, qué argumentos le pasó, qué le devolvió, y qué hizo después.

Asegúrate de que cada ejecución de herramienta sea su propio span bajo el mismo trace id, capturando el nombre de la herramienta, los argumentos exactos, el resultado (o el error) y la latencia. Cuando puedes poner los spans de una corrida uno tras otro, los fallos que parecían torpeza del modelo se revelan como bugs aburridos y arreglables:

  • El agente llamó a search, obtuvo un resultado vacío, y luego se inventó una respuesta que sonaba creíble en vez de reportar que no encontró nada. El arreglo es una instrucción en el prompt o en el resultado de la herramienta, no un modelo más grande.
  • El agente pasó un argumento que tu código convirtió en silencio (un string donde se esperaba un número) y la herramienta devolvió basura. El arreglo es validar en la frontera de la herramienta.
  • El agente entró en bucle, llamando cuatro veces a la misma herramienta de lectura con argumentos idénticos porque el resultado nunca volvió al contexto. El arreglo está en cómo le pasas los resultados hacia adelante.

Nada de eso se nota mirando solo la respuesta final. Todo se vuelve obvio en cuanto puedes leer la corrida como una secuencia.

05 · La privacidad no es opcional: enmascara antes de guardar

Una tabla de trazas guarda, por diseño, el texto más sensible de tu sistema: todo lo que escribieron los usuarios y todo lo que el modelo respondió. Eso suele incluir nombres, emails, datos de pago, información de salud, lo que sea que traigan tus usuarios. Una tabla de observabilidad que olvidaste proteger es una filtración esperando a ocurrir, y "la necesitábamos para depurar" no te sirve de defensa una vez que ya pasó.

Integra el enmascarado en el logger para que no exista ningún camino que escriba datos sensibles en crudo:

  1. Enmascara o hashea los campos que sabes que son sensibles antes de escribir el registro. Hashea lo que quizá necesites correlacionar (el mismo email entre trazas) pero nunca necesites leer; descarta o enmascara cualquier cosa que registraste solo por costumbre.
  2. Define una ventana de retención y hazla cumplir. Las trazas son útiles por días, a veces semanas, casi nunca para siempre. Un job programado que borra las filas que pasaron la ventana reduce tu radio de daño de forma automática.
  3. Restringe el acceso. Los datos de traza deben poder leerlos las personas que depuran, no toda la organización. En un setup de Supabase, déjalos detrás del service role y de row-level security, nunca alcanzables desde el cliente.
create table llm_trace (
  id          uuid primary key default gen_random_uuid(),
  trace_id    uuid not null,
  step        int  not null,
  model       text not null,
  params      jsonb,
  prompt      text,        -- ya enmascarado antes del insert
  response    jsonb,       -- ya enmascarado antes del insert
  tool_calls  jsonb,
  input_tokens  int,
  output_tokens int,
  latency_ms    int,
  outcome     text check (outcome in ('ok','error','escalated')),
  created_at  timestamptz not null default now()
);

create index on llm_trace (trace_id, step);
create index on llm_trace (created_at);
-- retención: corre un job diario
-- delete from llm_trace where created_at < now() - interval '30 days';

Consejo

Enmascara en la frontera, en el logger, no en cada punto de llamada. Una sola función de enmascarado por la que pasa cada span significa que una funcionalidad nueva no puede filtrar un campo por accidente. Físicamente no puede llegar a la tabla sin enmascarar. Centralizar la lógica de privacidad es la misma jugada que centralizar la llamada al modelo: hazlo una vez, confía en ello en todos lados.

06 · Muestrea por resultado y vigila el costo

A volumen, quizá no quieras guardar cada traza del camino feliz para siempre. La fidelidad total sobre millones de llamadas rutinarias sale cara en almacenamiento y en ruido. El error es muestrear de forma uniforme, porque el muestreo uniforme descarta justo los eventos raros que más necesitas. Mejor muestrea por resultado:

  • Guarda el 100% de lo interesante: errores, llamadas por encima de un umbral de latencia, escaladas a un modelo más potente, cualquier cosa marcada como baja confianza, y cualquier corrida que un usuario haya reportado.
  • Muestrea el aburrido camino feliz a la tasa que mantenga tu almacén manejable: 1 de cada 10 o 1 de cada 100 de las llamadas limpias, rápidas y esperadas suele bastar para detectar drift.
  • Conserva siempre los contadores agregados aunque descartes el detalle. Puedes descartar el prompt y la respuesta completos de una llamada rutinaria y aun así contar sus tokens y su latencia, así tus dashboards de costo y rendimiento quedan completos incluso sobre el tráfico no muestreado.

Esto mantiene tu poder de depuración justo donde viven los fallos sin que la factura se dispare. Las trazas que de verdad vas a abrir a las 2am son las raras, y el muestreo por resultado garantiza que esas siempre estén ahí.

Instrumenta el punto de paso una sola vez, registra el prompt resuelto y la respuesta cruda en vez de tu resumen prolijo de ellos, y trata la secuencia de tool calls como el artefacto principal de cualquier agente. Enmascara antes de guardar y define una ventana de retención para que la tabla de trazas que te salva en una mala noche no te hunda en una peor. Haz esto y la próxima vez que una funcionalidad de LLM falle, no vas a andar adivinando desde "dio una respuesta rara". Vas a abrir la traza, leer qué vio el modelo, y arreglar el bug de verdad.

Puntos clave

  • Instrumenta un solo punto de paso: pasa cada llamada al modelo por un único wrapper para registrar una sola vez y no perder ninguna llamada.
  • Captura el prompt resuelto y la respuesta cruda, no la plantilla ni tu resumen parseado. Los bugs se esconden en la interpolación y en lo que tu código hizo con la salida.
  • En agentes, registra cada ejecución de herramienta como un span bajo un mismo trace id; la secuencia de tool calls es lo que convierte un misterio en un arreglo.
  • Enmascara o hashea los campos sensibles en el logger y define una ventana de retención. Una tabla de trazas sin proteger es tu peor filtración.
  • Haz el logging fire-and-forget y muestrea por resultado: 100% de errores y escaladas, una fracción del camino feliz, contadores agregados en todo.

Preguntas frecuentes

¿Necesito una plataforma de tracing dedicada o puedo empezar con una tabla en la base de datos?

Empieza con una tabla. Una sola tabla de Postgres o Supabase con un trace id, un índice de paso, el prompt resuelto, la respuesta cruda, los tool calls, tokens, latencia y un resultado cubre la gran mayoría de la depuración. El único requisito firme es que los registros se puedan consultar por campo. Un JSON en una columna jsonb sirve. Pásate a una plataforma dedicada cuando necesites correlación entre servicios, vistas de cascada elaboradas o alertas que no quieras construir tú mismo; antes no.

¿Por qué registrar el prompt resuelto en vez de la plantilla y las variables?

Porque los bugs viven en la interpolación, y la plantilla es justo la parte que estaba bien. Si registras la plantilla, vas a ver la versión que funcionaba y te vas a perder el texto final roto: un undefined que se renderizó como el literal "undefined", un documento que se truncó, una línea de sistema que se cayó sin avisar. Registra los bytes exactos que se enviaron al modelo. Siempre puedes reconstruir la plantilla a partir del prompt, pero nunca el prompt a partir de la plantilla.

¿Registrar prompts y respuestas completos no sale caro a volumen?

Puede salir caro, y por eso muestreas por resultado en vez de uniformemente. Guarda el 100% de los errores, las llamadas lentas, las escaladas y las corridas reportadas (las trazas que de verdad vas a abrir) y muestrea el aburrido camino feliz 1 de cada 10 o 1 de cada 100. Lo clave: conserva los contadores agregados (tokens, latencia, resultado) en todo aunque descartes el texto completo, así tus dashboards de costo y rendimiento quedan completos mientras los campos pesados solo aterrizan en el tráfico que vale la pena guardar.

¿Cómo mantengo los datos personales fuera de mi tabla de trazas?

Enmascara en la frontera, dentro del logger, para que no exista ningún camino de código que escriba datos sensibles en crudo. Pasa cada span por una sola función de enmascarado: hashea lo que necesites correlacionar pero nunca leer (como un email que debe coincidir entre trazas) y enmascara o descarta lo que registraste por costumbre. Luego define una ventana de retención forzada por un delete programado, y deja las lecturas detrás del service role y de row-level security para que la tabla nunca sea alcanzable desde el cliente.

¿Qué es lo más útil que puedo capturar para depurar agentes?

La secuencia de tool calls, con cada ejecución de herramienta como su propio span: el nombre de la herramienta, los argumentos exactos, el resultado o el error, y la latencia, todo bajo un mismo trace id. La respuesta final te dice que el agente falló; la secuencia te dice por qué: buscó, no obtuvo nada y terminó alucinando, o entró en bucle en una lectura porque el resultado nunca volvió al contexto. En cuanto puedes leer una corrida de principio a fin, la mayoría de los fallos de "el modelo es tonto" resultan ser bugs comunes y arreglables.

¿El logger puede llegar a romper mi funcionalidad?

No debe pasar, y eso es una regla de diseño, no un accidente. Haz el logging fire-and-forget y envuelve la escritura en su propio try/catch para que un almacén de trazas caído no se propague al camino del request. Prefiere un insert no bloqueante o una cola en segundo plano, y asume que perder una traza es un costo aceptable. Que el request del usuario se complete siempre pesa más que capturar su traza. Un logger que tumba producción es peor que no tener logger.

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados