Anatomía de una petición LLM en producción, mayo 2026: tour por las seis etapas siguiendo una sola request

TL;DR

El blog ha desplegado a lo largo de varias series las piezas que sostienen un sistema LLM en producción: la etapa Data (versionado de datasets, ingestión y vector stores, RAG sobre Kafka), la etapa Tune (fine-tuning continuo), la etapa Eval (evals como capa después del tracing, guardrails y safety), la etapa Deploy (KV cache, PagedAttention, disaggregated serving, cluster GPU multi-tenant, vLLM en Kubernetes, operators de LLM en K8s), la etapa Observe (tracing con AgentSight, MCP observability, eBPF + drift), la etapa Retrain (cerrar el bucle feedback → dataset → adapter), y los componentes transversales (prompt versioning y data versioning). Lo que falta es unirlo: ver una única petición atravesando todas las piezas en orden, en una historia coherente. Eso hace este post. Cogemos una request específica de un chatbot de soporte multi-tenant, la rebobinamos hacia atrás hasta los datos que entrenaron el adapter que la sirve hoy, la seguimos hacia adelante por el serving, la vemos llegar al store de feedback cuando el usuario marca thumbs-down, y la dejamos como semilla del próximo ciclo trimestral de retrain. El recorrido sirve como mapa mental y como guía del integrador: el sistema no se sostiene si una sola de las siete piezas (seis etapas + dos transversales) está rota o ausente. La lección práctica del tour no es ninguna nueva — es que todo está conectado, que las medidas locales mienten cuando se aíslan, y que el coste real de no operar bien una etapa lo paga otra etapa más adelante.

Estás aquí: todas las etapas a la vez

A diferencia de los posts anteriores, donde el mini-mapa marcaba una sola caja activa, este recorre todo el pipeline. Es el único post del blog que activa las seis etapas y los dos componentes transversales simultáneamente, porque seguimos una request real que las cruza todas.

Tour completo: una request atraviesa las 6 etapas y los 2 componentes transversales1 · Data2 · Tune3 · Eval4 · Deploy5 · Observe6 · RetrainPrompt versioning (Langfuse / MLflow Prompts)Data versioning (DVC / lakeFS) · Schema Registrytrace_id · prompt_id · prompt_version · dataset_id · dataset_version · model_id · model_version · deployment_idtrace que recorre todo el sistema

La analogía: análisis forense de una request

Cuando ocurre un accidente aéreo, el análisis forense no se limita a mirar los últimos segundos del vuelo. El equipo de investigación rebobina hasta el mantenimiento de los seis meses previos, los protocolos del fabricante, el currículo del piloto, el briefing meteorológico, las decisiones del controlador, la historia de incidentes en el mismo modelo. La conclusión rara vez es “el ala se rompió”; es “el ala se rompió porque un protocolo de inspección redactado de tal forma no detectaba microfisuras que el modelo de cálculo del 2014 no consideraba críticas y que sí lo eran a partir de cierto ciclo de fatiga”.

Cuando una petición LLM en producción falla o acierta, también hay una cadena causal larga detrás. La respuesta que el usuario ve es el último frame; lo que la determinó empieza meses antes y se ramifica por seis etapas operativas. Si sólo miras el último frame, atribuyes el resultado al modelo. Si miras la cadena entera, ves que el modelo es uno de doce factores y rara vez el más importante.

Este post hace ese análisis forense, pero al revés: en lugar de partir de un fallo y rebobinar, partimos de una request específica que funciona y desglosamos qué tuvo que pasar para que llegara a funcionar, y qué pasará después con ella. Es un tour guiado, no un diagnóstico de incidente. Pero la disciplina mental es la misma: ninguna etapa es autónoma, y entender el sistema significa entender los puentes entre etapas, no solo las cajas.

El escenario: chatbot de soporte multi-tenant para clientes regulados

Para el tour usamos un escenario concreto realista, lo bastante representativo como para que las observaciones se transporten a la mayoría de despliegues serios en mayo 2026. Es un producto SaaS de soporte al cliente con LLM, vendido a varios clientes corporativos (multi-tenant) en sectores regulados (banca, seguros, salud). El producto:

  • Acepta preguntas en lenguaje natural por chat embebido en la web del cliente.
  • Recupera fragmentos relevantes del knowledge base interno del cliente (documentos de producto, condiciones contractuales, FAQs).
  • Responde citando el fragmento utilizado, en el idioma del usuario, con tono adaptado a la marca.
  • Si la pregunta requiere una acción (cancelar, modificar plan, abrir caso), invoca una tool del catálogo expuesto vía MCP, con confirmación del usuario.
  • Está obligado por contrato a no inventar datos del cliente, a respetar la guía de estilo del tenant, y a cumplir compliance ENS / NIS2 sobre datos en la UE.

