Observabilidad para apps con LLM: trazas, métricas y convenciones GenAI de OpenTelemetry

Por Felipe Traina · 28/9/2026 · 6 min · observabilidad, ia, opentelemetry, llm

Una aplicación con LLM puede responder con un 200, en tiempo razonable, y aun así estar fallando: inventando datos, usando un modelo distinto del que pensabas, perdiendo la caché de prompts y triplicando el costo, o entrando en un bucle de llamadas a herramientas. Los dashboards clásicos de APM (latencia, errores, throughput) no alcanzan para ver nada de eso.

La buena noticia es que no hace falta inventar un esquema propio. OpenTelemetry tiene convenciones semánticas específicas para IA generativa (GenAI) que definen nombres estándar para spans, atributos y métricas. Están en estado Development, es decir, todavía pueden cambiar, y recientemente se mudaron a un repositorio dedicado dentro del proyecto. Aun así, son la mejor base disponible para no quedar atado a un proveedor de observabilidad.

Qué hay que ver en una app con LLM

Además de las señales habituales, una app con LLM necesita responder estas preguntas:

Pregunta Señal
¿Cuánto tarda en empezar a responder? Tiempo hasta el primer chunk (streaming)
¿Cuánto cuesta cada request, feature y cliente? Tokens de entrada, salida y caché, por modelo
¿Estoy aprovechando la caché de prompts? Tokens leídos de caché sobre tokens de entrada
¿Qué modelo respondió realmente? Modelo pedido vs. modelo de la respuesta
¿Por qué terminó la generación? Finish reasons (longitud máxima, filtro, herramienta)
¿Qué hizo el agente? Árbol de spans: llamadas al modelo, herramientas y subagentes
¿La respuesta fue buena? Evals en línea, feedback de usuarios, revisión de muestras

Las convenciones GenAI de OpenTelemetry

Para una llamada de inferencia, la especificación define que el nombre del span sea {gen_ai.operation.name} {gen_ai.request.model} (por ejemplo, chat gpt-6-luna) y que el tipo de span sea CLIENT. Los atributos más útiles:

Atributo Qué registra
gen_ai.operation.name La operación: chat, generate_content, text_completion, etc. (obligatorio)
gen_ai.provider.name El proveedor, por ejemplo openai (obligatorio)
gen_ai.request.model El modelo pedido
gen_ai.response.model El modelo que respondió
gen_ai.usage.input_tokens / gen_ai.usage.output_tokens Tokens consumidos
gen_ai.usage.cache_read.input_tokens Tokens de entrada servidos desde la caché del proveedor
gen_ai.response.finish_reasons Por qué terminó cada generación
gen_ai.conversation.id Identificador de la conversación o sesión
error.type El tipo de error, si lo hubo

Para métricas, la especificación incluye, entre otras, gen_ai.client.operation.duration y gen_ai.client.operation.time_to_first_chunk del lado del cliente, y métricas para agentes y herramientas como gen_ai.invoke_agent.duration y gen_ai.execute_tool.duration.

Instrumentación manual, paso a paso

Muchas librerías y SDK ya ofrecen instrumentación automática. Si no es tu caso, o querés entender qué se registra, esto es lo mínimo con la API de OpenTelemetry en Python:

from opentelemetry import trace
from opentelemetry.trace import SpanKind, Status, StatusCode

tracer = trace.get_tracer("mi-app.llm")

def llamar_modelo(cliente, modelo: str, prompt: str, feature: str):
    with tracer.start_as_current_span(f"chat {modelo}", kind=SpanKind.CLIENT) as span:
        span.set_attribute("gen_ai.operation.name", "chat")
        span.set_attribute("gen_ai.provider.name", "openai")
        span.set_attribute("gen_ai.request.model", modelo)
        span.set_attribute("app.feature", feature)  # atributo propio para costos por feature
        try:
            resp = cliente.responses.create(model=modelo, input=prompt)
        except Exception as e:
            span.set_attribute("error.type", type(e).__name__)
            span.set_status(Status(StatusCode.ERROR))
            raise
        span.set_attribute("gen_ai.response.model", resp.model)
        span.set_attribute("gen_ai.usage.input_tokens", resp.usage.input_tokens)
        span.set_attribute("gen_ai.usage.output_tokens", resp.usage.output_tokens)
        return resp

Dos detalles importantes. Primero, app.feature no es parte del estándar: es un atributo propio, y es el que te va a permitir responder "¿cuánto cuesta el resumen de tickets?". Segundo, registrar gen_ai.response.model además del modelo pedido te permite detectar si un alias o un modelo por defecto empieza a resolver a otra versión.

Agentes: el árbol de spans es la herramienta de debugging

En un agente, una sola tarea puede generar decenas de llamadas al modelo y a herramientas. Si cada paso es un span hijo de la tarea, el trace muestra exactamente dónde se fue el tiempo y el dinero:

invoke_agent soporte-tickets        12,4 s
├── chat gpt-6-luna                  0,9 s   (clasificación)
├── execute_tool buscar_pedido       0,3 s
├── chat gpt-6-sol                   4,1 s
├── execute_tool buscar_pedido       0,3 s   <- la misma herramienta otra vez
├── chat gpt-6-sol                   5,8 s
└── execute_tool enviar_respuesta    1,0 s

Patrones a vigilar: la misma herramienta llamada varias veces con los mismos argumentos, cantidad de pasos por tarea que crece con el tiempo y tareas que terminan por límite de pasos en lugar de por éxito.

El contenido de los prompts: opt-in y con cuidado

Los atributos que capturan el contenido de los mensajes (gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions) están definidos como Opt-In en la especificación, y la propia documentación advierte que probablemente contengan información sensible, incluidos datos personales. Las instrumentaciones no deberían capturarlos por defecto.

Si decidís registrarlos:

De la telemetría al costo

Con tokens por modelo y por feature, el costo se calcula en tu backend de métricas o con un job simple que multiplique por el precio vigente. Algunas alertas útiles:

Qué hacer con esto

¿Hoy podés responder cuánto cuesta cada feature con IA de tu producto, o solo conocés la factura total del mes?

Lecturas relacionadas

Fuentes

← Volver al blog