LLMOps en Producción: Observabilidad, Trazabilidad y Monitoreo de Sistemas con LLMs
Tienes un pipeline RAG desplegado. Tiene un agente conectado a n8n, un modelo fine-tuned para tu dominio, y structured outputs que alimentan tu CRM. Todo funciona en staging. Lo subes a producción y a las 48 horas empieza el caos: usuarios reportan respuestas lentas, el costo mensual de la API se disparó un 400%, y alguien pregunta en qué llamada específica el modelo empezó a alucinar datos de clientes.
Sin observabilidad, no puedes responder ninguna de esas preguntas. Con ella, tienes el rastro completo de cada llamada, cada token, cada decisión del sistema.
LLMOps es la disciplina que cubre el ciclo operativo completo de sistemas con LLMs: desde el momento en que un prompt entra hasta que la respuesta llega al usuario, pasando por todos los pasos intermedios (retrieval, tool calls, chain reasoning, reranking). La observabilidad es su componente más crítico y más frecuentemente omitido.
Por qué los sistemas LLM son ciegos por defecto
Un microservicio tradicional falla de formas predecibles: timeout, error 500, null pointer. Los LLMs fallan de formas estadísticas: el modelo responde con confianza información incorrecta, el RAG recupera el chunk equivocado, el agente entra en un loop de tool calls, el costo por request se triplica por una mala estrategia de prompting.
Estos fallos no generan excepciones. No activan alertas en tu sistema de monitoreo estándar. Pasan en silencio hasta que alguien lo nota manualmente.
El stack típico de observabilidad (Prometheus + Grafana + Sentry) captura métricas de infraestructura: CPU, memoria, latencia HTTP. Pero no sabe nada de lo que ocurrió dentro de la llamada al LLM: cuántos tokens consumió el contexto de sistema vs. el contexto del usuario, cuántos documentos recuperó el retriever, qué score de relevancia tenían, si el modelo fue a buscar tools o respondió directo.
Para cubrir ese ciego necesitas herramientas diseñadas específicamente para LLMs.
Los tres pilares: trazas, métricas y evaluaciones
La observabilidad en sistemas LLM se estructura en tres capas que se complementan:
Trazas (traces): el registro completo de una ejecución. Una traza captura cada paso de tu pipeline en orden: el prompt recibido, el retrieval con sus resultados, la llamada al modelo con su request y response completos, los tool calls si los hay, y la respuesta final. Cada paso tiene su timestamp, duración y metadatos. Las trazas son tu fuente de verdad para debugging.
Métricas agregadas: latencia p50/p95/p99, tokens consumidos por request, costo estimado por sesión, tasa de errores, distribución de scores de calidad. Las métricas te dicen qué está pasando a nivel sistémico; las trazas te dicen por qué.
Evaluaciones: scores de calidad sobre las respuestas. Pueden ser automáticos (usando otro LLM como juez, o métricas como BLEU/ROUGE para tareas específicas) o humanos (anotadores que califican respuestas). Las evaluaciones responden la pregunta que las métricas de infraestructura no pueden: ¿el sistema está dando buenas respuestas?
Langfuse: setup completo en 30 minutos
Langfuse es actualmente la herramienta open-source más madura para observabilidad de LLMs. Ofrece trazas detalladas, métricas de costo y tokens, evaluaciones, y un dashboard que funciona desde el primer día. Puede usarse como SaaS o self-hosted con Docker.
La integración más directa en Python usa su SDK con decoradores:
pip install langfuse openai anthropicimport os
from langfuse import Langfuse
from langfuse.decorators import observe, langfuse_context
import anthropic
# Configuración — también acepta variables de entorno LANGFUSE_PUBLIC_KEY, etc.
langfuse = Langfuse(
public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
secret_key=os.environ["LANGFUSE_SECRET_KEY"],
host="https://cloud.langfuse.com" # o tu instancia self-hosted
)
client = anthropic.Anthropic()
@observe() # Este decorador crea automáticamente una traza en Langfuse
def retrieve_context(query: str) -> list[str]:
"""Simula un paso de retrieval — aquí iría tu vector DB"""
langfuse_context.update_current_observation(
name="retrieval",
input={"query": query},
metadata={"retriever": "chroma", "top_k": 5}
)
# Tu lógica de retrieval aquí
return ["Documento relevante 1...", "Documento relevante 2..."]
@observe()
def generate_response(query: str, context: list[str]) -> str:
"""Llama al LLM con el contexto recuperado"""
context_text = "\n\n".join(context)
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
system=f"Responde usando únicamente el siguiente contexto:\n\n{context_text}",
messages=[{"role": "user", "content": query}]
)
# Langfuse captura automáticamente el costo si registras los tokens
langfuse_context.update_current_observation(
name="llm-call",
usage={
"input": response.usage.input_tokens,
"output": response.usage.output_tokens,
"unit": "TOKENS"
},
model="claude-opus-4-6"
)
return response.content[0].text
@observe(name="rag-pipeline") # Traza raíz que agrupa todos los pasos
def rag_query(user_query: str, session_id: str = None) -> str:
# Asociar con sesión de usuario para agrupar conversaciones
if session_id:
langfuse_context.update_current_trace(
session_id=session_id,
user_id="user-123", # tu identificador de usuario
tags=["production", "rag-v2"]
)
context = retrieve_context(user_query)
response = generate_response(user_query, context)
# Score de calidad opcional — puedes automatizarlo o dejarlo para revisión humana
langfuse_context.score_current_trace(
name="context_relevance",
value=0.85, # aquí iría tu función de scoring
comment="Contexto recuperado altamente relevante"
)
return response
# Uso
result = rag_query(
"¿Cuál es la política de devoluciones?",
session_id="session-abc123"
)
print(result)
# Asegurar que todos los eventos se envíen antes de cerrar
langfuse.flush()
Esta estructura genera automáticamente en el dashboard de Langfuse: una traza con spans anidados (retrieval → llm-call), la duración de cada paso, el costo estimado en USD basado en los tokens y el modelo, y el score de calidad asociado.
Integración con la Anthropic SDK vía callbacks
Si prefieres no modificar tu código con decoradores, Langfuse ofrece integración directa como handler en el SDK de Anthropic:
from langfuse.anthropic import anthropic as langfuse_anthropic
# Drop-in replacement: usa la misma API pero con trazabilidad automática
client = langfuse_anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": "Explica qué es un embedding"}],
# Metadatos opcionales para Langfuse
metadata={
"langfuse_session_id": "session-xyz",
"langfuse_user_id": "user-456",
"langfuse_tags": ["production", "embeddings-explainer"]
}
)
print(response.content[0].text)
Cada llamada se registra automáticamente con tokens de entrada/salida, modelo usado, latencia y costo estimado. No necesitas cambiar nada más en tu código existente.
Métricas críticas a monitorear
No todas las métricas tienen el mismo valor operativo. Las que más impacto tienen en producción:
Latencia por etapa: no es suficiente medir el tiempo total de respuesta. Necesitas saber cuánto tarda el retrieval, cuánto tarda la llamada al modelo (time to first token vs. tiempo total), y cuánto tarda el post-processing. Un p95 de 8 segundos puede venir de un retriever lento, no del modelo.
Distribución de tokens de entrada: si el contexto promedio tiene 800 tokens pero tienes spikes de 12,000 tokens, alguien está inyectando contexto innecesario o hay un bug en tu chunking strategy. Esta métrica detecta problemas de costo y latencia antes de que exploten.
Tasa de tool calls por request: en sistemas agénticos, un agente que debería hacer 1-2 tool calls por request y está haciendo 8-10 indica un loop o un prompt mal definido. Monitorea el histograma, no solo el promedio.
Cache hit rate: si usas prompt caching (Anthropic lo soporta nativamente desde 2024), el porcentaje de tokens servidos desde caché vs. tokens procesados es directamente proporcional a tu ahorro de costos. Un 60% de cache hit en el system prompt puede reducir tu factura a la mitad.
import json
from dataclasses import dataclass, asdict
from datetime import datetime
from typing import Optional
import logging
@dataclass
class LLMCallMetrics:
trace_id: str
timestamp: str
model: str
input_tokens: int
output_tokens: int
cache_read_tokens: int
cache_creation_tokens: int
latency_ms: float
step_name: str
session_id: Optional[str] = None
user_id: Optional[str] = None
error: Optional[str] = None
@property
def cost_usd(self) -> float:
"""Cálculo de costo para Claude Sonnet (ajusta según modelo)"""
prices = {
"claude-sonnet-4-6": {
"input": 3.0 / 1_000_000,
"output": 15.0 / 1_000_000,
"cache_read": 0.30 / 1_000_000,
"cache_write": 3.75 / 1_000_000
}
}
p = prices.get(self.model, prices["claude-sonnet-4-6"])
return (
self.input_tokens * p["input"] +
self.output_tokens * p["output"] +
self.cache_read_tokens * p["cache_read"] +
self.cache_creation_tokens * p["cache_write"]
)
@property
def effective_cache_saving_usd(self) -> float:
"""Ahorro por cache: diferencia entre precio normal y precio cache"""
standard_price = 3.0 / 1_000_000
cache_read_price = 0.30 / 1_000_000
return self.cache_read_tokens * (standard_price - cache_read_price)
def log_metrics(metrics: LLMCallMetrics):
"""Emite métricas en formato estructurado para ingesta en cualquier backend"""
logging.info(json.dumps({
"event": "llm_call",
**asdict(metrics),
"cost_usd": round(metrics.cost_usd, 6),
"cache_saving_usd": round(metrics.effective_cache_saving_usd, 6)
}))
# Uso
metrics = LLMCallMetrics(
trace_id="trace-abc",
timestamp=datetime.utcnow().isoformat(),
model="claude-sonnet-4-6",
input_tokens=1200,
output_tokens=380,
cache_read_tokens=800,
cache_creation_tokens=0,
latency_ms=1840.5,
step_name="rag-generate",
session_id="session-xyz"
)
log_metrics(metrics)
print(f"Costo real: ${metrics.cost_usd:.6f}")
print(f"Ahorro por cache: ${metrics.effective_cache_saving_usd:.6f}")
Self-hosted con Docker: Langfuse en tu infraestructura
Si manejas datos sensibles o necesitas control total, Langfuse tiene una imagen oficial que levanta en minutos con Docker Compose:
# docker-compose.yml mínimo para Langfuse self-hosted
version: "3.8"
services:
langfuse-server:
image: langfuse/langfuse:2
depends_on:
db:
condition: service_healthy
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/langfuse
- NEXTAUTH_SECRET=tu-secret-aqui-mínimo-32-chars
- SALT=tu-salt-aqui
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=false # desactiva telemetría si lo requieres
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: langfuse
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
Después de docker compose up -d, accedes en http://localhost:3000, creas tu organización y obtienes tus API keys. El SDK apunta a tu instancia con host="http://localhost:3000".
Alertas: cuándo el sistema necesita atención
El monitoreo sin alertas es inútil. Las alertas más valiosas en sistemas LLM en producción son las que miden anomalías estadísticas, no umbrales fijos:
Spike de latencia: si la latencia p95 de un endpoint sube más de 2 desviaciones estándar respecto a la ventana de las últimas 24 horas, algo cambió. Puede ser el tamaño del contexto, rate limiting de la API, o un cambio en el modelo.
Costo diario acumulado: con una estimación por llamada, puedes calcular el burn rate proyectado al mes y alertar si supera tu presupuesto antes de llegar al final del ciclo.
Tasa de errores de parsing: si tus structured outputs empiezan a fallar más del 2-3%, hay un drift en el comportamiento del modelo o alguien modificó el prompt sin actualizar el schema esperado.
Degradación en scores de calidad: si mantienes evaluaciones automáticas (LLM-as-judge con criterios definidos), una caída sostenida del score promedio en una ventana de 1 hora es señal de regresión.
Cuándo NO necesitas LLMOps completo
Implementar Langfuse, OpenTelemetry y dashboards de observabilidad tiene un costo: tiempo de setup, infraestructura adicional, y overhead de desarrollo. No siempre está justificado.
No lo necesitas si: estás en prototipo o PoC con menos de 100 requests diarios. Si el sistema falla, lo sabes inmediatamente por feedback directo. La complejidad de setup no está justificada hasta que el volumen o la criticidad del sistema lo demanden.
Empieza con logging básico estructurado: antes de Langfuse, asegúrate de que cada llamada al LLM emite un log con al menos: timestamp, modelo, tokens de entrada/salida, latencia y si hubo error. Con eso puedes hacer queries en CloudWatch, Datadog o cualquier sistema de logs que ya tengas.
El trade-off de latencia: enviar spans a Langfuse es asíncrono y tiene impacto mínimo en producción (<5ms), pero hay overhead. Si tienes SLAs extremadamente estrictos de latencia (sub-100ms), mide el impacto antes de habilitarlo en el critical path.
Privacidad de datos: Langfuse Cloud almacena los prompts y respuestas completos. Si manejas datos PII o información confidencial, usa self-hosted obligatoriamente, o implementa un middleware que redacte datos sensibles antes de enviarlos al sistema de observabilidad.
Over-instrumentation: no necesitas un span por línea de código. Instrumenta a nivel de componentes lógicos: retrieval, generation, reranking, tool calls. Más granularidad de esa no aporta valor y satura el sistema de observabilidad.
El flujo de trabajo operativo completo
La observabilidad solo tiene valor si hay un proceso que la usa. Un flujo operativo mínimo viable:
Cada semana, revisa las trazas con scores de calidad más bajos: ¿qué tienen en común? ¿El retrieval recuperó contexto irrelevante? ¿El prompt system está desactualizado para esos casos? ¿El modelo alucina en un dominio específico?
Cada sprint, revisa la distribución de costos por endpoint y por usuario. Los 10% de requests más caros explican típicamente el 50-60% del costo total. Optimizar esos casos con prompt caching, context trimming o un modelo más pequeño para casos simples tiene impacto inmediato.
Antes de cada cambio en producción (nuevo prompt, nuevo modelo, nuevo chunking strategy), captura una muestra de 100-200 requests como baseline. Después del cambio, compara el p95 de latencia, el costo promedio y los scores de calidad. Esto convierte los despliegues de LLMs en experimentos medibles, no en apuestas.
La observabilidad no es el componente más glamoroso de un sistema con LLMs. Es el que te permite mantenerlo en producción sin apagarlo cada vez que algo sale mal.