La infraestructura es on-premise sobre Kubernetes (RKE2 con CNI Cilium, en el patrón cubierto en Cilium BGP y CNI networking). El GPU plane es un cluster genérico 4×H100 SXM (80 GB cada una, NVLink) para serving e entrenamiento productivo, más una RTX 4090 (24 GB) para desarrollo, evals offline y reembeddings ad-hoc.

La request que vamos a seguir:

Usuario (cliente final, español, plan premium de una aseguradora): "¿Cómo cancelo mi suscripción premium? Llevo tres meses pagando y no he usado la mitad de las coberturas."

A primera vista, una pregunta simple. Vamos a ver qué hay detrás.

Paso 0 — El frame: la respuesta que el usuario ve

A las 11:47:32 de un martes, el usuario envía el mensaje. A los 4.2 segundos, ve aparecer en la UI:

Para cancelar tu suscripción premium, sigue estos pasos: (1) Accede al portal de cliente con tu DNI y contraseña. (2) Ve a Mis pólizasPremium Vida 2024. (3) Pulsa Solicitar cancelación. (4) Selecciona el motivo (en tu caso, “no uso del producto”) y confirma. Te llegará un correo de confirmación en 24-48 h.

Antes de cancelar, te recuerdo que tu plan incluye revisión médica anual gratuita y asistencia 24/7 en viaje, coberturas que se pierden con la cancelación. Si quieres, puedo ayudarte a revisar tu uso de coberturas en los últimos tres meses para ver si te conviene cambiar a un plan más ajustado en vez de cancelar.

Fuente: [Política de cancelación, sección 4.2 de Condiciones Generales 2024]

El usuario marca después de leerla un thumbs-down y deja en el formulario: “la respuesta es correcta pero el tono es demasiado vendedor; sólo quería saber cómo cancelar”.

Esa secuencia —pregunta, respuesta, thumbs-down con feedback estructurado— es el último frame visible. Vamos hacia atrás para entender qué tuvo que ocurrir para que la respuesta saliera así.

Rebobinando hacia atrás: lo que ya estaba en su sitio antes de la request

Antes de que el usuario escribiera, el sistema ya tenía un modelo cargado en serving, un prompt activo etiquetado como production, un índice vectorial actualizado, un dataset versionado del último fine-tuning, y un golden eval set que validó la promotion. Cada uno de esos artefactos llegó allí por un proceso. Recorremos cuatro saltos hacia atrás.

t = −90 días — Etapa Retrain anterior cierra el ciclo previo

Hace tres meses, durante un ciclo de Retrain trimestral, ocurrieron dos cosas. La primera: el equipo de soporte revisó el feedback acumulado de los seis meses previos y vio un patrón —el modelo respondía con tono excesivamente formal a usuarios premium, que reportaban “se siente robótico”—. La segunda: un incidente puntual (un cliente cancela por una respuesta percibida como brusca) disparó un mini-ciclo incident-driven.

El proceso, en detalle cubierto en el post de Retrain, siguió cinco sub-procesos:

  1. Captura de feedback — thumbs-down explícitos + feedback implícito (abandonments, retries) acumulados en una tabla feedback_signals de Postgres, todos con trace_id que permite rebobinar hasta el contexto exacto.
  2. Triage por causa raíz — el cluster de incidentes “tono brusco” se categorizó como prompt issue (no era el modelo respondiendo mal, era el system prompt que pedía un registro demasiado formal). Un sub-cluster era model issue (en algunos casos el modelo se cerraba en banda incluso con un prompt más cálido).
  3. Enriquecimiento del dataset — el equipo anotó manualmente 280 casos donde el modelo fue demasiado brusco, etiquetados con la respuesta de referencia (“cómo debería haber respondido”). Doble anotación en el 20% críticos; los casos con quality score < 4 quedaron fuera.
  4. Decisión de cadencia — el incidente se trató como incident-driven; el resto del Retrain trimestral siguió calendario.
  5. Promotion — el nuevo adapter customer_support_v7 pasó por eval gates contra customer_support_v6, canary 5% durante una semana, y se promovió cuando las métricas del golden set mostraron mejora estable en el segmento “tono / claridad” sin regresiones en el resto.

Resultado: el adapter activo en producción cuando el usuario envió la request del Paso 0 es customer_support_v7, entrenado sobre el dataset enriquecido enriched_retrain_2026_q1 versión 3, con doble lineage hasta el incidente original.

t = −60 días — Etapa Data: el dataset enriquecido se versiona y entra a circulación

Inmediatamente después de Retrain, la etapa Data del pipeline LLMOps hace su trabajo. Tres operaciones críticas, cubiertas en detalle en el post de data versioning:

  • Versionado inmutable del dataset enriquecido con DVC, hash sha256 propagado al registry. El identificador (enriched_retrain_2026_q1, v3, sha256:9af...) se convierte en el ticket de equipaje que recorrerá las próximas etapas.
  • Schema contract validado por CI: cada fila cumple el JSON Schema del entry esperado por el trainer (example_id, input.user_query, input.retrieved_context, expected_output, rubric, segment, difficulty). Una validación falla en CI si alguna fila rompe el contract.
  • Holdout segregation check: hash sha256 normalizado de cada input se compara contra todos los hashes del golden eval set activo (customer_support_golden_v12). Cero solapamientos = el dataset no contamina la eval. Si hubiera habido uno solo, el CI habría bloqueado el merge.

En paralelo, el corpus RAG (manuales de producto, FAQs, condiciones generales del tenant aseguradora) se mantiene vivo. El pipeline de ingestión sigue capturando cambios desde el CMS del cliente: una nueva sección de la política de cancelación se modificó en febrero y se reindexó en Qdrant. Como cuenta el post sobre RAG sobre Kafka, el corpus no se reentrena con cada cambio: se reembedea solo el delta, y lakeFS mantiene un branch del bucket de embeddings con la versión nueva. El branch se mergea a main cuando el recall@10 sobre un set de queries representativas se mantiene por encima del threshold (0.78 en este sistema).

t = −45 días — Etapa Tune: el adapter customer_support_v7 se entrena

Tres semanas tras cerrar el dataset, el entrenamiento del nuevo adapter LoRA arranca. Como detalla el post de fine-tuning continuo, el patrón productivo en 2026 evita reentrenar el modelo base — costoso, lento, irreversible — y favorece adapter LoRA sobre un modelo base estable (en este sistema, Llama 3 70B-instruct cuantizado a INT8 para serving). El entrenamiento:

  • Corre sobre 4 de las H100 (NVLink, tensor parallel) durante ~6 horas.
  • Usa transformers + PEFT + bitsandbytes, con monitoring por MLflow.
  • Cada step registra el dataset_id, dataset_version, dataset_hash como input artifact en MLflow.
  • El output —un fichero customer_support_v7.safetensors de ~280 MB con los pesos LoRA— se sube a MinIO con su propio hash, y MLflow registra model_id, model_version, parent_dataset.

A este punto, la cadena de lineage está cerrada en este tramo:

enriched_retrain_2026_q1, v3, sha256:9af...
        │
        ▼
mlflow run train, run_id: 0xa721...
        │
        ▼
customer_support_v7, sha256:5c1...

t = −38 días — Etapa Eval: el adapter v7 pasa por eval gates

El adapter recién entrenado no se promociona. Pasa por una suite de evals cubierta en detalle en el post sobre evals. El golden eval set —customer_support_golden_v12, 850 ejemplos curados por humanos, con kappa inter-anotador 0.81— se ejecuta contra dos modelos: el adapter v7 candidato y el v6 actualmente en producción. Las métricas:

Métricav6 (prod)v7 (cand.)Threshold
Faithfulness al fragmento RAG0.870.89≥ 0.82
Toxicidad (low is good)0.0120.011≤ 0.02
Tono “cálido pero profesional” (judge LLM)0.710.84≥ 0.78
Format compliance (markdown estructurado)0.940.93≥ 0.90
Helpful-but-not-pushy (judge LLM)0.660.79≥ 0.75
Latency p95 (ms)28402910≤ 3500

A esto se añade la suite de guardrails y safety cubierta en el post de guardrails: jailbreak resistance, PII leakage detection, prompt injection sobre tools MCP. El v7 mejora en safety en dos métricas y empata en el resto.

El v7 entra al canary 5% del tráfico durante 7 días, manteniendo monitoreo cercano. Al final del canary, las métricas online confirman lo que el offline anticipaba: mejora en tono y helpfulness, latencia equivalente, sin nuevos modos de fallo. Promotion aprobada. El v7 pasa al label production.

t = −31 días — Etapa Deploy: el adapter v7 entra a serving

El adapter customer_support_v7 se promueve al cluster de serving. Tres piezas cubiertas en posts independientes entran en juego.

vLLM como motor de inferencia. El motor vive sobre Kubernetes, deployado vía un Operator dedicado, como cuenta el post sobre operators de LLM y el post sobre vLLM en K8s. El operator es responsable de detectar el nuevo adapter en el registry, hot-loadearlo sin reiniciar el motor (capacidad nativa de vLLM con --enable-lora), y dirigir tráfico a partir del label.

Disaggregated serving. Como detalla el post sobre disaggregated serving, el sistema separa prefill (intensivo en compute, throughput-bound) y decode (intensivo en memoria, latencia-bound) en pools de GPUs diferentes. La request del usuario, cuando llegue, prefila en un pod especializado y decodea en otro, comunicándose por NVLink + un fabric KV cache compartido.

Cluster GPU multi-tenant. El cluster H100 sirve a varios tenants, no solo a la aseguradora del Paso 0. Como cuenta el post sobre cluster multi-tenant, el aislamiento se materializa en cuatro planos: namespace de Kubernetes, ACLs sobre adapters (sólo el namespace del tenant carga sus LoRAs), partitioning del KV cache por tenant (un tenant no puede leer prefijos cacheados de otro), y quota de tokens/minuto enforzada en el gateway.

Prompt registry sincronizado. El system_prompt del producto vive en Langfuse con label production. La versión activa es customer_support_system_prompt, versión 12. El gateway lee el prompt de Langfuse en el path de la request (con cache de pocos segundos para no martillear el registry). Detallado en el post de prompt versioning.

Resultado en t = −31 días: la combinación (adapter v7, prompt v12, golden v12) está activa y servida. El sistema está listo para la request que llegará 31 días más tarde.

Avanzando: la request del usuario atraviesa el sistema

Volvemos al Paso 0: 11:47:32 de un martes. El usuario pulsa Enter. Vamos en tiempo real, en milisegundos.

t = 0 ms — Ingreso por el gateway

El navegador del usuario hace POST a chat.aseguradora-ejemplo.com/api/chat. El tráfico atraviesa el edge load balancer y entra al API gateway del producto SaaS. El gateway:

  • Autentica el JWT del usuario (cliente final del tenant aseguradora).
  • Extrae el tenant_id, valida que su quota de tokens/minuto no esté agotada.
  • Resuelve qué model_id, adapter_id, prompt_id corresponden a este tenant y producto. En este caso: llama-3-70b-int8 + customer_support_v7 + prompt label production.
  • Construye un trace_id único (W3C TraceContext, propagable a OTel) y arranca un span raíz.

A los 8 ms, el gateway pasa la request al pool de prefill.

t = 8 ms — Pull del prompt versionado

Antes de servir, el cliente OpenAI-compatible que el motor usa internamente hace pull del system prompt activo. Como detalla el post sobre prompt versioning, el patrón es:

prompt = prompt_registry.pull(
    name="customer_support_system_prompt",
    label="production",  # apuntando ahora a v12
)
# Cache local de 30 s reduce el round-trip al 0.1 % de las requests

El span OTel del prompt pull lleva los atributos gen_ai.prompt.id = customer_support_system_prompt, gen_ai.prompt.version = 12, gen_ai.prompt.label = production. Quedan propagados a todos los hijos.

t = 12 ms — Retrieval RAG

El sistema necesita contexto de la base de conocimiento del tenant. Ejecuta:

query_embedding = encoder.encode(user_query)
chunks = qdrant.search(
    collection=f"tenant_{tenant_id}_kb_v3",
    vector=query_embedding,
    limit=4,
    score_threshold=0.72,
)
reranked = reranker.rerank(user_query, chunks, top_k=2)

A los 38 ms, el reranker devuelve dos fragmentos: uno de la Política de cancelación, sección 4.2 y otro de Beneficios del plan premium, sección 2.1. Como detalla el post sobre PostgreSQL + Qdrant, el corpus del tenant se mantiene aislado por colección y ACL: ningún tenant puede leer chunks de otro.

t = 40 ms — Construcción del payload final

El motor compone:

[system_prompt v12]
+ [contexto recuperado: 2 chunks]
+ [historial breve de la sesión: 1 turno previo]
+ [user query]

Total: ~1850 tokens de contexto. El span OTel registra gen_ai.request.input_tokens = 1850, gen_ai.request.model = llama-3-70b-int8, gen_ai.request.adapter = customer_support_v7.

t = 45 ms — Prefill

El payload entra al pool de prefill. La GPU procesa los 1850 tokens en una sola pasada paralela, computando para cada token sus vectores K y V (clave y valor de atención). Esos vectores se materializan como KV cache, cubierto en detalle en el post de fundamentos del KV cache. El cache resultante ocupa ~120 MB de VRAM en INT8.

Aquí aparece una optimización clave: el system prompt v12 está cacheado en el pool de prefill (prefix caching, cubierto en el post sobre PagedAttention). Como el system prompt es el mismo para esta tenant, los primeros ~500 tokens del contexto no se recomputan: se leen del cache de prefijo. Eso reduce el prefill efectivo de 1850 tokens a ~1350 tokens, ahorrando ~270 ms de compute.

A los 580 ms (prefill efectivo), el TTFT (time to first token) está listo. El primer token sale hacia el pool de decode.

t = 580 ms — Decode (streaming)

El pool de decode recibe el KV cache prefilled y empieza la generación token a token. Como detalla el post sobre disaggregated serving, la separación prefill/decode es lo que permite que un sistema multi-tenant mantenga TPS estable: el pool de decode está dimensionado para sostener miles de sesiones decodeando en paralelo a bajo coste por token, mientras el de prefill se dimensiona para bursts de TTFT cortos.

Generación a ~80 tokens/segundo. La respuesta tendrá ~290 tokens. Tiempo total de decode: ~3.6 s. Streaming: el usuario empieza a ver palabras desde t = 580 ms.

Mientras el decode avanza, el motor emite spans hijo en cada iteración con gen_ai.response.tokens_generated, gen_ai.response.cache_hit_ratio, gen_ai.response.cumulative_latency. El post sobre AgentSight y el post sobre MCP observability con OTel cubren la instrumentación detallada de esta capa.

t = 4 200 ms — Respuesta completa, span raíz cerrado

La generación termina. El motor cierra el span raíz con gen_ai.response.completion_tokens = 290, gen_ai.response.finish_reason = stop, gen_ai.response.total_latency_ms = 4200. El usuario ve la respuesta final. La sesión queda lista para un siguiente turno o para que el usuario haga clic en thumbs-up/thumbs-down.

A esta altura, todas las etapas activas han participado:

  • Data (pre-existente): el corpus RAG indexado, el dataset que entrenó el adapter, el golden set que lo validó.
  • Tune (pre-existente): el adapter v7 entrenado hace 45 días.
  • Eval (pre-existente): los gates que aprobaron la promotion.
  • Deploy (en este preciso instante): vLLM + disaggregated + KV cache + multi-tenant.
  • Observe (en este preciso instante): los spans OTel emitidos a Langfuse + Tempo, las métricas a Prometheus.
  • Retrain (a punto de activarse): el feedback que el usuario marcará en 15 segundos.

En paralelo: Observe está mirando

Mientras la request sucede, varias piezas de Observe corren en paralelo y dejan huella estructurada.

Tracing OTel. Cada span (gateway, prompt pull, retrieval, prefill, decode) viaja a Langfuse y a un colector OTel que los reenvía a un backend (Tempo / Jaeger). El trace_id único enlaza todos los spans. Como detalla el post sobre tracing con AgentSight, la propagación end-to-end es el principal habilitador del debug post-incidente: sin ella, no se puede reconstruir qué pasó tres semanas más tarde.

Métricas de runtime. El motor emite métricas Prometheus por intervalo: gpu_utilization, kv_cache_usage, tokens_per_second, queue_depth, prefill_latency_p95, decode_latency_p95. Las métricas no se asocian a un trace; son agregadas por tenant y servicio.

LLM-as-judge online. Un porcentaje configurable de respuestas (en este sistema, 2%) se ejecuta también por un judge LLM en background, que puntúa la respuesta contra una rúbrica simple (correcta / parcial / incorrecta + score de tono). El judge no bloquea la respuesta al usuario; alimenta el dashboard.

Drift estadístico. En paralelo, una pipeline más lenta computa drift sobre la distribución de inputs y outputs. Como cuenta el post sobre eBPF + drift, el monitoreo de bajo nivel (latencia, error rate por endpoint) se complementa con drift detection estadístico (KS test, embedding distance) que detecta cuando “algo va mal” antes de que un thumbs-down lo confirme.

Safety y guardrails monitor. El post sobre guardrails describe la capa que vigila intentos de jailbreak, PII leakage, prompt injection vía tools MCP. En este caso, ninguno se dispara.

Todas estas piezas operan continuamente, no por request. Pero esta request en particular dejó su huella en cada una de ellas.

El feedback: el bucle se cierra

A los 15 segundos de leer la respuesta, el usuario marca thumbs-down y deja en el formulario: “la respuesta es correcta pero el tono es demasiado vendedor; sólo quería saber cómo cancelar”. Ese gesto, aparentemente trivial, dispara una secuencia importante.

Inserción en feedback_signals

Como detalla el post sobre Retrain, el thumbs-down se persiste como una fila estructurada en una tabla Postgres:

INSERT INTO feedback_signals (
  signal_id, trace_id, request_id, signal_type, signal_value,
  prompt_id, prompt_version, model, user_segment, occurred_at
) VALUES (
  gen_random_uuid(),
  '4f5...',         -- el trace_id del Paso 0
  'r-22a...',       -- request_id
  'thumbs',
  '{"vote":"down","reason":"too pushy","text":"sólo quería saber cómo cancelar"}',
  'customer_support_system_prompt',
  12,
  'llama-3-70b-int8+customer_support_v7',
  'premium-es',
  '2026-05-19T11:47:51+02:00'
);

Con esto, la fila queda enlazada por trace_id a todo lo que ocurrió: prompt v12, contexto recuperado, output completo, métricas de latencia, score del judge (en este caso 0.82, considerado bueno por el judge pero el humano discrepa).

Triage por causa raíz

El equipo MLE pasa por triage la próxima mañana. Combinando reglas heurísticas, LLM-as-classifier y revisión humana:

  • La señal no es model issue: el modelo respondió correctamente al prompt que recibió.
  • No es retrieval issue: los chunks recuperados eran los correctos.
  • No es infra issue: la latencia fue normal.
  • Es prompt issue: el system prompt v12 instruye al modelo a “ofrecer alternativas antes de procesar acciones destructivas”. Esa instrucción genera el “tono vendedor” en algunos contextos.

El incidente se acumula con otros del mes en el cluster “tono vendedor”. Cuando el cluster supere un threshold (típicamente 30-50 incidentes del mismo tipo o un porcentaje del total), entrará a un mini-ciclo incident-driven o esperará al Retrain trimestral, dependiendo del tamaño.

El siguiente ciclo lo recoge

Tres meses más tarde, en el siguiente Retrain trimestral, este feedback es uno de muchos que motivarán dos cambios:

  • Nueva versión de prompt v13 con instrucción ajustada: “ofrecer alternativas sólo si el usuario no expresa intención clara de cancelar”.
  • Posible refuerzo del adapter con casos de tono más directo para premium-es. Si el cluster lo justifica.

El v13 entrará en su propia eval gate. El golden set crecerá con casos donde el tono correcto sea “directo, no vendedor”. El v8 del adapter (si llega) reentrenará sobre el dataset enriquecido enriched_retrain_2026_q2 que ya contiene este caso anotado.

El ciclo se cierra. La request del Paso 0 ha contribuido a la versión del sistema que servirá a otro usuario tres meses después.

Lo que va en cada trace: identidad y trazabilidad

Si el lector mira los siete identificadores omnipresentes en este recorrido, ve la red de identidades que permite todo lo anterior. Es la infraestructura de identidad del sistema LLM en producción:

trace_id           4f5...       (unique per request)
request_id         r-22a...     (idem)
prompt_id          customer_support_system_prompt
prompt_version     12
prompt_label       production
dataset_id         enriched_retrain_2026_q1
dataset_version    v3 (sha256:9af...)
model_id           llama-3-70b-int8
adapter_id         customer_support_v7 (sha256:5c1...)
deployment_id      d-prod-7b
schema_version     3.2
tenant_id          aseguradora-ejemplo
user_segment       premium-es
golden_set_id      customer_support_golden_v12

Si una sola pieza de ese conjunto falta o no propaga, la cadena se rompe. El siguiente incidente investigado caerá en “no podemos rebobinar hasta el origen porque el sistema no lo registró”. Por eso los componentes transversales —prompt versioning y data versioning— no son lujos: son la conexión sin la cual las otras seis etapas operan a ciegas.

Diagrama síntesis: cómo encajan las piezas

                  ┌─────────────────────────────────────────┐
                  │       Usuario (cliente final, B2C)      │
                  └─────────────────┬───────────────────────┘
                                    │ chat msg + JWT
                                    ▼
                  ┌─────────────────────────────────────────┐
                  │       Edge LB + WAF + Cilium CNI        │
                  └─────────────────┬───────────────────────┘
                                    │ HTTPS, mTLS interno
                                    ▼
              ┌─────────────────────────────────────────────────┐
              │  API Gateway (auth, quota, model routing)       │
              │  - Resuelve tenant → model + adapter + prompt   │
              │  - Inicia trace_id (W3C)                        │
              └──────┬─────────────────────┬────────────────────┘
                     │                     │
       (pull prompt) │                     │ (pull config)
                     ▼                     ▼
       ┌────────────────────┐    ┌──────────────────────┐
       │ Langfuse Prompt    │    │  Model registry      │
       │ Registry (v12)     │    │  (adapter v7)        │
       └─────────┬──────────┘    └──────────┬───────────┘
                 │                          │
                 └──────────┬───────────────┘
                            │ payload listo
                            ▼
              ┌──────────────────────────────────────────┐
              │  vLLM motor (K8s Operator)               │
              │  ┌──────────────┐    ┌──────────────┐    │
              │  │ Pool prefill │ →  │ Pool decode  │    │
              │  │  (H100×N)    │    │  (H100×M)    │    │
              │  └──────┬───────┘    └──────┬───────┘    │
              │         │ KV cache fabric  │            │
              │         └──────────────────┘            │
              │  - prefix caching del system prompt     │
              │  - PagedAttention                       │
              └──────┬───────────────────────────────────┘
                     │ tokens stream
                     ▼
              ┌─────────────────────────────────────────┐
              │   Usuario ve respuesta + UI thumbs/UX   │
              └─────────────────┬───────────────────────┘
                                │ feedback (15 s después)
                                ▼
              ┌─────────────────────────────────────────┐
              │   feedback_signals (Postgres)           │
              │   + Langfuse scores                      │
              └─────────────────┬───────────────────────┘
                                │
       ┌────────────────────────┼────────────────────────┐
       │                        │                        │
       ▼                        ▼                        ▼
  triage          ciclo Retrain trimestral        dataset_id
  causa raíz      o incident-driven                enriquecido (DVC)
                                                    │
                                                    ▼
                                              Tune del v8
                                              (próximo ciclo)


  En paralelo durante toda la request, instrumentación OTel:
  spans → Tempo / Jaeger ; eventos → Langfuse ; métricas → Prometheus

El stack on-premise aplicado

Llevar lo anterior a una infra on-premise genérica de perfil consultor (RTX 4090 + cluster 4×H100 SXM):

CapaRecursos típicos
Plano de redEdge LB (HAProxy / nginx ingress) + CNI Cilium con BGP, cubierto en Cilium BGP
Plano de cómputo K8sRKE2 con dos nodes managers + node pool de GPU
Plano GPU productivo4× H100 SXM (NVLink, 80 GB cada una), particionadas vía MIG en pools prefill/decode
Plano GPU desarrollo1× RTX 4090 (24 GB) para evals offline, drift-check embeddings, smoke tests
Plano storageMinIO o Ceph object store; DVC remote + lakeFS backend
Plano datos OLTPPostgres 18 con replicación; pgvector 0.8 para casos pequeños
Plano vectorQdrant o Milvus para corpus RAG grandes
Plano streamKafka (Redpanda / Apache puro) + Schema Registry; CDC con Debezium o Flink CDC
Plano observabilidadOTel Collector + Tempo (traces) + Prometheus (metrics) + Loki (logs); Langfuse para LLM-específico
Plano runtime securityTetragon, cubierto en post sobre runtime security

La densidad real no es la suma de las cajas: es la operativa que ata las cajas. Un cluster con todas las piezas pero sin disciplina de versionado, sin propagación de trace_id extremo a extremo, sin schema contracts y sin retraining cadenciado, es un cluster que sirve LLM una vez y que envejece. La diferencia entre un proyecto y una plataforma es exactamente eso.

Diez puentes entre etapas donde se rompe el sistema

El recorrido revela algo importante: los fallos rara vez están dentro de una etapa; están en los puentes entre etapas. Diez puentes habituales:

  1. Data → Tune: el dataset no propaga su (dataset_id, dataset_version) al trainer. Mismo dataset entrenado dos veces produce dos model_id que no se pueden distinguir.
  2. Tune → Eval: el modelo entrenado no propaga su lineage al run de eval. El eval pasa, pero no queda registrado contra qué dataset se entrenó. Tres meses después, irreproducible.
  3. Eval → Deploy: la promotion ocurre sin que el sistema de serving registre qué versión del adapter está sirviendo en cada instante. El día que el modelo da una respuesta peligrosa, no se sabe qué adapter respondió.
  4. Deploy → Observe: el motor no emite gen_ai.request.adapter, gen_ai.prompt.version, gen_ai.dataset.version como atributos del span. Los traces existen pero no se pueden cruzar con el lineage.
  5. Observe → Retrain: el feedback se captura en una herramienta (Langfuse, Phoenix) pero nadie lo lee. La etapa Retrain “está”, pero el feedback se acumula sin triagear.
  6. Retrain → Data: el dataset enriquecido se mete en el siguiente Tune sin pasar por la disciplina de versionado, schema contract y holdout check. Contaminación silenciosa del golden set.
  7. Prompt versioning ↔ todo: el prompt_id, prompt_version no se propaga a los spans. El día que el equipo descubre que un cambio de prompt regresionó el sistema, no puede aislar cuál ni cuándo.
  8. Data versioning ↔ todo: el dataset_id, dataset_version no aparece en el experiment tracking. Se “vuelve a entrenar v8” pero nadie puede demostrar que sea sobre el dataset enriquecido y no sobre el viejo.
  9. MCP ↔ tools: el sistema invoca tools (cancelación, modificación de pólizas) pero no registra gen_ai.tool.invocation_id enlazado al trace. Las acciones quedan disociadas de la respuesta que las generó.
  10. Schema Registry ↔ datos: los datasets versionan contenido pero no schema. Un breaking change en el expected_output rompe el eval silenciosamente; nadie nota nada hasta que un humano revisa los resultados.

Los puentes están cubiertos a lo largo del blog. La operativa los enforza. La cultura del equipo los mantiene.

Cómo recorrer el blog

Si llegas a este post desde fuera y quieres una ruta de lectura:

  1. El mapa: Pipeline LLMOps de seis etapas — el mapa maestro de todo lo demás.
  2. El contexto: MLOps específico para LLMs en 2026 — el panorama y por qué LLMOps no es MLOps clásico.
  3. Inferencia desde dentro hacia afuera: KV cachePagedAttention deep diveDisaggregated servingCluster GPU multi-tenantvLLM en K8sOperators LLM K8s.
  4. Datos: Data versioning con DVC y lakeFSPostgreSQL + Qdrant ingestiónRAG sobre Kafka.
  5. Tune: Fine-tuning continuo en producción.
  6. Eval: Evals: la capa después del tracingGuardrails y safety.
  7. Observe: AgentSight tracing LLMMCP observability con OTeleBPF on-device + drift.
  8. Retrain: Cerrar el bucle feedback → dataset → adapter.
  9. Transversales: Prompt versioning con Langfuse y MLflow.
  10. Infra de soporte (la base sobre la que se monta todo): RKE2 con Cilium BGP, Hubble + observabilidad eBPF, Tetragon runtime security.

Lo que no hemos cubierto (todavía)

A primer nivel está lo principal. Los siguientes posts del blog —cuando los temas lo justifiquen— podrían profundizar en:

  • Schema Registry para LLM data y prompts: la otra mitad del data contract.
  • AI Gateway dedicado: LiteLLM, Portkey, Kong AI Gateway como plano de control.
  • OTel gen_ai semantic conventions: el estándar emergente que ata los siete identificadores del bloque “identidad” en spans bien formados.
  • Federated learning sobre datos de clientes regulados: cómo entrenar sin centralizar el corpus.
  • Capacity planning para clusters multi-tenant compartidos.
  • Disaster recovery de un servicio LLM: cómo reproducir el estado del sistema 30 días atrás.
  • Cost accounting por tenant: tokens × pesos × adapter × infraestructura → factura.

Ver también

Referencias