<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mlops on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/mlops/</link><description>Recent content in Mlops on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 21 May 2026 07:15:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/mlops/index.xml" rel="self" type="application/rss+xml"/><item><title>El cluster GPU como plataforma: cómo convertir un cluster compartido en un servicio multi-tenant que tus equipos puedan consumir</title><link>https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/</link><pubDate>Thu, 21 May 2026 07:15:00 +0200</pubDate><guid>https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Tener un cluster de GPUs caro y muchas cargas distintas que lo quieren usar no es un problema de &lt;strong>infraestructura&lt;/strong>: es un problema de &lt;strong>producto interno&lt;/strong>. Lo que separa &amp;ldquo;tenemos un cluster&amp;rdquo; de &amp;ldquo;tenemos una plataforma de inferencia&amp;rdquo; son cuatro capas que el mercado ha consolidado en 2026: una &lt;strong>capa de gateway&lt;/strong> que centraliza autenticación, routing y políticas (LiteLLM, Portkey, Kong AI Gateway); un &lt;strong>modelo de aislamiento GPU&lt;/strong> apropiado al perfil de los tenants (MIG hardware-isolation para multi-tenant no confiable, MPS para procesos del mismo equipo, time-slicing solo para dev); un &lt;strong>sistema de quotas y rate limiting&lt;/strong> con presupuestos por tenant/equipo/proyecto (LiteLLM lo hace en su core a nivel team/user/api-key con 429s descriptivos); y un &lt;strong>plano de observabilidad multi-tenant&lt;/strong> que permite cost attribution real (showback como paso intermedio, chargeback como destino), tracing por tenant y dashboards diferenciados. Aplicado a un cluster GPU mid-scale típico (un nodo con 4-8 H100 SXM y NVLink, un punto habitual para empezar en producción), esto se traduce en decisiones concretas: con ~640 GB de VRAM agregada en 8 GPUs y dos modelos típicos en producción (un modelo grande de 70B+ con tensor parallel y un modelo mediano replicado), el cluster sirve entre &lt;strong>decenas y bajos centenares de sesiones simultáneas&lt;/strong> según mix; el aislamiento GPU se suele resolver con &lt;strong>MIG en cargas inferiores y dedicación per-model&lt;/strong> en cargas grandes; y la métrica de éxito de la plataforma es la &lt;strong>utilización efectiva&lt;/strong>, que en producción típica está en &lt;strong>30-40%&lt;/strong> y el objetivo razonable de optimización es subirla a 60-70% sin degradar SLA.&lt;/p>
&lt;blockquote>
&lt;p>Este es el &lt;strong>quinto post de la serie MLOps para LLMs&lt;/strong>. Es el más operacionalmente orientado y atraviesa varias etapas del pipeline (Deploy + Observe + transversales). El &amp;ldquo;estás aquí&amp;rdquo; señala las dos etapas activas porque la noción de plataforma multi-tenant no vive en una sola.&lt;/p>
&lt;/blockquote>
&lt;h2 id="estás-aquí-deploy--observe-cluster-como-producto">Estás aquí: Deploy + Observe (cluster como producto)&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy + Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7adb7a;stroke-width:3}.active2{fill:#c47aff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mt1)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mt1)}&lt;/style>
&lt;defs>&lt;marker id="mt1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DEPLOY + OBSERVE · el cluster como plataforma con tenants&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box active2"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-pregunta-que-cambia-el-marco">La pregunta que cambia el marco&lt;/h2>
&lt;p>Cuando un equipo de plataforma adquiere hardware GPU caro y empieza a montar inferencia, la primera versión casi siempre es &lt;strong>mononosa&lt;/strong>: un modelo, un cliente, una latencia objetivo. Funciona. Cuando llega el segundo equipo pidiendo el mismo recurso, &lt;strong>la mononosa se vuelve política interna&lt;/strong>: ¿cuántas réplicas le damos? ¿Qué hacemos si chocan los SLA? ¿Quién paga los tokens del experimento del equipo B? Y cuando llega el tercero, lo que era un proyecto de SRE pasa a ser un proyecto de &lt;strong>producto interno&lt;/strong>.&lt;/p>
&lt;p>La distinción no es técnica, es de marco. &lt;strong>Un cluster es infra&lt;/strong>. &lt;strong>Una plataforma es un servicio con clientes, contratos y métricas de éxito&lt;/strong>. El cambio de marco implica:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Clientes identificables&lt;/strong> (tenants), no usuarios anónimos.&lt;/li>
&lt;li>&lt;strong>Contratos&lt;/strong> (latency SLA, throughput garantizado, modelos disponibles), no &amp;ldquo;lo que dé tiempo&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Métricas de éxito&lt;/strong> que no son técnicas sino de producto: adopción, satisfaction, cost per query por tenant, tiempo del primer &amp;ldquo;hello world&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>Este post recorre cómo se opera ese cambio de marco. Lo aterriza sobre un &lt;strong>cluster mid-scale (4-8 H100 SXM con NVLink en un solo nodo)&lt;/strong>, configuración habitual cuando se empieza con inferencia LLM seria; pero los principios se generalizan a cualquier topología, desde un nodo único con dos GPUs hasta clusters multi-nodo con InfiniBand.&lt;/p>
&lt;h2 id="las-cuatro-capas-de-una-plataforma-de-inferencia-multi-tenant">Las cuatro capas de una plataforma de inferencia multi-tenant&lt;/h2>
&lt;p>La arquitectura canónica que se ha establecido en 2026 tiene &lt;strong>cuatro capas&lt;/strong> que cualquier plataforma multi-tenant seria implementa, en orden de afuera hacia adentro:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 410" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Cuatro capas plataforma multi-tenant">
&lt;style>.title{font:700 13px sans-serif;fill:#222}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#555}.tiny{font:10px sans-serif;fill:#666}.layer{stroke:#444;stroke-width:1.5;rx:6}.gw{fill:#ffe9d6}.pol{fill:#d6eaff}.iso{fill:#d9f5d6}.obs{fill:#e9d6f5}.cluster{stroke:#666;stroke-dasharray:4 2;fill:none}.tenant{stroke:#888;stroke-width:1.4;fill:#fffce6;rx:4}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#pm1)}&lt;/style>
&lt;defs>&lt;marker id="pm1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="22" text-anchor="middle" class="title">Las cuatro capas de la plataforma multi-tenant&lt;/text>
&lt;rect x="40" y="50" width="100" height="40" class="tenant"/>&lt;text x="90" y="68" text-anchor="middle" class="sm">Tenant A&lt;/text>&lt;text x="90" y="82" text-anchor="middle" class="tiny">soporte chat&lt;/text>
&lt;rect x="160" y="50" width="100" height="40" class="tenant"/>&lt;text x="210" y="68" text-anchor="middle" class="sm">Tenant B&lt;/text>&lt;text x="210" y="82" text-anchor="middle" class="tiny">RAG legal&lt;/text>
&lt;rect x="280" y="50" width="100" height="40" class="tenant"/>&lt;text x="330" y="68" text-anchor="middle" class="sm">Tenant C&lt;/text>&lt;text x="330" y="82" text-anchor="middle" class="tiny">agente code&lt;/text>
&lt;rect x="400" y="50" width="100" height="40" class="tenant"/>&lt;text x="450" y="68" text-anchor="middle" class="sm">Tenant D&lt;/text>&lt;text x="450" y="82" text-anchor="middle" class="tiny">data extr.&lt;/text>
&lt;rect x="520" y="50" width="100" height="40" class="tenant"/>&lt;text x="570" y="68" text-anchor="middle" class="sm">Tenant E&lt;/text>&lt;text x="570" y="82" text-anchor="middle" class="tiny">batch ETL&lt;/text>
&lt;rect x="640" y="50" width="100" height="40" class="tenant"/>&lt;text x="690" y="68" text-anchor="middle" class="sm">notebooks&lt;/text>&lt;text x="690" y="82" text-anchor="middle" class="tiny">research&lt;/text>
&lt;rect x="40" y="120" width="700" height="60" class="layer gw"/>
&lt;text x="390" y="144" text-anchor="middle" class="lbl">Capa 1 · AI Gateway&lt;/text>
&lt;text x="55" y="166" class="sm">Auth (OIDC/API keys) · Routing por modelo · Failover · Caching · Logging · OTel emission · Rate limiting&lt;/text>
&lt;rect x="40" y="195" width="700" height="60" class="layer pol"/>
&lt;text x="390" y="219" text-anchor="middle" class="lbl">Capa 2 · Policy &amp;amp; Quota Plane&lt;/text>
&lt;text x="55" y="241" class="sm">Quotas RPS/TPM por tenant · Budgets mensuales · Whitelist modelos · Priority classes · Admission control&lt;/text>
&lt;rect x="40" y="270" width="700" height="60" class="layer iso"/>
&lt;text x="390" y="294" text-anchor="middle" class="lbl">Capa 3 · Isolation Plane&lt;/text>
&lt;text x="55" y="316" class="sm">MIG / MPS / time-slicing · Namespaces K8s · NetworkPolicies · ResourceQuotas · Priority + preemption&lt;/text>
&lt;rect x="40" y="345" width="700" height="55" class="layer obs"/>
&lt;text x="390" y="369" text-anchor="middle" class="lbl">Capa 4 · Observability Plane (multi-tenant)&lt;/text>
&lt;text x="55" y="391" class="sm">Traces con tenant_id · Métricas labeled · Cost attribution · Dashboards por tenant · Audit logs&lt;/text>
&lt;path class="arr" d="M90,90 L390,120"/>
&lt;path class="arr" d="M450,90 L390,120"/>
&lt;path class="arr" d="M690,90 L390,120"/>
&lt;/svg>
&lt;/div>
&lt;p>Cada capa resuelve un problema concreto. Vamos a una por una.&lt;/p>
&lt;h2 id="capa-1--ai-gateway-la-puerta-de-entrada-única">Capa 1 — AI Gateway: la puerta de entrada única&lt;/h2>
&lt;p>El &lt;strong>AI Gateway&lt;/strong> es el componente que tus tenants ven. Es una API HTTP/gRPC compatible con OpenAI (típicamente &lt;code>/v1/chat/completions&lt;/code>, &lt;code>/v1/embeddings&lt;/code>, &lt;code>/v1/models&lt;/code>) que &lt;strong>centraliza&lt;/strong> todo lo que pasa antes de tocar los backends de inferencia.&lt;/p>
&lt;h3 id="por-qué-centralizar">Por qué centralizar&lt;/h3>
&lt;p>Sin gateway, los tenants se conectan directamente a vLLM o al modelo que sea. Cada cambio (rotar un endpoint, añadir un modelo, cambiar credenciales, aplicar política) requiere notificar a todos los tenants. Cada tenant tiene su propia lógica de retry, su propio logging, su propio modelo de auth. Es inoperable a partir del tercer cliente.&lt;/p>
&lt;p>Con gateway, &lt;strong>el cambio se hace en un sitio&lt;/strong>. Los tenants tienen una URL estable y unas credenciales; el resto es problema del gateway.&lt;/p>
&lt;h3 id="las-tres-opciones-dominantes-2026">Las tres opciones dominantes 2026&lt;/h3>
&lt;p>&lt;strong>&lt;a href="https://docs.litellm.ai/">LiteLLM&lt;/a>&lt;/strong> es la opción &lt;strong>OSS más popular&lt;/strong>, Python-first, modelo de despliegue como proxy. Soporta &lt;strong>100+ proveedores&lt;/strong> (OpenAI, Anthropic, Bedrock, vLLM self-hosted, Ollama, etc.) detrás de una API OpenAI-compatible unificada. &lt;strong>Hierarchy nativa multi-tenant&lt;/strong> con Organizations → Teams → Users → API Keys, cada nivel con budget independiente. Versión Apache 2.0 cubre lo básico; &lt;strong>RBAC, SSO, audit logs y team-level enforcement requieren versión Enterprise paga&lt;/strong>. Despliegue en K8s con Helm chart oficial.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://portkey.ai/">Portkey&lt;/a>&lt;/strong> es la opción &lt;strong>comercial / SaaS&lt;/strong> más madura. Single control plane que enforces budgets, quotas, permissions, compliance. &lt;strong>Real-time spending tracking&lt;/strong> con alerting. RBAC, audit, workspaces, SSO incluidos. Trade-off: dependencia de un servicio externo y modelo de pricing por requests.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">Kong AI Gateway&lt;/a>&lt;/strong> es la opción para organizaciones &lt;strong>que ya tienen Kong como API gateway&lt;/strong>. Plug-in AI sobre el gateway Kong existente, integra con su modelo de plugins, consumers y rate-limits. Si tu equipo de plataforma ya opera Kong, es la fricción más baja.&lt;/p>
&lt;h3 id="cuándo-elegir-cada-uno">Cuándo elegir cada uno&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Situación&lt;/th>
&lt;th>Gateway&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>OSS puro, self-host, equipo Python-first&lt;/td>
&lt;td>&lt;strong>LiteLLM&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Necesitas RBAC, SSO, audit log out-of-the-box, presupuesto disponible&lt;/td>
&lt;td>&lt;strong>Portkey&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ya operas Kong como API gateway corporativo&lt;/td>
&lt;td>&lt;strong>Kong AI Gateway&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Greenfield enterprise con compliance estricto&lt;/td>
&lt;td>Portkey (probablemente)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Empresa media OSS-first sin compliance regulado&lt;/td>
&lt;td>LiteLLM (típicamente)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="lo-que-el-gateway-tiene-que-hacer-mínimo">Lo que el gateway tiene que hacer mínimo&lt;/h3>
&lt;p>Independientemente de la opción, lo que cualquier deployment serio debe enforcer:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Auth y identidad&lt;/strong>: cada request lleva una API key resoluble a un tenant + usuario + equipo.&lt;/li>
&lt;li>&lt;strong>Routing por modelo&lt;/strong>: el tenant pide &lt;code>model: &amp;quot;gpt-4o&amp;quot;&lt;/code>; el gateway decide si va a OpenAI, a Azure OpenAI, a tu vLLM con Qwen3 32B (fallback más barato), según política.&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong>: RPS por tenant, TPM (tokens por minuto), concurrency limits.&lt;/li>
&lt;li>&lt;strong>Caching de respuestas idénticas&lt;/strong>: 5-30% de las queries de RAG son repetidas; cachear ahorra latencia y coste.&lt;/li>
&lt;li>&lt;strong>OTel emission&lt;/strong>: cada llamada produce un span con &lt;code>gen_ai.*&lt;/code> semantic conventions y &lt;code>tenant_id&lt;/code> como atributo. Cubierto en &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">post de Evals&lt;/a> y &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Failover&lt;/strong>: si vLLM se cae, el gateway redirige a OpenAI API. Si OpenAI rate-limita, el gateway tira a Anthropic. Política configurable.&lt;/li>
&lt;/ul>
&lt;h3 id="ejemplo-de-configuración-litellm-multi-tenant">Ejemplo de configuración LiteLLM multi-tenant&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># litellm-config.yaml — ejemplo simplificado&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">model_list&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">litellm_params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/llama-3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-llama3-70b.inference/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/VLLM_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qwen3-32b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">litellm_params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/qwen3-32b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-qwen3-32b.inference/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/VLLM_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">litellm_params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">router_settings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routing_strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">usage-based-routing-v2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fallbacks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">llama-3-70b&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">qwen3-32b, gpt-4o] &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># si vLLM cae, fallback al externo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">general_settings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">master_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/LITELLM_MASTER_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database_url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/DATABASE_URL &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Postgres para budgets/keys&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Hierarchy: Organizations → Teams → Users → API Keys&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Se crean vía API, no en YAML estático&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Crear un team con presupuesto:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://litellm/team/new &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LITELLM_MASTER_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;team_alias&amp;#34;: &amp;#34;soporte-chat&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_budget&amp;#34;: 500, # 500 USD/mes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;budget_duration&amp;#34;: &amp;#34;30d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;tpm_limit&amp;#34;: 100000, # 100K tokens/min
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;rpm_limit&amp;#34;: 1000, # 1000 requests/min
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;models&amp;#34;: [&amp;#34;llama-3-70b&amp;#34;, &amp;#34;qwen3-32b&amp;#34;] # acceso a estos
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y la API key del team:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://litellm/key/generate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LITELLM_MASTER_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;team_id&amp;#34;: &amp;#34;&amp;lt;team-id&amp;gt;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;duration&amp;#34;: &amp;#34;30d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;metadata&amp;#34;: {&amp;#34;environment&amp;#34;: &amp;#34;production&amp;#34;, &amp;#34;app&amp;#34;: &amp;#34;support-bot&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esa API key es lo que el tenant usa. Cada request que pase con ella consumirá del budget del team. Cuando se agote, LiteLLM devuelve &lt;strong>HTTP 429&lt;/strong> con descripción.&lt;/p>
&lt;h2 id="capa-2--policy--quota-plane-qué-puede-hacer-cada-tenant">Capa 2 — Policy &amp;amp; Quota Plane: qué puede hacer cada tenant&lt;/h2>
&lt;p>El gateway es donde se enforza. La política es &lt;strong>lo que se enforza&lt;/strong>. Cinco ejes de política multi-tenant:&lt;/p>
&lt;h3 id="quotas-técnicas">Quotas técnicas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>TPM&lt;/strong> (tokens por minuto): el límite duro de consumo. Para un Llama 3 70B en TP=5, ~3000 tokens/s salidos sostenidos = 180K TPM agregados. Si tienes 10 tenants, asignar 18K cada uno como techo.&lt;/li>
&lt;li>&lt;strong>RPS / RPM&lt;/strong>: control de carga, no de consumo. Una sesión de 4K tokens cuenta como una request; un batch de 100 mini-completions también. Útil contra abuso.&lt;/li>
&lt;li>&lt;strong>Concurrency&lt;/strong>: cuántas requests simultáneas activas por tenant. Importante para SLA de latencia: 100 RPS con concurrency=50 se traducen en 2 segundos por request.&lt;/li>
&lt;/ul>
&lt;h3 id="budgets-económicos">Budgets económicos&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Mensual por tenant&lt;/strong>: hard cap en USD.&lt;/li>
&lt;li>&lt;strong>Diario y por hora&lt;/strong>: soft caps para evitar runaway en un solo día.&lt;/li>
&lt;li>&lt;strong>Por proyecto / API key&lt;/strong>: granularidad fina dentro de un mismo tenant.&lt;/li>
&lt;/ul>
&lt;p>LiteLLM tiene un campo &lt;code>max_budget&lt;/code> en cada nivel de la jerarquía (organization, team, user, api key). Los presupuestos se heredan/restringen hacia abajo.&lt;/p>
&lt;h3 id="whitelist-y-blacklist-de-modelos">Whitelist y blacklist de modelos&lt;/h3>
&lt;p>Tenants con cargas críticas → solo modelos estables (&lt;code>llama-3-70b&lt;/code>, &lt;code>gpt-4o&lt;/code>). Tenants de investigación → acceso también a modelos experimentales.&lt;/p>
&lt;h3 id="priority-classes">Priority classes&lt;/h3>
&lt;p>No todos los requests son iguales. Tres clases típicas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Guaranteed&lt;/strong>: cargas con SLA, latencia respetada incluso bajo presión.&lt;/li>
&lt;li>&lt;strong>Best-effort&lt;/strong>: cargas normales sin SLA estricto.&lt;/li>
&lt;li>&lt;strong>Spot&lt;/strong>: batches que pueden esperar, evictable si llega un guaranteed.&lt;/li>
&lt;/ul>
&lt;p>El &lt;a href="https://arxiv.org/abs/2603.00356">paper Token Management in Multi-Tenant AI Inference Platforms&lt;/a> (2026) formaliza esto con un &lt;strong>modelo de token pools por priority class&lt;/strong> que se ha empezado a adoptar en producción. Mantiene &lt;strong>P99 latency garantizada&lt;/strong> para guaranteed workloads incluso bajo overload, throttling selectivo sobre spot.&lt;/p>
&lt;h3 id="admission-control">Admission control&lt;/h3>
&lt;p>Antes de aceptar una request: ¿hay capacidad? Si no, devolver 429 inmediatamente en vez de encolar y degradar a todos. Es la disciplina operacional más infravalorada — un cluster con admission control bien hecho tiene &lt;strong>latencia predecible&lt;/strong>; sin él, &lt;strong>catastrophic degradation&lt;/strong> cuando llega el pico.&lt;/p>
&lt;h3 id="el-patrón-típico-en-2026">El patrón típico en 2026&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Política conceptual para un tenant &amp;#34;soporte-chat&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">tenant&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">soporte-chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">quotas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tpm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rpm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">max_concurrency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">budget&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">monthly_usd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">800&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">alert_thresholds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="m">0.5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.95&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># avisa cuando llegues&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">models_allowed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">llama-3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">qwen3-32b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">priority&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">guaranteed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">fallback_on_overload&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">qwen3-32b &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># si guaranteed se llena, fallback&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">gpt-4o-mini &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># último recurso, modelo externo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="capa-3--isolation-plane-aislar-las-cargas-físicamente">Capa 3 — Isolation Plane: aislar las cargas físicamente&lt;/h2>
&lt;p>Esta es la capa más densa técnicamente. Tienes un nodo con varias GPUs H100 SXM interconectadas por NVLink. ¿Cómo las particionas entre tenants?&lt;/p>
&lt;h3 id="tres-mecanismos-nvidia-para-compartir-gpu">Tres mecanismos NVIDIA para compartir GPU&lt;/h3>
&lt;p>&lt;strong>MIG (Multi-Instance GPU)&lt;/strong> es el aislamiento más fuerte. Particiona la GPU en hasta &lt;strong>7 instancias&lt;/strong> con &lt;strong>memoria HBM separada físicamente&lt;/strong> y &lt;strong>compute units (SMs) dedicados&lt;/strong>. Los tenants en MIG diferentes no pueden tocarse: una carga no consume memoria que otra necesita, una no degrada el throughput de otra. &lt;strong>Aislamiento hardware&lt;/strong>. Disponible en A100, H100, B100, B200.&lt;/p>
&lt;p>&lt;strong>MPS (Multi-Process Service)&lt;/strong> es soft. Múltiples procesos comparten la GPU concurrentemente, NVIDIA reparte SMs según uso. Buen rendimiento si todos los procesos son tuyos y confías en ellos. Peor para multi-tenant entre clientes que no se conocen porque un proceso ruidoso puede degradar a los otros.&lt;/p>
&lt;p>&lt;strong>Time-slicing&lt;/strong> es lo más simple: la GPU se asigna alternadamente, slot por slot, a procesos distintos. Latencia mucho peor (waits entre slots); no se recomienda para cargas de producción con SLA.&lt;/p>
&lt;h3 id="la-elección-para-multi-tenant-2026">La elección para multi-tenant 2026&lt;/h3>
&lt;p>Según el survey de adopción enterprise: &lt;strong>80% usa MIG para multi-tenant no confiable&lt;/strong> (clientes distintos que no se conocen) y &lt;strong>MPS para entornos confiados&lt;/strong> (procesos del mismo equipo) donde quieres maximizar throughput. Time-slicing solo se usa en dev/staging para que cada developer toque GPU sin coste de exclusividad.&lt;/p>
&lt;p>Limitación importante de MIG: &lt;strong>aísla compute y memoria HBM&lt;/strong>, pero &lt;strong>el camino PCIe sigue siendo compartido&lt;/strong>. Para cargas PCIe-bound (mucho tráfico host↔device), tenants en MIG distintos pueden seguir afectándose. Para inferencia LLM, el path principal es HBM, así que esto rara vez es problema. Pero conviene saberlo.&lt;/p>
&lt;h3 id="las-particiones-mig-en-h100">Las particiones MIG en H100&lt;/h3>
&lt;p>Una H100 (80GB HBM3) se puede particionar en perfiles fijos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Perfil&lt;/th>
&lt;th>SM&lt;/th>
&lt;th>Memoria&lt;/th>
&lt;th>Instancias máx por GPU&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1g.10gb&lt;/td>
&lt;td>14&lt;/td>
&lt;td>10 GB&lt;/td>
&lt;td>7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1g.20gb&lt;/td>
&lt;td>14&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2g.20gb&lt;/td>
&lt;td>28&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3g.40gb&lt;/td>
&lt;td>42&lt;/td>
&lt;td>40 GB&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7g.80gb&lt;/td>
&lt;td>98&lt;/td>
&lt;td>80 GB&lt;/td>
&lt;td>1 (toda la GPU)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para un cluster mid-scale con NVLink, &lt;strong>MIG tiene un problema fundamental&lt;/strong>: cuando particionas con MIG, &lt;strong>se desactiva el NVLink entre GPUs&lt;/strong>. Una H100 en MIG &lt;strong>no&lt;/strong> participa en tensor parallel multi-GPU. Si vas a servir un modelo grande con tensor parallel (Llama 3 70B con TP=4 o TP=8, por ejemplo), esas GPUs deben estar enteras, sin MIG.&lt;/p>
&lt;p>Esto define la decisión arquitectónica. Hay dos enfoques principales:&lt;/p>
&lt;h3 id="enfoque-a--modelo-grande-compartido-con-quotas-en-gateway">Enfoque A — Modelo grande compartido con quotas en gateway&lt;/h3>
&lt;p>Todas las GPUs del nodo sirven &lt;strong>un único modelo grande con tensor parallel&lt;/strong> que abarca el nodo entero. Todos los tenants comparten esa instancia. El aislamiento se hace en la capa de gateway (quotas, rate limiting) y la capa de policy (priority classes). El kernel del cluster es una sola instancia vLLM enorme con &lt;code>--max-num-seqs=128&lt;/code> o similar; vLLM internamente reparte tiempo de GPU entre las requests activas con continuous batching.&lt;/p>
&lt;p>&lt;strong>Ventajas&lt;/strong>: aprovechas todas las GPUs al máximo, NVLink activo, mejor utilización del KV cache.
&lt;strong>Desventajas&lt;/strong>: aislamiento blando — un tenant que satura no degrada a otros directamente (vLLM bachea), pero sí compite por slots del batch. Necesitas priority classes serias.&lt;/p>
&lt;h3 id="enfoque-b--dedicar-gpus-por-modelo--tenant">Enfoque B — Dedicar GPUs por modelo / tenant&lt;/h3>
&lt;p>Divides las GPUs en pools dedicados a modelos distintos. Ejemplos en un nodo de 8 GPUs:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>4 GPUs&lt;/strong>: modelo grande de 70B con TP=4.&lt;/li>
&lt;li>&lt;strong>2 GPUs&lt;/strong>: modelo mediano de 32B replicado (2 instancias independientes) para tenants con SLA estricto.&lt;/li>
&lt;li>&lt;strong>2 GPUs&lt;/strong>: cargas misceláneas (modelos más pequeños, experimentación).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Ventajas&lt;/strong>: aislamiento físico entre modelos / tenants críticos.
&lt;strong>Desventajas&lt;/strong>: peor utilización agregada; algunas GPUs idle mientras otras saturan.&lt;/p>
&lt;h3 id="enfoque-c-avanzado--mig-en-algunas-gpus--dedicar-el-resto">Enfoque C (avanzado) — MIG en algunas GPUs + dedicar el resto&lt;/h3>
&lt;p>Si tienes cargas pequeñas (modelos de 4B, 7B), puedes hacer MIG en 1-2 GPUs para servirlas y dedicar las restantes a tensor parallel del modelo grande. Combina aislamiento fuerte para cargas chicas con aprovechamiento del NVLink para el modelo grande.&lt;/p>
&lt;h3 id="la-elección-operativa-empieza-por-a-sube-a-c-si-hace-falta">La elección operativa: empieza por A, sube a C si hace falta&lt;/h3>
&lt;p>En la mayoría de despliegues, el Enfoque A (modelo grande compartido + quotas) es el punto de partida correcto. La utilización es mejor, la operación es más simple, y los aislamientos blandos del gateway funcionan para cargas razonables.&lt;/p>
&lt;p>Cuando hay un tenant con SLA estricto que no tolera competir con otros, mueves a Enfoque B para ese tenant en particular (dedicar GPUs a una instancia del modelo solo para él), manteniendo el resto del cluster compartido.&lt;/p>
&lt;p>Enfoque C es para cuando tienes 10+ tenants con perfiles muy heterogéneos.&lt;/p>
&lt;h3 id="aislamiento-a-nivel-kubernetes">Aislamiento a nivel Kubernetes&lt;/h3>
&lt;p>Independiente del aislamiento GPU, en K8s se aplica aislamiento de pod:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Namespaces por tenant&lt;/strong>: &lt;code>tenant-soporte&lt;/code>, &lt;code>tenant-legal&lt;/code>, etc.&lt;/li>
&lt;li>&lt;strong>ResourceQuotas y LimitRanges&lt;/strong>: límites de CPU/memoria por namespace.&lt;/li>
&lt;li>&lt;strong>NetworkPolicies&lt;/strong>: tenant A no puede hablar con namespaces de tenant B.&lt;/li>
&lt;li>&lt;strong>PriorityClasses K8s&lt;/strong>: clases con valor numérico que define preemption order si llega un pod más crítico.&lt;/li>
&lt;li>&lt;strong>PodDisruptionBudgets&lt;/strong>: cuántos pods de cada deployment pueden caer simultáneamente.&lt;/li>
&lt;/ul>
&lt;h2 id="capa-4--observability-plane-ver-lo-que-pasa-por-tenant">Capa 4 — Observability Plane: ver lo que pasa por tenant&lt;/h2>
&lt;p>La cuarta capa: &lt;strong>observabilidad con dimensión tenant&lt;/strong>. Sin esto, no puedes hacer cost attribution, no puedes debugear incidentes de un solo tenant, no puedes mostrar dashboards a stakeholders.&lt;/p>
&lt;h3 id="las-cuatro-propiedades-obligatorias">Las cuatro propiedades obligatorias&lt;/h3>
&lt;p>&lt;strong>1. tenant_id en todos los spans&lt;/strong>. El AI gateway resuelve la API key y atribuye un &lt;code>tenant_id&lt;/code>. Ese ID &lt;strong>se propaga&lt;/strong> vía &lt;code>params._meta&lt;/code> o headers OTel a todos los componentes downstream (vLLM, retrieval, MCP servers, tools). Cualquier span en cualquier sistema lleva ese label. Es lo que permite reconstruir traces tenant-específicos.&lt;/p>
&lt;p>&lt;strong>2. Métricas labeled por tenant&lt;/strong>. &lt;code>gen_ai.usage.input_tokens{tenant=&amp;quot;soporte-chat&amp;quot;}&lt;/code> o equivalentes. Prometheus, Grafana, agrupable por tenant.&lt;/p>
&lt;p>&lt;strong>3. Cost attribution real&lt;/strong>. La suma de tokens × cost/token por tenant da el coste. Para vLLM self-hosted, el coste es por hora de GPU + parte proporcional de tokens (puedes calcular un cost-per-1k-tokens equivalente).&lt;/p>
&lt;p>&lt;strong>4. Audit log inmutable&lt;/strong>. Cada API key usada, cada modelo invocado, cada cambio de quota, cada budget exceeded. Para compliance.&lt;/p>
&lt;h3 id="showback-vs-chargeback">Showback vs chargeback&lt;/h3>
&lt;p>Distinción importante de FinOps que ha ganado claridad en 2026:&lt;/p>
&lt;p>&lt;strong>Showback&lt;/strong>: visibilidad sin consecuencia. &amp;ldquo;Equipo de soporte, has consumido $623 este mes en LLM&amp;rdquo;. Información, no factura. Permite detectar abusos sin penalizar antes de que el equipo entienda.&lt;/p>
&lt;p>&lt;strong>Chargeback&lt;/strong>: el coste se imputa al presupuesto del equipo. Cuando se acaba, se acaba. Cambia comportamiento.&lt;/p>
&lt;p>La práctica que funciona: &lt;strong>6-18 meses en showback&lt;/strong> mientras se calibran tags, se identifican misattributions, se forma a los equipos. &lt;strong>Después chargeback&lt;/strong> cuando los números son creíbles. Lanzar chargeback el día 1 cuando los costs aún están sucios crea pelea política inmediata; lanzar showback prepara terreno para que el chargeback aterrice ordenadamente.&lt;/p>
&lt;p>&lt;a href="https://spendark.com/blog/kubernetes-cost-allocation/">Solo 14% de organizaciones tienen chargeback activo&lt;/a> según un survey reciente, lo que indica que esto sigue siendo mayoritariamente showback en producción real.&lt;/p>
&lt;h3 id="herramientas">Herramientas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://www.kubecost.com/">Kubecost&lt;/a>&lt;/strong>: cost allocation por namespace, deployment, pod en Kubernetes. Para el coste de la GPU compartida, allocate proporcionalmente a tokens consumidos por tenant.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.finout.io/">Finout&lt;/a>&lt;/strong>: FinOps platform que combina cloud bills + LLM API costs en una vista unificada con tagging virtual.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://langfuse.com/">Langfuse&lt;/a>&lt;/strong>: ya cubierto. Cost tracking por trace, agrupable por usuario o session metadata.&lt;/li>
&lt;li>&lt;strong>LiteLLM tracking nativo&lt;/strong>: el master DB de LiteLLM mantiene running spend por team, user, API key, accesible vía API o UI.&lt;/li>
&lt;/ul>
&lt;h3 id="dashboard-mínimo-multi-tenant">Dashboard mínimo multi-tenant&lt;/h3>
&lt;p>Cualquier plataforma debería tener:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Resumen por tenant&lt;/strong>: spend mensual, RPS actual, TPM consumido, % budget gastado, sesiones activas.&lt;/li>
&lt;li>&lt;strong>Top usuarios&lt;/strong> dentro de cada tenant (para detección de abuso interno).&lt;/li>
&lt;li>&lt;strong>Latencia p95 por tenant&lt;/strong>: SLA tracking.&lt;/li>
&lt;li>&lt;strong>Errores 429 / 503&lt;/strong>: cuántas requests están siendo rate-limitadas o rechazadas por overload.&lt;/li>
&lt;li>&lt;strong>Cost trend&lt;/strong>: trayectoria mensual con proyección.&lt;/li>
&lt;li>&lt;strong>Drift por tenant&lt;/strong> (de la serie post-tracing): si un tenant empieza a tener peores resultados, alerta.&lt;/li>
&lt;/ol>
&lt;h2 id="dimensionado-en-clusters-gpu-mid-scale-decisiones-concretas">Dimensionado en clusters GPU mid-scale: decisiones concretas&lt;/h2>
&lt;p>Bajemos a hardware. Tomamos como referencia un nodo con &lt;strong>N H100 SXM (entre 4 y 8) con NVLink/NVSwitch&lt;/strong>, 80 GB HBM3 cada una. Eso da entre &lt;strong>320 GB y 640 GB de VRAM agregada&lt;/strong>. Conectividad inter-GPU 900 GB/s (NVLink 4) o 600 GB/s (NVLink 3) según generación. Ancho de banda HBM por GPU 3.35 TB/s.&lt;/p>
&lt;h3 id="decisiones-por-defecto">Decisiones por defecto&lt;/h3>
&lt;p>Empezar con &lt;strong>Enfoque A&lt;/strong>: todas las GPUs del nodo sirviendo &lt;strong>un único modelo grande de 70B en BF16 con tensor parallel = N&lt;/strong>. Capacidad real esperada (calculada para un nodo HGX estándar de 8 GPUs como ejemplo; escala aproximadamente lineal con N):&lt;/p>
&lt;ul>
&lt;li>VRAM modelo (70B BF16): ~140 GB (≈ 17.5 GB/GPU en TP=8).&lt;/li>
&lt;li>VRAM overhead vLLM + activations: ~10 GB/GPU.&lt;/li>
&lt;li>VRAM libre para KV cache: ~52 GB/GPU. En un nodo de 8 GPUs son &lt;strong>~416 GB agregados&lt;/strong>; en uno de 4 son ~210 GB.&lt;/li>
&lt;li>Con &lt;code>--kv-cache-dtype=fp8&lt;/code> y un modelo 70B GQA: ~320 KB/token.&lt;/li>
&lt;li>Capacidad agregada de cache (nodo de 8 GPUs): &lt;strong>~1.3M tokens&lt;/strong> repartibles entre sesiones simultáneas.&lt;/li>
&lt;/ul>
&lt;p>Esto se traduce en throughput y concurrencia (cifras orientativas para un nodo de 8 GPUs):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Sesiones simultáneas&lt;/th>
&lt;th style="text-align:right">Contexto medio por sesión&lt;/th>
&lt;th style="text-align:right">Throughput agregado (tokens/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">16K&lt;/td>
&lt;td style="text-align:right">~5000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">8K&lt;/td>
&lt;td style="text-align:right">~8000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">4K&lt;/td>
&lt;td style="text-align:right">~12000&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Latencias típicas: &lt;strong>TTFT ~150ms&lt;/strong> a tráfico bajo, &lt;strong>TPOT ~15-20 ms/tok&lt;/strong>. Con concurrencia alta, TTFT sube hasta ~500ms si el queue está saturado.&lt;/p>
&lt;h3 id="esquema-de-tenants-ejemplo">Esquema de tenants ejemplo&lt;/h3>
&lt;p>Cluster con 4 tenants y un pool de research:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tenant&lt;/th>
&lt;th style="text-align:right">TPM cap&lt;/th>
&lt;th style="text-align:right">RPM cap&lt;/th>
&lt;th style="text-align:right">Concurrency&lt;/th>
&lt;th style="text-align:right">Budget&lt;/th>
&lt;th>Priority&lt;/th>
&lt;th>Modelos&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Soporte chat&lt;/td>
&lt;td style="text-align:right">80K&lt;/td>
&lt;td style="text-align:right">800&lt;/td>
&lt;td style="text-align:right">50&lt;/td>
&lt;td style="text-align:right">1500 USD/mes&lt;/td>
&lt;td>Guaranteed&lt;/td>
&lt;td>llama-3-70b, qwen3-32b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Legal RAG&lt;/td>
&lt;td style="text-align:right">30K&lt;/td>
&lt;td style="text-align:right">200&lt;/td>
&lt;td style="text-align:right">15&lt;/td>
&lt;td style="text-align:right">600 USD/mes&lt;/td>
&lt;td>Guaranteed&lt;/td>
&lt;td>llama-3-70b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agente code&lt;/td>
&lt;td style="text-align:right">50K&lt;/td>
&lt;td style="text-align:right">300&lt;/td>
&lt;td style="text-align:right">25&lt;/td>
&lt;td style="text-align:right">1200 USD/mes&lt;/td>
&lt;td>Best-effort&lt;/td>
&lt;td>llama-3-70b, qwen-coder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Data extr. batch&lt;/td>
&lt;td style="text-align:right">40K&lt;/td>
&lt;td style="text-align:right">1000&lt;/td>
&lt;td style="text-align:right">40&lt;/td>
&lt;td style="text-align:right">400 USD/mes&lt;/td>
&lt;td>Spot&lt;/td>
&lt;td>llama-3-70b, qwen3-32b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Research / notebooks&lt;/td>
&lt;td style="text-align:right">10K&lt;/td>
&lt;td style="text-align:right">100&lt;/td>
&lt;td style="text-align:right">5&lt;/td>
&lt;td style="text-align:right">200 USD/mes&lt;/td>
&lt;td>Spot&lt;/td>
&lt;td>todos&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Suma TPM: 210K. Capacidad agregada del cluster: ~180K TPM sostenidos. &lt;strong>Está overcommit del ~15%&lt;/strong>, asumiendo que no todos los tenants llegan al techo simultáneamente. Es lo normal y deseable; si todos lo hacen al mismo tiempo, las priority classes degradan ordenadamente.&lt;/p>
&lt;h3 id="cuándo-añadir-hardware">Cuándo añadir hardware&lt;/h3>
&lt;p>Señales que indican que el nodo se ha quedado pequeño:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT p95 sostenida &amp;gt; 500 ms&lt;/strong> durante horas de pico → el queue se está acumulando.&lt;/li>
&lt;li>&lt;strong>&lt;code>vllm:num_requests_waiting&lt;/code> constantemente &amp;gt; 20&lt;/strong> → admission control empezando a rechazar.&lt;/li>
&lt;li>&lt;strong>Utilización GPU sostenida &amp;gt; 80% en horas críticas&lt;/strong> sin caer abajo en horas valle → no hay margen.&lt;/li>
&lt;li>&lt;strong>Tasa de 429 sobre los tenants guaranteed &amp;gt; 1%&lt;/strong> → la plataforma rompe SLA en producción.&lt;/li>
&lt;/ul>
&lt;p>Cuando varios de estos se cumplan, el siguiente paso natural es añadir otro nodo HGX con NVLink interno y montar &lt;strong>una segunda instancia vLLM&lt;/strong> del mismo modelo. El gateway hace load balancing entre las dos instancias. Throughput agregado se duplica; latencia se mantiene.&lt;/p>
&lt;h2 id="trampas-operativas-comunes">Trampas operativas comunes&lt;/h2>
&lt;h3 id="gateway-sin-auth-backdoor-al-cluster">Gateway sin auth: backdoor al cluster&lt;/h3>
&lt;p>Tu vLLM está en un Service ClusterIP, la app principal habla con él. Algún tenant directo descubre el endpoint y le pega directamente sin pasar por el gateway. Quotas y costs se evaden silenciosamente. &lt;strong>NetworkPolicy estricta&lt;/strong>: solo el gateway puede hablar con los Service vLLM; el resto del cluster no.&lt;/p>
&lt;h3 id="mig-y-nvlink-incompatibles">MIG y NVLink incompatibles&lt;/h3>
&lt;p>Activas MIG en una GPU pensando que tendrás aislamiento + multi-GPU; descubres que MIG desactiva NVLink. Cualquier modelo grande con TP queda inservible. &lt;strong>Decide MIG vs NVLink globalmente por cluster&lt;/strong>, no por GPU individual.&lt;/p>
&lt;h3 id="quotas-pegadas-al-techo-del-cluster">Quotas pegadas al techo del cluster&lt;/h3>
&lt;p>Sumas los TPM de todos los tenants y dan exactamente la capacidad del cluster. Cuando dos tenants pico simultáneamente, ambos esperan o uno rechaza. &lt;strong>Overcommit 10-20%&lt;/strong> es saludable (asume que no todos pican a la vez); más es peligroso.&lt;/p>
&lt;h3 id="sin-observabilidad-multi-tenant-desde-el-día-1">Sin observabilidad multi-tenant desde el día 1&lt;/h3>
&lt;p>Lanzas con quotas y aislamiento pero sin tenant_id en spans. A los 3 meses, tu CFO pregunta &amp;ldquo;¿cuánto cuesta el agente de soporte vs el de legal?&amp;rdquo; y no puedes responder. &lt;strong>OTel con tenant_id obligatorio desde la primera versión&lt;/strong>, aunque no haya dashboards aún; tener los datos vale más que tener dashboards perfectos sin datos.&lt;/p>
&lt;h3 id="showback-que-nunca-llega-a-chargeback">Showback que nunca llega a chargeback&lt;/h3>
&lt;p>Llevas 18 meses en showback, los equipos saben los números, nadie cambia comportamiento. Sin la presión del chargeback real, el incentivo se diluye. &lt;strong>Calendario explícito&lt;/strong> para la transición a chargeback, con dueño y deadline.&lt;/p>
&lt;h3 id="modelos-no-whitelisteados-consumiendo-presupuesto">Modelos no whitelisteados consumiendo presupuesto&lt;/h3>
&lt;p>Un equipo descubre que LiteLLM tiene &lt;code>gpt-4o&lt;/code> configurado. Lo usa sin permiso. El budget se quema en API externa cuando la idea era usar el self-hosted barato. &lt;strong>Whitelist explícita por team de modelos accesibles&lt;/strong>.&lt;/p>
&lt;h3 id="priority-classes-mal-calibradas">Priority classes mal calibradas&lt;/h3>
&lt;p>Todo el mundo se declara &amp;ldquo;guaranteed&amp;rdquo;. En el primer pico, no queda nada por degradar y todo sufre. &lt;strong>Priority classes solo para casos críticos&lt;/strong> con justificación. La mayoría debería ser best-effort.&lt;/p>
&lt;h3 id="sin-failover-desde-el-gateway">Sin failover desde el gateway&lt;/h3>
&lt;p>Tu vLLM se cae. El gateway no tiene fallback configurado y devuelve 503 a todos los tenants. &lt;strong>Fallback configurado&lt;/strong> a otro modelo, idealmente externo (OpenAI) para cargas guaranteed, aunque pague más por hora — la disponibilidad vale más que el coste por hora.&lt;/p>
&lt;h2 id="roadmap-operativo-de-arranque">Roadmap operativo de arranque&lt;/h2>
&lt;p>Si parte de cero con un nodo GPU vacío, el orden mínimo es el siguiente. Cada hito es un día de trabajo con margen, no apretado:&lt;/p>
&lt;p>&lt;strong>Día 1-2 — Infra base K8s&lt;/strong>. NVIDIA GPU Operator + nvidia-device-plugin + dcgm-exporter + NetworkPolicies cluster-default. Validación: un pod básico con &lt;code>nvidia.com/gpu: 1&lt;/code> se schedulea.&lt;/p>
&lt;p>&lt;strong>Día 3 — vLLM con un modelo grande y tensor parallel del nodo entero&lt;/strong>. Helm chart de vLLM Production Stack (o vLLM bare manifests). Pesos del modelo en PVC compartido (CephFS o NFS). Validación: una petición &lt;code>curl&lt;/code> contra el Service interno responde.&lt;/p>
&lt;p>&lt;strong>Día 4 — AI Gateway: LiteLLM&lt;/strong>. Helm chart, Postgres para budgets, master key, primer model_list pointing a vLLM. Validación: una petición OpenAI-compatible vía LiteLLM responde con el mismo contenido que el vLLM directo.&lt;/p>
&lt;p>&lt;strong>Día 5 — Multi-tenancy básica&lt;/strong>. Crear teams, API keys, budget, model whitelist. Probar con dos teams. Validación: el segundo team usando el modelo que no tiene whitelisteado recibe 403.&lt;/p>
&lt;p>&lt;strong>Día 6 — Observabilidad mínima&lt;/strong>. Prometheus + Grafana scraping vLLM y LiteLLM. Dashboard con TTFT, TPOT, throughput, num_requests_waiting, budget_consumed_per_team. Validación: visible en Grafana con datos reales.&lt;/p>
&lt;p>&lt;strong>Día 7-8 — Cliente piloto&lt;/strong>. Un tenant real (idealmente uno interno controlado) empieza a usar. Mide latencias reales, descubre los primeros incidentes operativos.&lt;/p>
&lt;p>&lt;strong>Día 9-10 — Tuning&lt;/strong>. Ajustar &lt;code>--max-num-seqs&lt;/code>, &lt;code>--gpu-memory-utilization&lt;/code>, priority classes, quotas según lo aprendido del piloto.&lt;/p>
&lt;p>&lt;strong>Día 11-14 — Onboarding del segundo tenant + iteración&lt;/strong>. Repeat. Cada nuevo tenant onboarded revela nuevos casos.&lt;/p>
&lt;p>A las dos semanas tienes una plataforma operacional con dos tenants reales y datos para decidir si está lista para más. La línea de avance de aquí en adelante es &lt;strong>horizontal&lt;/strong> (más tenants) hasta saturar; a partir de ahí, &lt;strong>vertical&lt;/strong> (más hardware).&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-posts">Lo que no hemos cubierto (próximos posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Fine-tuning continuo en producción&lt;/strong> (post 6, decidido): LoRA/QLoRA/DPO, dataset curation, eval gates, A/B versioning con tráfico real entre versiones del modelo.&lt;/li>
&lt;li>&lt;strong>Constitutional AI y alignment runtime&lt;/strong>: opción que sigue en la mesa.&lt;/li>
&lt;li>&lt;strong>Edge LLMs&lt;/strong>: cuando un cluster H100 es demasiado caro para una carga concreta, modelos distillados corriendo en NPUs o GPUs consumer.&lt;/li>
&lt;li>&lt;strong>GPU networking deep dive&lt;/strong>: NCCL, InfiniBand, GPUDirect, RDMA. Para clusters multi-nodo con tensor parallel cross-host.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>Multi-tenancy y aislamiento GPU:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.linuxoperatingsystem.net/multitenant-gpu-infrastructure-4-powerful-design-rules/">Multitenant GPU Infrastructure: 4 Powerful Design Rules&lt;/a> — survey de patrones enterprise.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/run-multiple-llms-one-gpu-mig-time-slicing-guide/">Run Multiple LLMs on One GPU: MIG, Time-Slicing, and MPS Guide (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://sagar-parmar.medium.com/a-practical-guide-to-gpu-partitioning-with-mig-on-on-prem-servers-and-kubernetes-797ccea7e1c7">A Practical Guide to GPU Partitioning with MIG (Medium)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.suse.com/c/kubecon-eu-2026-nvidia-mig-suse-virtualization/">GPU Partitioning for AI Workloads: NVIDIA MIG with SUSE Virtualization (KubeCon EU 2026)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2508.20274">Predictable LLM Serving on GPU Clusters (arxiv 2508.20274)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2603.00356">Token Management in Multi-Tenant AI Inference Platforms (arxiv 2603.00356)&lt;/a> — paper de priority + admission control.&lt;/li>
&lt;/ul>
&lt;p>AI Gateways:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://docs.litellm.ai/docs/proxy/multi_tenant_architecture">LiteLLM — Multi-Tenant Architecture&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://docs.litellm.ai/docs/proxy/users">LiteLLM — Budgets and Rate Limits&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://portkey.ai/">Portkey AI Gateway&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">Kong AI Gateway — LLM Cost Management&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/ai-gateway-litellm-portkey-kong-gpu-cloud/">AI Gateway Setup 2026: LiteLLM, Portkey, Kong (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://techsy.io/en/blog/best-llm-gateway-tools">Stop Juggling LLM APIs: 8 Gateways Ranked 2026 (TECHSY)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>FinOps multi-tenant:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.digiusher.com/blog/the-death-of-cost-allocation-why-chargeback-models-are-failing-in-the-kubernetes-and-ai-era/">The Death of Chargeback in the Kubernetes and AI Era (DigiUsher)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://medium.com/@nicholasthoni/how-to-actually-track-kubernetes-costs-in-2026-a-practical-guide-to-showback-chargeback-and-the-6a4c23f9cf51">How to Actually Track Kubernetes Costs in 2026 (Medium)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">LLM Cost Management: AI Showback and Chargeback (Kong)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kubecost.com/">Kubecost — cost allocation&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.finout.io/">Finout — FinOps + AI costs&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Posts previos serie 4: &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama MLOps LLMs&lt;/a>, &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka&lt;/a>, &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline de 6 etapas&lt;/a>, &lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">PostgreSQL + Qdrant&lt;/a>.&lt;/li>
&lt;li>Posts relevantes de la serie inferencia: &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a> — el escenario de nodo HGX multi-GPU que aquí desarrollamos. &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a> — vLLM Production Stack y OME que el gateway puede dirigir.&lt;/li>
&lt;li>Observabilidad: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>, &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF + drift&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>PostgreSQL + Qdrant en la etapa de ingestión: patrones de sincronización, microservicios y cómo encaja todo sin romperse</title><link>https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/</link><pubDate>Thu, 21 May 2026 06:50:00 +0200</pubDate><guid>https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>PostgreSQL es la fuente de verdad transaccional de la mayoría de las empresas; Qdrant es el motor de búsqueda vectorial que más equipos eligen cuando pgvector se queda corto. Combinarlos no es trivial: tu modelo de dominio vive en Postgres con ACID, las relaciones, las constraints, los triggers; los embeddings viven en Qdrant con HNSW filterable, quantization escalar, multivectors, sparse-dense hybrid search. &lt;strong>Mantener los dos sincronizados es el problema operacional número uno&lt;/strong> que el campo LLMOps ha codificado en 2026 con tres patrones canónicos: &lt;strong>dual-write&lt;/strong> (simple, frágil, válido para prototipos), &lt;strong>transactional outbox + CDC con Debezium&lt;/strong> (la opción &amp;ldquo;correcta&amp;rdquo; para producción seria) y &lt;strong>event-driven directo a Kafka&lt;/strong> (cuando el evento es el ciudadano de primera y la DB es proyección). La elección de Qdrant sobre pgvector se justifica con números concretos —&lt;strong>filtered search 6ms vs 29ms&lt;/strong> en 500K vectores, &lt;strong>65% menos memoria&lt;/strong> con scalar quantization, &lt;strong>HNSW filterable&lt;/strong> que no se hunde con metadata, escalabilidad horizontal—. El precio es operacional: un servicio stateful adicional que mantener, snapshots que gestionar, gRPC que asegurar. Este post entra en detalle en cómo se sitúa PostgreSQL + Qdrant en la &lt;strong>etapa Data&lt;/strong> del pipeline LLMOps que dibujamos en el post anterior, qué microservicios participan, cómo se sincronizan, cómo se observan y dónde están las trampas que se ven una y otra vez en producción.&lt;/p>
&lt;blockquote>
&lt;p>Este es el &lt;strong>cuarto post de la serie MLOps para LLMs&lt;/strong> y el primero que aplica el patrón &amp;ldquo;estás aquí&amp;rdquo; sobre el mini-mapa que definimos en el &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">post anterior sobre el pipeline de seis etapas&lt;/a>. Aquí estamos plenamente en la primera etapa: &lt;strong>Data&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;h2 id="estás-aquí-etapa-data-del-pipeline">Estás aquí: etapa Data del pipeline&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Data">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#nv1)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#nv1)}&lt;/style>
&lt;defs>&lt;marker id="nv1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DATA · PostgreSQL + Qdrant + patrones de sincronización en microservicios&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box active"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-pregunta-que-define-la-arquitectura-una-db-o-dos">La pregunta que define la arquitectura: ¿una DB o dos?&lt;/h2>
&lt;p>Antes de hablar de patrones, vamos a la decisión que marca el resto del diseño. Tienes datos transaccionales en PostgreSQL —usuarios, productos, documentos, conversaciones— y necesitas búsqueda vectorial sobre ellos para RAG. Dos respuestas razonables:&lt;/p>
&lt;p>&lt;strong>Opción A — pgvector dentro de Postgres&lt;/strong>: añades la extensión &lt;code>vector&lt;/code>, una columna &lt;code>embedding vector(1536)&lt;/code>, un índice HNSW. Cero arquitectura nueva, cero servicio nuevo. Tu DBA sigue siendo el DBA. Una sola DB, ACID con tus tablas relacionales, JOINs entre embedding y metadata. &lt;strong>Una sola fuente de verdad&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Opción B — Qdrant separado&lt;/strong>: dejas Postgres como está y montas Qdrant como servicio stateful aparte. Tu microservicio escribe a las dos. &lt;strong>Dos fuentes parciales que mantener en sync&lt;/strong>.&lt;/p>
&lt;p>La elección depende de números. Vamos a ellos.&lt;/p>
&lt;h3 id="cuándo-pgvector-basta-y-cuándo-no">Cuándo pgvector basta y cuándo no&lt;/h3>
&lt;p>&lt;a href="https://qdrant.tech/blog/pgvector-tradeoffs/">Los benchmarks 2026&lt;/a> son consistentes. La regla del pulgar:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hasta ~1M vectores&lt;/strong>: pgvector es excelente. Setup en minutos, cero overhead operacional, queries ACID con JOINs naturales.&lt;/li>
&lt;li>&lt;strong>1-10M vectores&lt;/strong>: pgvector funciona pero ya empiezas a sufrir. Index builds tardan, recall baja bajo carga, memoria sube linealmente.&lt;/li>
&lt;li>&lt;strong>&amp;gt;10M vectores&lt;/strong>: pgvector se hunde a no ser que tunes mucho. Index build pasa de horas; query p95 deriva por encima de 200ms.&lt;/li>
&lt;li>&lt;strong>&amp;gt;50M vectores&lt;/strong>: pgvector deja de ser opción razonable en single-node.&lt;/li>
&lt;/ul>
&lt;p>Qdrant escala a billones con sharding. Numéricamente, en 500K vectores con 3 condiciones de payload:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Qdrant&lt;/strong>: 6 ms p95 (filtered HNSW).&lt;/li>
&lt;li>&lt;strong>pgvector&lt;/strong>: 29 ms p95 (heap scans rompen la localidad del índice).&lt;/li>
&lt;/ul>
&lt;p>Y en memoria: &lt;strong>Qdrant con scalar quantization usa 65% menos RAM&lt;/strong> que pgvector con IVFFlat sobre el mismo dataset. Para 50M vectores de 1024 dimensiones eso son decenas de GB de diferencia. Multiplicado por tres réplicas para HA, es un nodo entero menos.&lt;/p>
&lt;p>Pero pgvector tiene una ventaja decisiva en proyectos pequeños y medianos: &lt;strong>es gratis, embebido, y lo opera tu DBA&lt;/strong>. La fricción de adoptar Qdrant —un servicio stateful nuevo, gRPC, snapshots, observabilidad propia— solo se justifica cuando el dolor de pgvector es real, no anticipado.&lt;/p>
&lt;h3 id="el-veredicto-operativo-2026">El veredicto operativo 2026&lt;/h3>
&lt;ul>
&lt;li>Empieza con &lt;strong>pgvector&lt;/strong> si tu corpus es &amp;lt;5M vectores y tu equipo es pequeño.&lt;/li>
&lt;li>Migra a &lt;strong>Qdrant&lt;/strong> cuando uno de los tres siguientes signos aparezca: latencia p95 inaceptable, presión de memoria sobre el cluster Postgres principal, necesidad de hybrid search (sparse + dense) avanzada.&lt;/li>
&lt;li>&lt;strong>No migres anticipadamente&lt;/strong>: el coste operacional de Qdrant es real; sufre cuando lo necesitas, no por si acaso.&lt;/li>
&lt;/ul>
&lt;p>Lo importante: &lt;strong>diseña la capa de acceso a embeddings con una abstracción&lt;/strong> (un &lt;code>VectorStore&lt;/code> interface en tu código) para que cambiar de pgvector a Qdrant sea cambiar la implementación, no reescribir la app.&lt;/p>
&lt;h2 id="qdrant-en-detalle-lo-que-ofrece-sobre-pgvector">Qdrant en detalle: lo que ofrece sobre pgvector&lt;/h2>
&lt;p>Si decides que Qdrant es la opción, vale la pena entender qué te da más allá del rendimiento bruto. Cinco features dominantes:&lt;/p>
&lt;h3 id="1-filterable-hnsw">1. Filterable HNSW&lt;/h3>
&lt;p>El &lt;strong>HNSW filterable&lt;/strong> es lo que más se nota en producción. En pgvector, filtrar por metadata (&lt;code>WHERE category = 'tech' AND date &amp;gt; '2026-01-01'&lt;/code>) hace que el índice HNSW pierda eficiencia: la búsqueda tiene que recorrer más nodos para encontrar los que cumplen el filtro. En Qdrant, el HNSW está construido para &lt;strong>podar la búsqueda con filtros dentro del propio recorrido del grafo&lt;/strong>, sin escapar a heap scans externos. Para queries con filtros densos (lo normal en RAG con permisos multi-tenant), la diferencia es brutal.&lt;/p>
&lt;h3 id="2-multivector-y-late-interaction-colbert">2. Multivector y late-interaction (ColBERT)&lt;/h3>
&lt;p>Qdrant permite almacenar &lt;strong>una matriz de vectores por punto&lt;/strong>, no solo un vector. Esto soporta nativamente modelos late-interaction como ColBERT, que codifican un vector por token y comparan con &lt;code>MaxSim&lt;/code>. La calidad de retrieval con ColBERT-style multivectors es típicamente 5-15% mejor que single-vector en cargas semánticas complejas.&lt;/p>
&lt;h3 id="3-sparse--dense-hybrid-search">3. Sparse + dense hybrid search&lt;/h3>
&lt;p>&lt;a href="https://qdrant.tech/articles/sparse-vectors/">Hybrid search&lt;/a> combina un vector denso (semántico, eg embeddings de SentenceTransformers) con un vector disperso (lexical, eg SPLADE, BM25 reproducido como sparse). El denso captura &amp;ldquo;esto es semánticamente similar&amp;rdquo;; el disperso captura &amp;ldquo;esta palabra concreta aparece&amp;rdquo;. Combinados —tipicamente con reciprocal rank fusion o weighted combination— recuperan tanto la similitud semántica como los matches exactos de keyword. Es el patrón de retrieval que más calidad da en 2026 y Qdrant lo trae nativo desde la versión 1.10.&lt;/p>
&lt;h3 id="4-quantization-escalar-y-binaria">4. Quantization escalar y binaria&lt;/h3>
&lt;p>Para cargas grandes, Qdrant ofrece &lt;strong>scalar quantization&lt;/strong> (&lt;code>int8&lt;/code> en lugar de &lt;code>float32&lt;/code>, 4× menos memoria con pérdida marginal de recall) y &lt;strong>binary quantization&lt;/strong> (1 bit por dimensión, 32× menos memoria con pérdida moderada que se recupera con rescoring de los top-K). En el roadmap 2026 está la &lt;strong>4-bit quantization&lt;/strong>, que será un punto medio.&lt;/p>
&lt;h3 id="5-named-vectors">5. Named vectors&lt;/h3>
&lt;p>Una colección Qdrant puede tener &lt;strong>múltiples espacios vectoriales por punto&lt;/strong>, llamados named vectors. Caso típico: el mismo documento se indexa con un vector denso (&lt;code>text-embedding-3-small&lt;/code>) y un vector sparse (SPLADE), bajo el mismo &lt;code>point_id&lt;/code>. Las queries pueden buscar en el vector concreto que les interesa.&lt;/p>
&lt;p>A esto se suma el roadmap 2026: &lt;strong>4-bit quantization, read-write segregation, expanded inference capabilities&lt;/strong> (Qdrant puede embeddar texto él mismo, sin un servicio externo).&lt;/p>
&lt;h2 id="la-arquitectura-de-microservicios-dónde-encaja-cada-pieza">La arquitectura de microservicios: dónde encaja cada pieza&lt;/h2>
&lt;p>Aquí está lo que el usuario que monta esto en producción tiene que diseñar. La arquitectura típica que se ha estabilizado tiene &lt;strong>cinco microservicios&lt;/strong> que tocan estas piezas, cada uno con su responsabilidad clara:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Microservicios PG + Qdrant">
&lt;style>.title{font:700 13px sans-serif;fill:#222}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#555}.tiny{font:10px sans-serif;fill:#666}.svc{stroke:#444;stroke-width:1.5;rx:6}.domain{fill:#ffe9d6}.emb{fill:#d6eaff}.idx{fill:#d9f5d6}.retr{fill:#e9d6f5}.llm{fill:#ffd6d6}.db{stroke:#666;stroke-width:1.5;rx:4}.pg{fill:#fff5b0}.qd{fill:#d6f0ff}.kafka{fill:#f4d6ff}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#nv2)}.async{stroke:#aa6;stroke-width:1.4;fill:none;marker-end:url(#nv2);stroke-dasharray:5 3}&lt;/style>
&lt;defs>&lt;marker id="nv2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="22" text-anchor="middle" class="title">Microservicios típicos en un RAG con PostgreSQL + Qdrant&lt;/text>
&lt;rect x="30" y="50" width="150" height="60" class="svc domain"/>&lt;text x="105" y="74" text-anchor="middle" class="lbl">Domain Service&lt;/text>&lt;text x="105" y="92" text-anchor="middle" class="sm">CRUD de documentos&lt;/text>&lt;text x="105" y="106" text-anchor="middle" class="sm">/users, /products...&lt;/text>
&lt;rect x="30" y="180" width="150" height="80" class="db pg"/>&lt;text x="105" y="204" text-anchor="middle" class="lbl">PostgreSQL&lt;/text>&lt;text x="105" y="222" text-anchor="middle" class="sm">documents (main)&lt;/text>&lt;text x="105" y="238" text-anchor="middle" class="sm">outbox (events)&lt;/text>&lt;text x="105" y="254" text-anchor="middle" class="sm">ACID&lt;/text>
&lt;rect x="220" y="180" width="140" height="80" class="db kafka"/>&lt;text x="290" y="204" text-anchor="middle" class="lbl">Kafka&lt;/text>&lt;text x="290" y="222" text-anchor="middle" class="sm">documents.changes&lt;/text>&lt;text x="290" y="238" text-anchor="middle" class="sm">documents.embedded&lt;/text>&lt;text x="290" y="254" text-anchor="middle" class="sm">retention 30d+&lt;/text>
&lt;rect x="400" y="50" width="160" height="60" class="svc emb"/>&lt;text x="480" y="74" text-anchor="middle" class="lbl">Embedding Service&lt;/text>&lt;text x="480" y="92" text-anchor="middle" class="sm">consume Kafka,&lt;/text>&lt;text x="480" y="106" text-anchor="middle" class="sm">batch embed, escribe&lt;/text>
&lt;rect x="400" y="180" width="160" height="60" class="svc idx"/>&lt;text x="480" y="204" text-anchor="middle" class="lbl">Indexing Worker&lt;/text>&lt;text x="480" y="222" text-anchor="middle" class="sm">consume embedded,&lt;/text>&lt;text x="480" y="238" text-anchor="middle" class="sm">upsert a Qdrant&lt;/text>
&lt;rect x="580" y="180" width="170" height="80" class="db qd"/>&lt;text x="665" y="204" text-anchor="middle" class="lbl">Qdrant&lt;/text>&lt;text x="665" y="222" text-anchor="middle" class="sm">collection: documents&lt;/text>&lt;text x="665" y="238" text-anchor="middle" class="sm">HNSW + payload&lt;/text>&lt;text x="665" y="254" text-anchor="middle" class="sm">3 réplicas&lt;/text>
&lt;rect x="220" y="310" width="160" height="60" class="svc retr"/>&lt;text x="300" y="334" text-anchor="middle" class="lbl">Retrieval Service&lt;/text>&lt;text x="300" y="352" text-anchor="middle" class="sm">query Qdrant + reranker&lt;/text>&lt;text x="300" y="366" text-anchor="middle" class="sm">+ enrich con PG&lt;/text>
&lt;rect x="430" y="310" width="160" height="60" class="svc llm"/>&lt;text x="510" y="334" text-anchor="middle" class="lbl">LLM Service&lt;/text>&lt;text x="510" y="352" text-anchor="middle" class="sm">vLLM / API externa&lt;/text>&lt;text x="510" y="366" text-anchor="middle" class="sm">recibe context + query&lt;/text>
&lt;path class="arr" d="M105,110 L105,180"/>
&lt;text x="115" y="148" class="tiny">tx con outbox&lt;/text>
&lt;path class="async" d="M180,228 L220,228"/>
&lt;text x="200" y="222" class="tiny">CDC&lt;/text>
&lt;text x="200" y="245" class="tiny">Debezium&lt;/text>
&lt;path class="arr" d="M360,228 L360,148 L400,80"/>
&lt;text x="365" y="160" class="tiny">consume&lt;/text>
&lt;text x="365" y="173" class="tiny">events&lt;/text>
&lt;path class="arr" d="M480,110 L480,180"/>
&lt;text x="490" y="148" class="tiny">produce&lt;/text>
&lt;text x="490" y="161" class="tiny">embedded&lt;/text>
&lt;path class="arr" d="M560,225 L580,225"/>
&lt;text x="565" y="217" class="tiny">upsert&lt;/text>
&lt;path class="arr" d="M580,225 C600,250 600,300 510,310"/>
&lt;text x="590" y="285" class="tiny">query&lt;/text>
&lt;path class="arr" d="M180,340 L220,340"/>
&lt;text x="185" y="333" class="tiny">enrich&lt;/text>
&lt;text x="180" y="346" class="tiny">metadata&lt;/text>
&lt;path class="arr" d="M380,340 L430,340"/>
&lt;text x="395" y="333" class="tiny">context&lt;/text>
&lt;text x="395" y="346" class="tiny">+ query&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Vamos a cada microservicio.&lt;/p>
&lt;h3 id="1-domain-service">1. Domain Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: la lógica de negocio. CRUD de documentos, productos, conversaciones. Endpoints REST/gRPC para el front-end o para otros servicios. &lt;strong>Solo conoce PostgreSQL como sistema de persistencia&lt;/strong>; no sabe nada de Qdrant.&lt;/p>
&lt;p>Esto es &lt;strong>importante por diseño&lt;/strong>: el domain service no debería tener nunca una referencia directa a Qdrant. Si la tiene, ya estás en el antipattern del dual-write. El domain service escribe a Postgres en una transacción ACID; el resto del pipeline se entera vía eventos.&lt;/p>
&lt;h3 id="2-postgresql">2. PostgreSQL&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: source of truth transaccional. Schemas relacionales, constraints, triggers, ACID. &lt;strong>Y la outbox table&lt;/strong> que veremos en breve, que es lo que va a permitir la sincronización fiable.&lt;/p>
&lt;p>Patrón típico de despliegue: HA con Patroni + repmgr + PgBouncer para connection pooling, replicas de lectura para offloading.&lt;/p>
&lt;h3 id="3-kafka">3. Kafka&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: el bus de eventos. Recibe los cambios capturados por CDC (Debezium leyendo el WAL de Postgres o leyendo la outbox table) y los pone disponibles para los consumidores. Cubierto en profundidad en &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka&lt;/a>.&lt;/p>
&lt;p>Topics típicos:&lt;/p>
&lt;ul>
&lt;li>&lt;code>documents.changes&lt;/code>: eventos crudos de cambio (insert/update/delete).&lt;/li>
&lt;li>&lt;code>documents.embedded&lt;/code>: eventos con embedding ya calculado.&lt;/li>
&lt;/ul>
&lt;h3 id="4-embedding-service">4. Embedding Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: consumir eventos de cambio, calcular embeddings, publicar al topic &lt;code>embedded&lt;/code>. Esta es la pieza que más coste consume si usas embeddings vía API (OpenAI, Cohere, Voyage AI).&lt;/p>
&lt;p>Estructura típica:&lt;/p>
&lt;ul>
&lt;li>Consumer Kafka con consumer group propio.&lt;/li>
&lt;li>Batching de eventos para llamadas embedding (mucho más eficiente que uno a uno).&lt;/li>
&lt;li>Llamadas paralelas con concurrency control.&lt;/li>
&lt;li>Retry con exponential backoff ante rate limits.&lt;/li>
&lt;li>Métricas exportadas (latencia, throughput, errores, coste).&lt;/li>
&lt;li>Idempotencia (key del topic = doc_id, mismo doc no se re-embedea sin necesidad).&lt;/li>
&lt;/ul>
&lt;p>Patrón de optimización clave: &lt;strong>deduplicate por hash de contenido&lt;/strong>. Si el documento se actualiza pero el texto no cambió (solo metadata), no merece la pena re-embedear. Hash + cache de embeddings ahorra 30-70% del coste en cargas reales.&lt;/p>
&lt;h3 id="5-indexing-worker">5. Indexing Worker&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: consumir el topic &lt;code>embedded&lt;/code> y hacer &lt;code>upsert&lt;/code> a Qdrant. Es la pieza más simple de toda la arquitectura: lee del topic, escribe al vector store. Pero importante para la fiabilidad: tiene que ser &lt;strong>idempotente&lt;/strong> (el mismo &lt;code>doc_id&lt;/code> puede llegar varias veces si el consumer reinicia) y &lt;strong>resiliente&lt;/strong> (si Qdrant está caído, reintentar sin perder eventos).&lt;/p>
&lt;p>Estructura: Consumer Kafka con commit manual de offset solo después de confirmación del upsert. Si Qdrant falla, el offset no se commitea y el evento se reprocesa.&lt;/p>
&lt;h3 id="6-retrieval-service">6. Retrieval Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: la cara que el LLM Service ve. Recibe una query del usuario, hace búsqueda en Qdrant (vector + filtros + reranker), enriquece los resultados con metadata fresca de PostgreSQL si hace falta, y devuelve top-K documentos con su contenido para que el LLM construya su prompt.&lt;/p>
&lt;p>Es &lt;strong>el único servicio que consulta Qdrant&lt;/strong>. Esto centraliza la lógica de retrieval: cuando quieras añadir reranking, hybrid search, query rewriting, lo haces aquí sin tocar el resto.&lt;/p>
&lt;h3 id="7-llm-service">7. LLM Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: la generación. Recibe del Retrieval Service el contexto + query, construye el prompt, llama al LLM (self-hosted vLLM o API externa vía LiteLLM), devuelve la respuesta. Lo cubrimos en posts anteriores; no es el foco aquí.&lt;/p>
&lt;h2 id="el-problema-del-dual-write-y-los-tres-patrones-de-solución">El problema del dual-write y los tres patrones de solución&lt;/h2>
&lt;p>Aquí está la pieza arquitectónica más importante del post. El problema: tu Domain Service necesita escribir a &lt;strong>dos lugares&lt;/strong>: PostgreSQL (el documento) y, indirectamente vía pipeline, Qdrant (el embedding del documento). Si lo haces ingenuamente —escribir a uno y luego al otro— tienes &lt;strong>el problema del dual-write&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>La escritura a Postgres tiene éxito, pero la publicación del evento a Kafka falla → &lt;strong>el embedding no se calcula, Qdrant nunca se entera&lt;/strong>.&lt;/li>
&lt;li>La publicación a Kafka tiene éxito, pero el commit a Postgres falla → &lt;strong>evento fantasma&lt;/strong>, el embedding se calcula sobre algo que no existe.&lt;/li>
&lt;li>El servicio crashea entre las dos operaciones → &lt;strong>estado parcial&lt;/strong>, no sabes qué pasó.&lt;/li>
&lt;/ul>
&lt;p>Distributed transactions (two-phase commit) son la solución teórica pero &lt;strong>nadie las quiere en producción&lt;/strong>: requieren coordinator XA, latencia alta, locking distribuido. La solución práctica son los patrones modernos. Tres opciones:&lt;/p>
&lt;h3 id="patrón-1--dual-write-naïve-prototipos">Patrón 1 — Dual-write naïve (prototipos)&lt;/h3>
&lt;p>El Domain Service escribe a Postgres, luego publica a Kafka:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">create_document&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transaction&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">doc_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;INSERT INTO documents ...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">kafka&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">publish&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;documents.changes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">doc_id&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Funciona&lt;/strong> en happy path. &lt;strong>Falla&lt;/strong> cuando algo entre las dos operaciones se rompe. Para prototipos donde la inconsistencia es aceptable, vale; para producción seria, no.&lt;/p>
&lt;h3 id="patrón-2--transactional-outbox--cdc-con-debezium-la-opción-correcta">Patrón 2 — Transactional outbox + CDC con Debezium (la opción correcta)&lt;/h3>
&lt;p>Solución elegante: &lt;strong>el Domain Service escribe a Postgres en una sola transacción que incluye tanto la tabla principal como una &lt;code>outbox&lt;/code> table&lt;/strong>. La outbox no es consumida directamente; &lt;strong>Debezium lee el WAL de Postgres y produce a Kafka los eventos de la outbox&lt;/strong>.&lt;/p>
&lt;p>Schema típico:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">outbox&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">gen_random_uuid&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">aggregate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;document&amp;#39;, &amp;#39;user&amp;#39;, &amp;#39;product&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">aggregate_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- el doc_id que cambió
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">event_type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;created&amp;#39;, &amp;#39;updated&amp;#39;, &amp;#39;deleted&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMPTZ&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NOW&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Cuando el Domain Service crea un documento:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">create_document&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transaction&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">doc_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;INSERT INTO documents (id, body) VALUES (...)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO outbox (aggregate, aggregate_id, event_type, payload) VALUES (...)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;document&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># transacción committed; Debezium leerá el WAL y publicará a Kafka&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">doc_id&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Lo crucial&lt;/strong>: las dos inserciones están en &lt;strong>la misma transacción ACID&lt;/strong> de Postgres. O las dos van, o ninguna va. Garantía absoluta de consistencia local.&lt;/p>
&lt;p>Configuración Debezium para leer la outbox:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;outbox-debezium-connector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector.class&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.connector.postgresql.PostgresConnector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.hostname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.dbname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;table.include.list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;public.outbox&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;outbox&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.transforms.outbox.EventRouter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.route.by.field&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;aggregate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.key&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;aggregate_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.payload&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;payload&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.route.topic.replacement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;${routedByValue}.changes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.storage.StringConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.json.JsonConverter&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>EventRouter&lt;/code> enruta a topics distintos según el valor de &lt;code>aggregate&lt;/code>: eventos de &lt;code>document&lt;/code> van a &lt;code>document.changes&lt;/code>, los de &lt;code>user&lt;/code> a &lt;code>user.changes&lt;/code>, etc.&lt;/p>
&lt;p>&lt;strong>Ventajas&lt;/strong>: garantía &amp;ldquo;exactly-once&amp;rdquo; desde el punto de vista de la aplicación; eventos en orden del commit; sin polling.&lt;/p>
&lt;p>&lt;strong>Coste&lt;/strong>: una tabla extra, una configuración Debezium, ~5-10 ms extra de latencia en la escritura.&lt;/p>
&lt;h3 id="patrón-3--event-driven-directo-event-sourcing-puro">Patrón 3 — Event-driven directo (event sourcing puro)&lt;/h3>
&lt;p>Variante más radical: &lt;strong>el evento es el primer ciudadano&lt;/strong>; PostgreSQL es solo una proyección. El Domain Service publica el evento a Kafka, y un consumer lo escribe a Postgres y otro lo procesa para embedding. &lt;strong>No hay tabla principal, no hay outbox&lt;/strong>; el log Kafka es la fuente de verdad.&lt;/p>
&lt;p>Más limpio conceptualmente pero requiere repensar el modelo de dominio (eventos como source of truth, queries reconstruidas de la proyección). Más adecuado para greenfield con equipo que entiende event sourcing.&lt;/p>
&lt;h3 id="comparativa">Comparativa&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Patrón&lt;/th>
&lt;th>Setup&lt;/th>
&lt;th>Consistencia&lt;/th>
&lt;th>Cuando&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Dual-write naïve&lt;/td>
&lt;td>Trivial&lt;/td>
&lt;td>Frágil&lt;/td>
&lt;td>Prototipos, PoC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Outbox + CDC&lt;/td>
&lt;td>Medio&lt;/td>
&lt;td>&lt;strong>Sólido&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Producción seria&lt;/strong> (default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Event-driven directo&lt;/td>
&lt;td>Alto&lt;/td>
&lt;td>Sólido&lt;/td>
&lt;td>Greenfield con event sourcing&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El default en 2026 para &lt;strong>producción&lt;/strong> es &lt;strong>outbox + CDC con Debezium&lt;/strong>. Es lo suficientemente simple para mantenerse, lo suficientemente robusto para no preocupar de noche.&lt;/p>
&lt;h2 id="manifest-completo-despliegue-qdrant-en-kubernetes">Manifest completo: despliegue Qdrant en Kubernetes&lt;/h2>
&lt;p>Ya cubrimos cómo se monta el resto del pipeline (Kafka, Debezium, Flink) en el &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">post anterior de Kafka&lt;/a>. La pieza que añadimos aquí es Qdrant. Despliegue típico vía Helm chart oficial:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml para qdrant/qdrant chart&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">replicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># cluster con 3 réplicas&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">repository&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qdrant/qdrant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;v1.14.0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">persistence&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storageClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;fast-ssd&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">8Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># clustering: cada réplica conoce a las otras&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">cluster&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">consensus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tickPeriodMs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># auth via API key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qdrant-auth&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># observability&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serviceMonitor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># scrapping desde kube-prometheus&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># snapshots periódicos&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">snapshots&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schedule&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0 3 * * *&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># diario a las 3 AM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retention&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;s3&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">s3&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;qdrant-snapshots-prod&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">performance&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">max_search_threads&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">quantization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">always_ram&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># quantized vectors en RAM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enable_tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y la creación de la colección con configuración para hybrid search:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">qdrant_client&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">QdrantClient&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">qdrant_client.models&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">VectorParams&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SparseVectorParams&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Distance&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">HnswConfigDiff&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ScalarQuantization&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ScalarType&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">QdrantClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;https://qdrant.internal:6333&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">API_KEY&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_collection&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">collection_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">vectors_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;dense&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">VectorParams&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1536&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">distance&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Distance&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">COSINE&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_disk&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># en RAM para latencia&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sparse_vectors_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;sparse&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">SparseVectorParams&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># para BM25-style lexical&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hnsw_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">HnswConfigDiff&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">m&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ef_construct&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">128&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_disk&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">quantization_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ScalarQuantization&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">scalar&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ScalarType&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">INT8&lt;/span> &lt;span class="c1"># 65% menos memoria&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_disk_payload&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># payload en disco&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">shard_number&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># particionado para escala&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">replication_factor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># cada shard replicado&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">write_consistency_factor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con esta config, una colección de 50M vectores de 1536 dimensiones ocupa ~150-200 GB en RAM (vs ~600 GB con float32 puro), con queries p95 sub-10ms en cargas típicas.&lt;/p>
&lt;h2 id="observabilidad-ver-qué-está-pasando">Observabilidad: ver qué está pasando&lt;/h2>
&lt;p>Cuatro métricas que cualquier dashboard mínimo de la etapa Data debería tener:&lt;/p>
&lt;h3 id="1-lag-del-outbox">1. Lag del outbox&lt;/h3>
&lt;p>&lt;code>debezium_lag_seconds&lt;/code>: cuánto tarda Debezium en leer un evento desde que se commitea. &lt;strong>Objetivo: &amp;lt;1 segundo&lt;/strong>. Si sube, indica WAL retention insuficiente o consumer rate menor que producer.&lt;/p>
&lt;h3 id="2-lag-del-embedding-service">2. Lag del embedding service&lt;/h3>
&lt;p>&lt;code>embedding_service_consumer_lag_messages&lt;/code>: cuántos eventos pendientes hay en el topic &lt;code>documents.changes&lt;/code>. &lt;strong>Objetivo: &amp;lt;100 sostenido&lt;/strong>. Si crece, indica que el rate de cambios supera la capacidad del embedding service. Soluciones: más consumers (paralelismo), batching más grande, modelo de embedding más rápido.&lt;/p>
&lt;h3 id="3-tasa-de-upsert-a-qdrant">3. Tasa de upsert a Qdrant&lt;/h3>
&lt;p>&lt;code>qdrant_upsert_rate&lt;/code> y &lt;code>qdrant_upsert_p95_latency&lt;/code>. &lt;strong>Objetivo: latencia &amp;lt;50 ms p95, tasa estable acorde al CDC rate&lt;/strong>. Si la latencia sube, Qdrant está degradado (memory pressure, disk slow, conn pool saturado).&lt;/p>
&lt;h3 id="4-recall-en-producción-offline-check">4. Recall en producción (offline check)&lt;/h3>
&lt;p>Una vez al día, ejecutar un job que toma N queries reales, busca en Qdrant, busca en pgvector si lo mantienes en paralelo, compara recall@k. Si Qdrant deja de devolver lo que debería, lo detectas antes de que un usuario se queje.&lt;/p>
&lt;h2 id="trampas-operativas-comunes">Trampas operativas comunes&lt;/h2>
&lt;h3 id="sin-outbox-el-equipo-aprende-dual-write-a-base-de-incidentes">Sin outbox: el equipo aprende dual-write a base de incidentes&lt;/h3>
&lt;p>Lo más común. La primera versión hace dual-write directo &amp;ldquo;para empezar simple&amp;rdquo;; un día se cae Kafka durante 10 minutos y miles de embeddings quedan sin generar. Migrar a outbox &lt;strong>después de tener tráfico&lt;/strong> es caro porque hay que backfill. &lt;strong>Outbox desde el día 1&lt;/strong>.&lt;/p>
&lt;h3 id="reembedding-ignorante-del-coste">Reembedding ignorante del coste&lt;/h3>
&lt;p>Cambias el modelo de embedding (&lt;code>text-embedding-3-small&lt;/code> → &lt;code>text-embedding-3-large&lt;/code>). Tu pipeline reemboda los 5M documentos. &lt;strong>17 horas y $1500 de coste&lt;/strong> que nadie anticipó. &lt;strong>Calcular reembedding upfront&lt;/strong>: documentos × tokens promedio × coste/1k tokens × throughput limits.&lt;/p>
&lt;h3 id="snapshot-de-qdrant-sin-testear-restore">Snapshot de Qdrant sin testear restore&lt;/h3>
&lt;p>Sacas snapshots diarios pero nunca pruebas restaurar. Un día Qdrant se corrompe y descubres que el snapshot está incompleto o que tu storage class no permite recuperarlo. &lt;strong>Test trimestral de restore&lt;/strong> en entorno paralelo, obligatorio para producción.&lt;/p>
&lt;h3 id="qdrant-detrás-de-service-clusterip-estándar-sin-grpc-affinity">Qdrant detrás de Service ClusterIP estándar sin gRPC affinity&lt;/h3>
&lt;p>Qdrant habla gRPC. Si el Service hace round-robin connection-level pero el cliente reusa connections, todo el tráfico va a un solo pod. &lt;strong>Headless Service + client-side load balancing&lt;/strong> o gRPC-aware service mesh.&lt;/p>
&lt;h3 id="pg-y-qdrant-sin-shared-trace-id">PG y Qdrant sin shared trace id&lt;/h3>
&lt;p>El Domain Service recibe un request, lo procesa, escribe a PG, dispara evento. Cuando un día algo va mal, no puedes correlar el span del Domain Service con el span del Indexing Worker porque no propagaste trace context. &lt;strong>OTel context propagation&lt;/strong> por el topic Kafka (vía headers Kafka), igual que hicimos en el &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">post de MCP observability&lt;/a>.&lt;/p>
&lt;h3 id="vector-y-metadata-en-sync-nominal-pero-no-real">Vector y metadata en sync nominal pero no real&lt;/h3>
&lt;p>PG dice &amp;ldquo;documento X tiene categoría tech&amp;rdquo;; Qdrant dice &amp;ldquo;documento X tiene categoría legal&amp;rdquo; (porque el cambio de categoría se actualizó en PG pero el evento de update no llegó a regenerar el payload en Qdrant). Filtras &lt;code>category=tech&lt;/code>, no aparece. &lt;strong>Tests periódicos de consistencia cross-store&lt;/strong> sobre muestreo aleatorio.&lt;/p>
&lt;h3 id="dimensión-del-vector-hardcodeada-en-mil-sitios">Dimensión del vector hardcodeada en mil sitios&lt;/h3>
&lt;p>&lt;code>1536&lt;/code> aparece en el código del Domain Service, del Embedding Service, del Indexing Worker, del Retrieval Service, en la creación de la colección Qdrant. Cuando cambias modelo (a uno de 768 dimensiones), olvidas uno y todo se rompe. &lt;strong>Configuración centralizada&lt;/strong> del modelo + dimensión.&lt;/p>
&lt;h3 id="sin-rate-limiting-al-embedding-provider">Sin rate limiting al embedding provider&lt;/h3>
&lt;p>Tu CDC procesa una migración masiva: 1M documentos cambian. El embedding service intenta procesar todo a la vez. &lt;strong>OpenAI te rate-limita&lt;/strong>, el consumer queda atascado, los eventos se acumulan, tu cluster Kafka queda con horas de lag. &lt;strong>Rate limiting en el consumer&lt;/strong>, no en el producer.&lt;/p>
&lt;h2 id="cuándo-no-usar-qdrant-el-contrapunto-honesto">Cuándo NO usar Qdrant: el contrapunto honesto&lt;/h2>
&lt;p>Para no presentar Qdrant como bala de plata:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tu corpus es &amp;lt;1M vectores&lt;/strong> y no esperas crecer. pgvector basta y te ahorra un servicio.&lt;/li>
&lt;li>&lt;strong>Tu equipo es pequeño y no tiene capacidad de operar un stateful service más&lt;/strong>. Qdrant añade snapshots, gRPC, mTLS, observabilidad propia. Cada uno de esos puntos es un día de trabajo de un SRE.&lt;/li>
&lt;li>&lt;strong>Tu retrieval es batch off-hours&lt;/strong>, no real-time. Si solo haces RAG para reportes nocturnos, la latencia de pgvector no duele.&lt;/li>
&lt;li>&lt;strong>Necesitas JOINs nativos&lt;/strong> entre embeddings y tablas relacionales en queries críticos. pgvector permite hacer &lt;code>JOIN documents d ON d.id = embedding.doc_id WHERE d.tenant_id = X&lt;/code>. Qdrant lo simula con payload pero menos elegante.&lt;/li>
&lt;/ul>
&lt;p>Y al revés, cuando Qdrant &lt;strong>gana claramente&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Corpus &amp;gt;10M vectores con queries con filtros densos.&lt;/li>
&lt;li>Necesidad de hybrid search nativo (sparse + dense + multivector).&lt;/li>
&lt;li>Multi-tenant con strict latency requirements por cliente.&lt;/li>
&lt;li>Quantization agresiva para mantener todo en RAM en hardware limitado.&lt;/li>
&lt;li>Cluster mode con sharding horizontal real.&lt;/li>
&lt;/ul>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Migración pgvector → Qdrant en vivo&lt;/strong>: patrón con dual-read durante la transición.&lt;/li>
&lt;li>&lt;strong>Vector search federation&lt;/strong>: queries que cruzan múltiples Qdrant collections o múltiples vector stores. Tema propio.&lt;/li>
&lt;li>&lt;strong>Multi-tenancy en Qdrant&lt;/strong>: payload filters + namespace isolation + per-tenant rate limiting.&lt;/li>
&lt;li>&lt;strong>Cold storage para vectores antiguos&lt;/strong>: archivo de partitions a object storage con índice secundario.&lt;/li>
&lt;li>&lt;strong>Embedding model self-hosted con vLLM&lt;/strong>: alternativa a OpenAI API que reduce coste y mejora privacidad. Tema cruzado con la serie de inferencia.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>PostgreSQL y pgvector:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/pgvector/pgvector">PostgreSQL pgvector extension (GitHub)&lt;/a> — el de toda la vida.&lt;/li>
&lt;li>&lt;a href="https://www.tigerdata.com/blog/pgvector-vs-qdrant">Pgvector vs Qdrant (Tiger Data)&lt;/a> — comparativa con números.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/blog/pgvector-tradeoffs/">Start with pgvector: Why You&amp;rsquo;ll Outgrow It Faster Than You Think (Qdrant blog)&lt;/a> — los tradeoffs honestos desde Qdrant.&lt;/li>
&lt;/ul>
&lt;p>Qdrant:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://qdrant.tech/">Qdrant — sitio oficial&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/documentation/private-cloud/changelog/">Qdrant changelog&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/articles/sparse-vectors/">Sparse Vectors in Qdrant&lt;/a> — hybrid search nativo.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/documentation/tutorials-search-engineering/using-multivector-representations/">Multivectors and Late Interaction&lt;/a> — ColBERT-style.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/blog/2025-recap/">Qdrant 2025 Recap: Powering the Agentic Era&lt;/a> — estado del proyecto y roadmap.&lt;/li>
&lt;/ul>
&lt;p>Outbox y CDC:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/">Reliable Microservices Data Exchange With the Outbox Pattern (Debezium blog)&lt;/a> — el post canónico.&lt;/li>
&lt;li>&lt;a href="https://streamkap.com/resources-and-guides/outbox-pattern-explained">The Outbox Pattern Explained (Streamkap)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://thorben-janssen.com/outbox-pattern-with-cdc-and-debezium/">Outbox Pattern with Debezium (Thorben Janssen)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://debezium.io/blog/2020/02/10/event-sourcing-vs-cdc/">Distributed Data for Microservices — Event Sourcing vs CDC (Debezium blog)&lt;/a> — comparativa entre patrones.&lt;/li>
&lt;/ul>
&lt;p>Comparativas 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.knowsync.ai/blog/choosing-vector-database-qdrant-pinecone-pgvector-2026">Choosing Your Vector Database: Qdrant vs Pinecone vs pgvector in 2026 (KnowSync)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://open-techstack.com/blog/pgvector-vs-qdrant-2026/">pgvector vs Qdrant: Production Tradeoffs 2026 (Open Techstack)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://markaicode.com/vs/qdrant-vs-pgvector/">qdrant vs pgvector: Which Vector Database Should You Choose in 2026 (Markaicode)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.marktechpost.com/2026/05/10/best-vector-databases-in-2026-pricing-scale-limits-and-architecture-tradeoffs-across-nine-leading-systems/">Best Vector Databases in 2026: Pricing, Scale Limits, Architecture Tradeoffs (MarkTechPost)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://callsphere.ai/blog/vector-database-benchmarks-2026-pgvector-qdrant-weaviate-milvus-lancedb">Vector Database Benchmarks 2026: pgvector 0.9, Qdrant, Weaviate, Milvus, LanceDB (CallSphere)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Post anterior: &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline LLMOps de 6 etapas&lt;/a> — donde definimos el mini-mapa.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka — arquitectura técnica&lt;/a> — la pieza que precede a Qdrant en el pipeline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama MLOps LLMs 2026&lt;/a> — el marco general.&lt;/li>
&lt;li>Series previas: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">post-tracing&lt;/a> y &lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>El pipeline LLMOps de seis etapas: arquitectura global y deep dive en cada componente</title><link>https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/</link><pubDate>Thu, 21 May 2026 06:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Los dos primeros posts de la serie establecieron el &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">panorama LLMOps&lt;/a> y bajaron al detalle del &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">pipeline de datos con Kafka&lt;/a>. Este post hace el zoom intermedio: dibuja &lt;strong>el mapa completo del sistema&lt;/strong> —una arquitectura global de un LLMOps moderno con todas las piezas que el campo ha estabilizado en 2026— y entra en profundidad en cada una de las &lt;strong>seis etapas canónicas del pipeline&lt;/strong>: &lt;strong>Data&lt;/strong>, &lt;strong>Tune&lt;/strong>, &lt;strong>Eval&lt;/strong>, &lt;strong>Deploy&lt;/strong>, &lt;strong>Observe&lt;/strong>, &lt;strong>Retrain&lt;/strong>. Para cada etapa damos las &lt;strong>sub-tareas operativas&lt;/strong>, las &lt;strong>herramientas dominantes&lt;/strong>, las &lt;strong>decisiones de diseño&lt;/strong> que aparecen siempre, y las &lt;strong>trampas específicas&lt;/strong> que se ven repetidamente en producción. Y, lo más importante operativamente: cada etapa lleva un &lt;strong>mini-mapa &amp;ldquo;estás aquí&amp;rdquo;&lt;/strong> sobre el ciclo, que se reutilizará en cualquier post posterior de la serie para situar al lector. La idea: que cualquiera leyendo un post sobre fine-tuning, sobre prompt versioning, sobre eval gates o sobre drift detection, pueda mirar el mini-mapa y saber inmediatamente en qué pieza del sistema más grande está pensando ese día.&lt;/p>
&lt;blockquote>
&lt;p>Este es el &lt;strong>tercer post de la serie MLOps específico para LLMs&lt;/strong>. Anteriores: &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama 2026&lt;/a> y &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka&lt;/a>. Aquí pasamos de &amp;ldquo;el qué&amp;rdquo; y &amp;ldquo;una pieza&amp;rdquo; a &lt;strong>el mapa entero&lt;/strong>, con detalle por etapa.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-arquitectura-global-el-mapa-maestro">La arquitectura global: el mapa maestro&lt;/h2>
&lt;p>Antes de bajar a cada etapa, fijemos el mapa entero. Lo que sigue es el dibujo de referencia de un sistema LLMOps de producción en 2026, con todos los componentes que el campo ha estabilizado en su lugar:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 580" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Arquitectura global LLMOps 2026">
&lt;style>.title{font:700 14px sans-serif;fill:#222}.stage-title{font:700 13px sans-serif;fill:#222}.lbl{font:11px sans-serif;fill:#333}.sm{font:10px sans-serif;fill:#555}.tiny{font:9px sans-serif;fill:#666}.stage{stroke:#444;stroke-width:1.5;rx:8}.data{fill:#ffe9d6}.tune{fill:#ffd6d6}.eval{fill:#d6eaff}.deploy{fill:#d9f5d6}.obs{fill:#e9d6f5}.retrain{fill:#fff5b0}.cross{fill:#f0f0f0;stroke:#888;stroke-dasharray:4 2;rx:6}.arr{stroke:#444;stroke-width:1.6;fill:none;marker-end:url(#ar)}.cycle{stroke:#888;stroke-width:1.4;fill:none;marker-end:url(#ar);stroke-dasharray:6 3}&lt;/style>
&lt;defs>&lt;marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="22" text-anchor="middle" class="title">Arquitectura global LLMOps 2026 — las seis etapas y los componentes transversales&lt;/text>
&lt;rect x="20" y="50" width="240" height="170" class="stage data"/>
&lt;text x="140" y="72" text-anchor="middle" class="stage-title">1 · DATA&lt;/text>
&lt;text x="35" y="92" class="sm">• Origenes: OLTP, APIs, logs, scraping&lt;/text>
&lt;text x="35" y="108" class="sm">• CDC: Debezium, Flink CDC&lt;/text>
&lt;text x="35" y="124" class="sm">• Transport: Kafka + Schema Registry&lt;/text>
&lt;text x="35" y="140" class="sm">• Stream proc: Flink SQL, RisingWave&lt;/text>
&lt;text x="35" y="156" class="sm">• Versioning: DVC + lakeFS&lt;/text>
&lt;text x="35" y="172" class="sm">• Tableflow → Iceberg/Delta&lt;/text>
&lt;text x="35" y="188" class="sm">• Vector stores: Milvus, Qdrant,&lt;/text>
&lt;text x="35" y="202" class="sm"> Weaviate, pgvector, LanceDB&lt;/text>
&lt;rect x="280" y="50" width="240" height="170" class="stage tune"/>
&lt;text x="400" y="72" text-anchor="middle" class="stage-title">2 · TUNE&lt;/text>
&lt;text x="295" y="92" class="sm">• Modalidades: fine-tune / RAG /&lt;/text>
&lt;text x="295" y="106" class="sm"> agent training&lt;/text>
&lt;text x="295" y="124" class="sm">• Frameworks: PEFT, Axolotl, TRL,&lt;/text>
&lt;text x="295" y="138" class="sm"> Unsloth, llama-factory&lt;/text>
&lt;text x="295" y="156" class="sm">• Técnicas: LoRA, QLoRA, DPO, RLHF&lt;/text>
&lt;text x="295" y="172" class="sm">• Clusters: H100/B200 + NVLink&lt;/text>
&lt;text x="295" y="188" class="sm">• Experiment tracking: MLflow, W&amp;amp;B&lt;/text>
&lt;text x="295" y="202" class="sm">• Adapter registry: HF Hub privado&lt;/text>
&lt;rect x="540" y="50" width="220" height="170" class="stage eval"/>
&lt;text x="650" y="72" text-anchor="middle" class="stage-title">3 · EVAL&lt;/text>
&lt;text x="555" y="92" class="sm">• CI frameworks: DeepEval,&lt;/text>
&lt;text x="555" y="106" class="sm"> Promptfoo, Ragas, OpenAI Evals&lt;/text>
&lt;text x="555" y="124" class="sm">• Platforms: Langfuse, LangSmith,&lt;/text>
&lt;text x="555" y="138" class="sm"> Phoenix, Braintrust&lt;/text>
&lt;text x="555" y="156" class="sm">• Judge LLM (G-Eval, Prometheus)&lt;/text>
&lt;text x="555" y="172" class="sm">• Golden dataset versionado&lt;/text>
&lt;text x="555" y="188" class="sm">• Eval gates en CI/CD&lt;/text>
&lt;text x="555" y="202" class="sm">• Calibración 85-90% vs humano&lt;/text>
&lt;rect x="20" y="245" width="240" height="170" class="stage deploy"/>
&lt;text x="140" y="267" text-anchor="middle" class="stage-title">4 · DEPLOY&lt;/text>
&lt;text x="35" y="287" class="sm">• Model registry: MLflow, OME&lt;/text>
&lt;text x="35" y="303" class="sm">• Serving: vLLM, SGLang, TRT-LLM&lt;/text>
&lt;text x="35" y="319" class="sm">• Operators K8s: vLLM Prod Stack,&lt;/text>
&lt;text x="35" y="333" class="sm"> KServe, OME, NVIDIA Dynamo, llm-d&lt;/text>
&lt;text x="35" y="349" class="sm">• Gateway / router: LiteLLM&lt;/text>
&lt;text x="35" y="365" class="sm">• Estrategias: canary, blue-green,&lt;/text>
&lt;text x="35" y="379" class="sm"> shadow, A/B versioning&lt;/text>
&lt;text x="35" y="395" class="sm">• Autoscaling: KEDA + métricas LLM&lt;/text>
&lt;rect x="280" y="245" width="240" height="170" class="stage obs"/>
&lt;text x="400" y="267" text-anchor="middle" class="stage-title">5 · OBSERVE&lt;/text>
&lt;text x="295" y="287" class="sm">• Tracing: OpenLLMetry, Langfuse,&lt;/text>
&lt;text x="295" y="301" class="sm"> Phoenix, LangSmith&lt;/text>
&lt;text x="295" y="319" class="sm">• Métricas: Prometheus, Grafana&lt;/text>
&lt;text x="295" y="335" class="sm">• Guardrails: NeMo, Llama Guard 4,&lt;/text>
&lt;text x="295" y="349" class="sm"> LLM Guard, Lakera&lt;/text>
&lt;text x="295" y="367" class="sm">• eBPF: Hubble, Tetragon, AgentSight&lt;/text>
&lt;text x="295" y="383" class="sm">• MCP observability (OTel GenAI)&lt;/text>
&lt;text x="295" y="399" class="sm">• Drift: Evidently, NannyML, WhyLabs&lt;/text>
&lt;rect x="540" y="245" width="220" height="170" class="stage retrain"/>
&lt;text x="650" y="267" text-anchor="middle" class="stage-title">6 · RETRAIN&lt;/text>
&lt;text x="555" y="287" class="sm">• Feedback explícito (thumbs)&lt;/text>
&lt;text x="555" y="303" class="sm">• Feedback implícito (latencia,&lt;/text>
&lt;text x="555" y="317" class="sm"> abandonment, retries)&lt;/text>
&lt;text x="555" y="335" class="sm">• Triaging de incidentes&lt;/text>
&lt;text x="555" y="351" class="sm">• Dataset enrichment con casos&lt;/text>
&lt;text x="555" y="365" class="sm"> donde el modelo falló&lt;/text>
&lt;text x="555" y="383" class="sm">• Cadence: trimestral o&lt;/text>
&lt;text x="555" y="397" class="sm"> incident-driven&lt;/text>
&lt;rect x="100" y="440" width="580" height="120" class="cross"/>
&lt;text x="390" y="462" text-anchor="middle" class="stage-title">Componentes transversales (atraviesan todas las etapas)&lt;/text>
&lt;text x="115" y="482" class="sm">• OpenTelemetry Collector (gen_ai.* y mcp.* semantic conventions)&lt;/text>
&lt;text x="115" y="498" class="sm">• Prompt versioning: Langfuse / MLflow Prompts (versionado v1/v2/v3 + labels + cache)&lt;/text>
&lt;text x="115" y="514" class="sm">• MCP servers + MCP Gateway (Traefik Hub, MintMCP) — interfaz herramientas-modelo&lt;/text>
&lt;text x="115" y="530" class="sm">• Model gateway: LiteLLM (100+ providers unificados como una API OpenAI-compatible)&lt;/text>
&lt;text x="115" y="546" class="sm">• Schema Registry (Avro/Protobuf/JSON Schema) compartido entre data y serving&lt;/text>
&lt;path class="arr" d="M260,135 L280,135"/>
&lt;path class="arr" d="M520,135 L540,135"/>
&lt;path class="arr" d="M650,220 L650,245"/>
&lt;path class="arr" d="M540,330 L520,330"/>
&lt;path class="arr" d="M280,330 L260,330"/>
&lt;path class="arr" d="M140,415 L140,440"/>
&lt;path class="arr" d="M400,415 L400,440"/>
&lt;path class="arr" d="M650,415 L650,440"/>
&lt;path class="cycle" d="M650,330 C780,330 780,135 760,135 L760,135"/>
&lt;text x="745" y="245" class="sm" text-anchor="middle">ciclo&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Lo que ves: las &lt;strong>seis cajas grandes&lt;/strong> son las etapas; las &lt;strong>flechas continuas&lt;/strong> son el flujo del pipeline; la &lt;strong>flecha discontinua&lt;/strong> que va de &lt;strong>Retrain&lt;/strong> a &lt;strong>Data&lt;/strong> es el ciclo de feedback que convierte LLMOps en un proceso vivo, no en un proyecto que termina. La banda gris al pie son &lt;strong>componentes transversales&lt;/strong> —observabilidad, prompt versioning, MCP, gateway, schema— que atraviesan todas las etapas y se conectan a cada una.&lt;/p>
&lt;p>Tres lecturas rápidas del mapa:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Horizontal arriba&lt;/strong>: el camino feliz, &lt;strong>data → tune → eval&lt;/strong>. Lo que pasa cuando preparas el modelo.&lt;/li>
&lt;li>&lt;strong>Horizontal abajo&lt;/strong>: el camino de servicio, &lt;strong>deploy → observe → retrain&lt;/strong>. Lo que pasa cuando el modelo está vivo.&lt;/li>
&lt;li>&lt;strong>Vertical&lt;/strong>: la conexión entre los dos pisos. Eval gateway alimenta Deploy; Observe alimenta Retrain; Retrain devuelve a Data.&lt;/li>
&lt;/ul>
&lt;p>Cada etapa de aquí en adelante incluirá un &lt;strong>mini-mapa de navegación&lt;/strong> (&amp;ldquo;estás aquí&amp;rdquo;) para situarte en el ciclo completo. Vamos a cada una.&lt;/p>
&lt;h2 id="etapa-1--data-ingestión-transporte-versionado-indexación">Etapa 1 — Data: ingestión, transporte, versionado, indexación&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Data">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn)}&lt;/style>
&lt;defs>&lt;marker id="mn" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DATA · ingestión → transporte → versionado → indexación&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box active"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h3 id="sub-tareas-operativas">Sub-tareas operativas&lt;/h3>
&lt;p>La etapa Data es la más infravalorada y la que más bloquea proyectos. Sus sub-tareas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Ingestión&lt;/strong> desde origenes heterogéneos: bases de datos OLTP (Postgres, MySQL), APIs externas, file shares, scraping, sistemas SaaS, logs de aplicaciones, mensajería interna.&lt;/li>
&lt;li>&lt;strong>Captura de cambios&lt;/strong> (CDC) en streaming si el dato es dinámico. Debezium sobre Kafka, Flink CDC, alternativas modernas como RisingWave que lee WAL directamente.&lt;/li>
&lt;li>&lt;strong>Transformación&lt;/strong> (cleansing, dedup, normalización, sanitization de PII).&lt;/li>
&lt;li>&lt;strong>Schema management&lt;/strong>: registro de esquemas, evolución compatible, compatibilidad backward/forward.&lt;/li>
&lt;li>&lt;strong>Versionado&lt;/strong> de datasets de training y golden datasets: DVC + lakeFS (unificadas en noviembre 2025).&lt;/li>
&lt;li>&lt;strong>Indexación&lt;/strong> para RAG: chunking, embeddings, escritura a vector stores. Cubierto en profundidad en el &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">post de Kafka&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Materialización&lt;/strong> a tablas analíticas: Tableflow → Iceberg/Delta, para consumo de BI y queries de baja latencia.&lt;/li>
&lt;/ul>
&lt;h3 id="herramientas-dominantes">Herramientas dominantes&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Sub-tarea&lt;/th>
&lt;th>Herramientas 2026&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CDC&lt;/td>
&lt;td>Debezium, Flink CDC, RisingWave&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Transport&lt;/td>
&lt;td>Kafka (Confluent Cloud, Redpanda, Apache puro)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Schema Registry&lt;/td>
&lt;td>Confluent Schema Registry, Apicurio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stream processing&lt;/td>
&lt;td>Apache Flink, RisingWave, Kafka Streams&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Versionado de datos&lt;/td>
&lt;td>DVC + lakeFS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector stores&lt;/td>
&lt;td>Milvus, Qdrant, Weaviate, pgvector, LanceDB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tablas materializadas&lt;/td>
&lt;td>Tableflow → Iceberg/Delta&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ETL/ELT batch (cuando aplica)&lt;/td>
&lt;td>dbt + Snowflake/Databricks&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="decisiones-de-diseño">Decisiones de diseño&lt;/h3>
&lt;p>Las tres decisiones que aparecen siempre:&lt;/p>
&lt;p>&lt;strong>Batch vs streaming&lt;/strong>: cuanto más dinámico sea el dato, más streaming. Para corpus estáticos (manuales que nunca cambian) batch nocturno basta; para datos transaccionales que el agente necesita ver minuto a minuto, streaming desde el día 1.&lt;/p>
&lt;p>&lt;strong>Embedding model&lt;/strong>: cambiar el modelo de embeddings invalida todos los vectores indexados. Decisión arquitectónica: pinning del modelo + plan explícito de migración (dual-index pattern visto en el post de Kafka).&lt;/p>
&lt;p>&lt;strong>Vector store&lt;/strong>: pgvector si ya tienes Postgres operado y eres &amp;lt;10M vectores; Qdrant si quieres simplicidad mid-scale; Milvus si necesitas billones; Weaviate si valoras hybrid search nativo.&lt;/p>
&lt;h3 id="trampas">Trampas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Hardcodear conexiones a la fuente&lt;/strong> (sin abstracción): cuando la base de datos cambia (versión, host, esquema), rompes todo el pipeline. &lt;strong>Adapter layer&lt;/strong> desde el día 1.&lt;/li>
&lt;li>&lt;strong>Sin schema registry&lt;/strong>: los topics empiezan a romperse silenciosamente.&lt;/li>
&lt;li>&lt;strong>Reindexación full cuando algo cambia&lt;/strong>: cuesta horas o días. Diseñar &lt;strong>dual-index pattern&lt;/strong> desde el principio.&lt;/li>
&lt;li>&lt;strong>PII no sanitizada&lt;/strong>: el RAG está sirviendo datos sensibles sin querer. Anonymización en el pipeline, no en el consumo.&lt;/li>
&lt;/ul>
&lt;h2 id="etapa-2--tune-preparar-el-modelo-para-tu-caso">Etapa 2 — Tune: preparar el modelo para tu caso&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Tune">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff7777;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn2)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn2)}&lt;/style>
&lt;defs>&lt;marker id="mn2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: TUNE · fine-tuning / RAG-as-tuning / agent training&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box active"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h3 id="sub-tareas-operativas-1">Sub-tareas operativas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Selección de modelo base&lt;/strong>: Llama, Qwen, Mistral, Gemma, DeepSeek según licencia, tamaño, calidad en tu dominio.&lt;/li>
&lt;li>&lt;strong>Preparación del dataset&lt;/strong>: split train/val/test, formato (chat templates, JSONL), augmentación si aplica.&lt;/li>
&lt;li>&lt;strong>Configuración del adapter&lt;/strong>: LoRA rank, target modules, alpha; QLoRA si quieres entrenar en una GPU consumer; full fine-tune solo si tienes presupuesto.&lt;/li>
&lt;li>&lt;strong>Training loop&lt;/strong>: HuggingFace Transformers + PEFT + TRL como stack canónico; Axolotl o llama-factory como wrappers convenience; Unsloth si quieres 2-4× más velocidad en GPUs consumer.&lt;/li>
&lt;li>&lt;strong>Hyperparameter sweep&lt;/strong>: W&amp;amp;B Sweeps, Optuna, Ray Tune.&lt;/li>
&lt;li>&lt;strong>Checkpointing y resumability&lt;/strong>: save cada N pasos, resume desde fallo.&lt;/li>
&lt;li>&lt;strong>Promotion&lt;/strong>: el adapter promueve al registry tras pasar la siguiente etapa (Eval).&lt;/li>
&lt;/ul>
&lt;h3 id="las-tres-modalidades-de-tune">Las tres modalidades de Tune&lt;/h3>
&lt;p>Detalle del cuadro que vimos en el &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">panorama&lt;/a>:&lt;/p>
&lt;p>&lt;strong>Fine-tuning supervisado (SFT)&lt;/strong> con LoRA/QLoRA. Recoges pares (prompt, ideal-response), aplicas SFT con cross-entropy loss. Lo más simple. La regla del pulgar: &lt;strong>300-3 000 ejemplos&lt;/strong> bien curados suelen ser más útiles que 50 000 ruidosos.&lt;/p>
&lt;p>&lt;strong>DPO (Direct Preference Optimization)&lt;/strong> y &lt;strong>RLAIF&lt;/strong>. En vez de &amp;ldquo;ideal-response&amp;rdquo;, recoges pares &lt;strong>(prompt, respuesta_buena, respuesta_mala)&lt;/strong> y entrenas al modelo a preferir la buena. Más estable que RLHF clásico, mismo objetivo. Es lo que la mayoría de equipos usa cuando van más allá de SFT.&lt;/p>
&lt;p>&lt;strong>Agent training&lt;/strong> (RFT / Reinforcement Fine-Tuning, RLHF puro). Para casos donde el modelo necesita aprender &lt;strong>trayectorias multistep&lt;/strong>: cuándo elegir tool A vs B, cuándo pedir confirmación, cómo descomponer una tarea grande. Mucho más caro y complejo. Lo de OpenAI con RFT marcó el patrón en 2024-2025; en 2026 está saliendo del experimental.&lt;/p>
&lt;p>&lt;strong>RAG como alternativa a Tune&lt;/strong>: aunque conceptualmente es otra etapa (vive en Data + Deploy), funcionalmente compite con fine-tuning para muchos casos. El veredicto 2026: &lt;strong>hybrid es default&lt;/strong> (60% de despliegues), fine-tune para behavior + RAG para conocimiento volátil.&lt;/p>
&lt;h3 id="herramientas">Herramientas&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Aspecto&lt;/th>
&lt;th>Herramientas 2026&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Framework base&lt;/td>
&lt;td>HuggingFace Transformers, PEFT, TRL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Wrappers convenience&lt;/td>
&lt;td>Axolotl, llama-factory&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Velocidad consumer&lt;/td>
&lt;td>Unsloth (2-4× speedup en GPUs RTX)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Distributed training&lt;/td>
&lt;td>DeepSpeed, FSDP, NeMo Framework&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Experiment tracking&lt;/td>
&lt;td>MLflow, W&amp;amp;B, ClearML&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Adapter registry&lt;/td>
&lt;td>HuggingFace Hub privado, MLflow registry&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hyperparameter&lt;/td>
&lt;td>W&amp;amp;B Sweeps, Optuna, Ray Tune&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="trampas-1">Trampas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Catastrophic forgetting&lt;/strong>: SFT muy agresivo destruye capacidades generales del modelo. Conservar small % del dataset original o usar regularización.&lt;/li>
&lt;li>&lt;strong>Overfitting al golden dataset&lt;/strong>: el modelo aprende a memorizar el set de eval. Mantener un &lt;strong>test set holdout&lt;/strong> que nadie del equipo mira hasta el release final.&lt;/li>
&lt;li>&lt;strong>Train/serve skew&lt;/strong>: prompts en training con formato distinto al de producción. &lt;strong>Mismo chat template&lt;/strong> en ambos.&lt;/li>
&lt;li>&lt;strong>Lora rank demasiado alto&lt;/strong>: parece mejorar metricas pero infla el adapter sin beneficio real. Empezar con &lt;code>r=8&lt;/code> o &lt;code>r=16&lt;/code>; subir solo si hay evidencia.&lt;/li>
&lt;/ul>
&lt;h2 id="etapa-3--eval-validar-antes-de-promover">Etapa 3 — Eval: validar antes de promover&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Eval">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7aafff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn3)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn3)}&lt;/style>
&lt;defs>&lt;marker id="mn3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: EVAL · CI gates + platform regression + human review&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box active"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h3 id="sub-tareas-operativas-2">Sub-tareas operativas&lt;/h3>
&lt;p>Cubierto en profundidad en &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals: la capa después del tracing&lt;/a>. Resumen estructurado para el pipeline:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Curación del golden dataset&lt;/strong>: 100-500 ejemplos como mínimo, mantenidos activamente con casos de incidentes.&lt;/li>
&lt;li>&lt;strong>Evaluators&lt;/strong>: heurísticos (regex, length), semánticos (embeddings), LLM-as-judge (G-Eval), humanos (golden labels).&lt;/li>
&lt;li>&lt;strong>Ejecución en CI&lt;/strong>: bloquear el merge si métricas críticas caen &amp;gt;X%.&lt;/li>
&lt;li>&lt;strong>Ejecución en platform&lt;/strong>: sobre tráfico de producción muestreado, persistir resultados, detectar regresión a largo plazo.&lt;/li>
&lt;li>&lt;strong>Calibración del judge&lt;/strong>: 85-90% agreement con humanos antes de aceptar el judge como productivo.&lt;/li>
&lt;li>&lt;strong>Eval gates&lt;/strong>: thresholds explícitos por métrica (faithfulness &amp;gt; 0.85, relevancy &amp;gt; 0.80, etc.).&lt;/li>
&lt;/ul>
&lt;h3 id="herramientas-1">Herramientas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>CI gates&lt;/strong>: DeepEval (Apache 2.0, pytest-style), Promptfoo (MIT, CLI), Ragas (RAG-specific), Inspect AI (safety/capability).&lt;/li>
&lt;li>&lt;strong>Platform&lt;/strong>: Langfuse (MIT, suite completa), LangSmith (LangChain), Phoenix (ELv2, OTel), Braintrust.&lt;/li>
&lt;li>&lt;strong>Judges&lt;/strong>: GPT-4 (caro pero referencia), Claude 3.5 Sonnet, Prometheus (OSS 0.897 correlación), JudgeLM.&lt;/li>
&lt;/ul>
&lt;h3 id="trampas-2">Trampas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Golden dataset envejecido&lt;/strong>: si no se actualiza, deja de reflejar producción.&lt;/li>
&lt;li>&lt;strong>Judge contaminado&lt;/strong>: el judge sabe del dataset (apareció en su training).&lt;/li>
&lt;li>&lt;strong>Sample size insuficiente&lt;/strong>: &amp;lt;50 ejemplos hace que diferencias parezcan ruido.&lt;/li>
&lt;li>&lt;strong>Costes runaway&lt;/strong>: G-Eval con GPT-4 sobre muchos casos cuesta miles USD/mes.&lt;/li>
&lt;li>&lt;strong>Olvidar el segmento&lt;/strong>: media 0.85 puede esconder 0.55 en alemán.&lt;/li>
&lt;/ul>
&lt;h2 id="etapa-4--deploy-poner-el-modelo-en-producción">Etapa 4 — Deploy: poner el modelo en producción&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7adb7a;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn4)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn4)}&lt;/style>
&lt;defs>&lt;marker id="mn4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DEPLOY · operators + serving + canary + autoscaling&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h3 id="sub-tareas-operativas-3">Sub-tareas operativas&lt;/h3>
&lt;p>Cubierto en profundidad en &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a> y &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a>. Resumen para el pipeline:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Selección del runtime&lt;/strong>: vLLM (default), SGLang (agentes con prefix caching alto), TensorRT-LLM (latencia pura), llama.cpp (edge).&lt;/li>
&lt;li>&lt;strong>Selección del operator&lt;/strong>: vLLM Production Stack, KServe, OME (LMSYS), NVIDIA Dynamo, llm-d (CNCF).&lt;/li>
&lt;li>&lt;strong>Configuración del serving&lt;/strong>: &lt;code>--tensor-parallel-size&lt;/code>, &lt;code>--kv-cache-dtype=fp8&lt;/code>, &lt;code>--enable-prefix-caching&lt;/code>, &lt;code>--enable-chunked-prefill&lt;/code>, &lt;code>--gpu-memory-utilization=0.92&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Routing entre modelos&lt;/strong>: LiteLLM como abstracción para multi-provider.&lt;/li>
&lt;li>&lt;strong>Estrategia de release&lt;/strong>: canary (1% → 10% → 100%), blue-green (todo o nada con rollback rápido), shadow (eval en paralelo sin afectar usuarios).&lt;/li>
&lt;li>&lt;strong>Autoscaling con métricas LLM&lt;/strong>: KEDA + Prometheus sobre &lt;code>vllm:num_requests_waiting&lt;/code> o equivalente.&lt;/li>
&lt;li>&lt;strong>Gateway / Inference Extension&lt;/strong>: Gateway API Inference Extension cuando esté GA.&lt;/li>
&lt;/ul>
&lt;h3 id="herramientas-dominantes-1">Herramientas dominantes&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Serving engines&lt;/strong>: vLLM, SGLang, TensorRT-LLM, llama.cpp, MLX.&lt;/li>
&lt;li>&lt;strong>Operators&lt;/strong>: OME, vLLM Production Stack, NVIDIA Dynamo, llm-d, KServe.&lt;/li>
&lt;li>&lt;strong>Routing&lt;/strong>: LiteLLM (100+ providers), OpenRouter (managed), LangChain Router.&lt;/li>
&lt;li>&lt;strong>GPU primitivas&lt;/strong>: NVIDIA GPU Operator, LeaderWorkerSet (LWS) para tensor parallel multi-pod, KEDA para autoscaling.&lt;/li>
&lt;/ul>
&lt;h3 id="trampas-3">Trampas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Rolling update naïve&lt;/strong> que corta sesiones: &lt;code>maxUnavailable: 0, maxSurge: 1&lt;/code> y &lt;code>terminationGracePeriodSeconds: 120+&lt;/code>.&lt;/li>
&lt;li>&lt;strong>readiness probe corta&lt;/strong> que mata pods cargando: &lt;code>startupProbe&lt;/code> con &lt;code>failureThreshold: 60&lt;/code>.&lt;/li>
&lt;li>&lt;strong>HPA por CPU%&lt;/strong> sin métricas LLM: vLLM bachea internamente, una réplica atiende decenas. KEDA por queue depth.&lt;/li>
&lt;li>&lt;strong>KV cache sin cuantizar&lt;/strong>: &lt;code>--kv-cache-dtype=fp8&lt;/code> casi siempre rentable.&lt;/li>
&lt;li>&lt;strong>Tensor parallel en GPUs sin NVLink&lt;/strong>: all-reduce satura PCIe, throughput se hunde.&lt;/li>
&lt;/ul>
&lt;h2 id="etapa-5--observe-ver-lo-que-pasa-en-producción">Etapa 5 — Observe: ver lo que pasa en producción&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#c47aff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn5)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn5)}&lt;/style>
&lt;defs>&lt;marker id="mn5" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: OBSERVE · tracing + métricas + guardrails + drift + eBPF&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box active"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h3 id="sub-tareas-operativas-4">Sub-tareas operativas&lt;/h3>
&lt;p>Esta es la etapa que más profundamente hemos cubierto en series previas: toda la serie eBPF (4 posts) y la serie post-tracing (4 posts) tratan sub-tareas de Observe. Resumen estructurado:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tracing&lt;/strong>: OpenLLMetry/Traceloop, Langfuse, Phoenix, LangSmith. Spans con OTel GenAI semantic conventions (&lt;code>gen_ai.*&lt;/code>, &lt;code>mcp.*&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Métricas&lt;/strong>: Prometheus + Grafana. TTFT, TPOT, throughput, queue depth, KV cache usage, cost por tool.&lt;/li>
&lt;li>&lt;strong>Guardrails activos&lt;/strong> (no solo eval): NeMo Guardrails con rails de 5 tipos, Llama Guard 4 multimodal, Llama Prompt Guard 2 (86M/22M), LLM Guard.&lt;/li>
&lt;li>&lt;strong>eBPF observability&lt;/strong> (zero-instrumentation): Hubble (red), Tetragon (proceso/syscall), AgentSight (agente LLM con SSL uprobes + stdiocap MCP).&lt;/li>
&lt;li>&lt;strong>eBPF en motor local&lt;/strong> (inferencia): ProfInfer-style con uprobes en llama.cpp / vLLM / libcudart.&lt;/li>
&lt;li>&lt;strong>Drift detection&lt;/strong>: Evidently AI, NannyML, WhyLabs. KS, PSI, MMD sobre embeddings.&lt;/li>
&lt;li>&lt;strong>MCP observability&lt;/strong>: OpenTelemetry GenAI MCP semantic conventions, trace propagation via &lt;code>params._meta&lt;/code>, MCP Gateway centralizado.&lt;/li>
&lt;/ul>
&lt;h3 id="las-cuatro-métricas-obligatorias">Las cuatro métricas obligatorias&lt;/h3>
&lt;p>De todo lo cubierto, las cuatro que cualquier dashboard mínimo debe tener:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>TTFT p50/p95&lt;/strong> (time to first token) — lo que el usuario percibe.&lt;/li>
&lt;li>&lt;strong>TPOT p50/p95&lt;/strong> (time per output token) — velocidad de streaming.&lt;/li>
&lt;li>&lt;strong>Throughput&lt;/strong> (tokens/segundo agregados) — capacity planning.&lt;/li>
&lt;li>&lt;strong>Queue depth&lt;/strong> (&lt;code>vllm:num_requests_waiting&lt;/code>) — indicador adelantado.&lt;/li>
&lt;/ol>
&lt;p>A esto se suman, por dominio:&lt;/p>
&lt;ul>
&lt;li>Para RAG: faithfulness rolling mean, retrieval hit rate.&lt;/li>
&lt;li>Para agentes: tool call accuracy, multi-step task completion.&lt;/li>
&lt;li>Para multi-tenant: cost per tenant, p95 latency per tenant.&lt;/li>
&lt;/ul>
&lt;h3 id="trampas-4">Trampas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Cardinalidad en Prometheus&lt;/strong>: las métricas con todos los labels K8s explotan.&lt;/li>
&lt;li>&lt;strong>Tracing sin sampling&lt;/strong>: el storage crece sin control.&lt;/li>
&lt;li>&lt;strong>Guardrails permanentemente en monitoring mode&lt;/strong>: nunca llegan a enforce.&lt;/li>
&lt;li>&lt;strong>Drift sin alertas&lt;/strong>: detectas drift en el dashboard una vez al mes; mientras tanto el problema lleva semanas.&lt;/li>
&lt;li>&lt;strong>OTel sin propagación&lt;/strong>: spans MCP, Tetragon, AgentSight desconectados.&lt;/li>
&lt;/ul>
&lt;h2 id="etapa-6--retrain-cerrar-el-bucle">Etapa 6 — Retrain: cerrar el bucle&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Retrain">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ffd24a;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mn6)}.cyc{stroke:#c66;stroke-width:2;fill:none;stroke-dasharray:4 2;marker-end:url(#mn6)}&lt;/style>
&lt;defs>&lt;marker id="mn6" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c66"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: RETRAIN · cerrar el bucle hacia DATA&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box active"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h3 id="sub-tareas-operativas-5">Sub-tareas operativas&lt;/h3>
&lt;p>Esta es la etapa que más se descuida en proyectos GenAI. Cerrar el bucle convierte LLMOps en una práctica viva; no cerrarlo lo deja como un proyecto que envejece.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Feedback explícito&lt;/strong>: thumbs up/down en la UI, anotaciones por usuarios power, formularios para &amp;ldquo;qué falló&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Feedback implícito&lt;/strong>: latencia anómala, abandonment rate, retries del usuario, sesiones abortadas.&lt;/li>
&lt;li>&lt;strong>Triaging de incidentes&lt;/strong>: clasificar incidentes por causa raíz (model issue, retrieval issue, prompt issue, infra issue).&lt;/li>
&lt;li>&lt;strong>Dataset enrichment&lt;/strong>: incorporar al golden dataset los casos donde el sistema falló, con la respuesta correcta etiquetada por humano.&lt;/li>
&lt;li>&lt;strong>Cadence de retrain&lt;/strong>: trimestral por defecto, &lt;strong>incident-driven&lt;/strong> cuando un patrón problemático supera threshold.&lt;/li>
&lt;li>&lt;strong>Promotion&lt;/strong>: el nuevo modelo/adapter pasa por las etapas Tune → Eval → Deploy, con eval gates que comparan contra el modelo en producción.&lt;/li>
&lt;/ul>
&lt;h3 id="las-dos-cadencias">Las dos cadencias&lt;/h3>
&lt;p>&lt;strong>Scheduled retrain&lt;/strong> (trimestral o semestral): un proceso establecido. Permite planificar capacity, presupuesto, riesgo. El default.&lt;/p>
&lt;p>&lt;strong>Incident-driven retrain&lt;/strong>: cuando un incidente serio (drift detectado, segmento que falla, ataque de prompt injection) supera threshold, se dispara un mini-ciclo. Más caro pero necesario para casos críticos.&lt;/p>
&lt;h3 id="herramientas-dominantes-2">Herramientas dominantes&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Annotation y feedback collection&lt;/strong>: Langfuse (UI built-in), Argilla (OSS), Label Studio.&lt;/li>
&lt;li>&lt;strong>Dataset enrichment&lt;/strong>: pipelines en Airflow o Argo Workflows.&lt;/li>
&lt;li>&lt;strong>Triaging&lt;/strong>: dashboards Langfuse + filtros por traces con eval bajo.&lt;/li>
&lt;li>&lt;strong>Promoting candidate&lt;/strong>: MLflow model registry stages.&lt;/li>
&lt;/ul>
&lt;h3 id="trampas-5">Trampas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Bucle abierto&lt;/strong>: producción no informa al dataset; el modelo nunca mejora.&lt;/li>
&lt;li>&lt;strong>Feedback humano se pierde&lt;/strong>: thumbs down sin canal de captura estructurado.&lt;/li>
&lt;li>&lt;strong>Cadence sin definir&lt;/strong>: &amp;ldquo;ya retrenamos cuando haga falta&amp;rdquo; → nunca se retrena.&lt;/li>
&lt;li>&lt;strong>Sin holdout test set&lt;/strong>: el golden dataset se enriquece con los mismos casos que se usan para evaluar; eval mide memorización.&lt;/li>
&lt;li>&lt;strong>Promotion sin gates&lt;/strong>: el nuevo modelo entra a producción sin pasar las verificaciones de los modelos anteriores.&lt;/li>
&lt;/ul>
&lt;h2 id="el-ciclo-completo-cómo-encajan-las-etapas">El ciclo completo: cómo encajan las etapas&lt;/h2>
&lt;p>Ahora que vimos cada etapa por separado, el insight clave es &lt;strong>cómo se enganchan&lt;/strong>. Cinco propiedades emergentes del ciclo:&lt;/p>
&lt;p>&lt;strong>1. Data es la materia prima de todas las etapas&lt;/strong>. Tune lee del golden dataset. Eval lee del eval dataset. Deploy lee del RAG (vector store). Observe produce nuevos datos. Retrain crea datasets nuevos. &lt;strong>El log Kafka es el evangelio del sistema entero&lt;/strong> (post 2 de la serie).&lt;/p>
&lt;p>&lt;strong>2. Eval es el gatekeeper bidireccional&lt;/strong>. Antes de Deploy: bloquea release si el modelo regresa. Después de Observe: alimenta Retrain identificando casos peor evaluados. La calidad del eval determina la calidad del ciclo entero.&lt;/p>
&lt;p>&lt;strong>3. Observe alimenta a Retrain y a Eval simultáneamente&lt;/strong>. Las traces producen métricas para Observe; las traces problemáticas se anotan y van al dataset; los nuevos casos enriquecen el eval golden. &lt;strong>Observe es la fuente de verdad operativa&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>4. Los componentes transversales (banda gris del mapa) no son una etapa, son una infraestructura&lt;/strong>. OpenTelemetry, prompt versioning, MCP gateway, model gateway, schema registry. Mal configurados, cada etapa sufre por separado. Bien configurados, las etapas se integran sin fricción.&lt;/p>
&lt;p>&lt;strong>5. El ciclo no es secuencial estricto, es concurrente&lt;/strong>. En cualquier momento dado, el sistema tiene: requests siendo servidas (Deploy + Observe), una versión nueva en training (Tune), eval continuo en CI (Eval), datos llegando del CDC (Data), análisis de incidentes (Retrain). &lt;strong>Todas las etapas están vivas a la vez&lt;/strong>.&lt;/p>
&lt;h2 id="trampas-cross-etapa-cosas-que-rompen-el-sistema-entero">Trampas cross-etapa: cosas que rompen el sistema entero&lt;/h2>
&lt;p>Hay errores que no son de una etapa, sino de las interfaces entre etapas. Los más comunes:&lt;/p>
&lt;h3 id="trainserve-skew">Train/serve skew&lt;/h3>
&lt;p>El formato exacto del prompt en training es distinto al de producción. Resultado: el modelo entrenado para responder a &lt;code>&amp;lt;|im_start|&amp;gt;user\n...\n&amp;lt;|im_end|&amp;gt;&lt;/code> recibe en producción &lt;code>User: ...\nAssistant:&lt;/code> y rinde peor. &lt;strong>Solución&lt;/strong>: extraer el chat template en una librería compartida que use el pipeline de Tune &lt;strong>y&lt;/strong> el de Deploy.&lt;/p>
&lt;h3 id="eval-que-no-refleja-producción">Eval que no refleja producción&lt;/h3>
&lt;p>Tu golden dataset son preguntas cuidadas; producción es preguntas reales con errores tipográficos, idiomas mezclados, etc. Eval pasa al 95%, producción rinde al 70%. &lt;strong>Solución&lt;/strong>: enriquecer continuamente el golden con muestras reales.&lt;/p>
&lt;h3 id="drift-sin-pipeline-de-respuesta">Drift sin pipeline de respuesta&lt;/h3>
&lt;p>Detectas drift en el dashboard de Observe; nadie tiene un workflow definido sobre qué hacer. &lt;strong>Solución&lt;/strong>: cada alerta de drift debe tener un runbook claro: investiga, clasifica, actúa (retrain, ajustar prompt, ampliar retrieval).&lt;/p>
&lt;h3 id="schema-break-cascada">Schema break cascada&lt;/h3>
&lt;p>Cambias el schema en la fuente OLTP; Debezium lo refleja; Flink job se rompe; topic embedded deja de actualizarse; vector store envejece; RAG responde sobre datos viejos. Tres etapas afectadas por un cambio en Data. &lt;strong>Solución&lt;/strong>: schema evolution &lt;strong>backward-compatible&lt;/strong> obligatoria, contracts entre productores y consumidores.&lt;/p>
&lt;h3 id="sin-observabilidad-del-propio-pipeline">Sin observabilidad del propio pipeline&lt;/h3>
&lt;p>El pipeline LLMOps es un sistema complejo. Si no tiene observabilidad propia (cuánto tarda el entrenamiento, cuántos jobs fallan, cuántas re-embedding pasan), debugar fallos es un proceso de spelunking. &lt;strong>Solución&lt;/strong>: OTel sobre el pipeline mismo, no solo sobre las llamadas LLM.&lt;/p>
&lt;h3 id="vendor-lock-in-invisible">Vendor lock-in invisible&lt;/h3>
&lt;p>Pipelines escritos contra LangChain, prompts pegados en LangSmith, embeddings en Pinecone, modelo en OpenAI. Migrar es un proyecto de meses. &lt;strong>Solución&lt;/strong>: abstracciones LiteLLM, OpenLLMetry, vendor-neutral desde el principio.&lt;/p>
&lt;h2 id="lo-que-viene-en-los-siguientes-posts">Lo que viene en los siguientes posts&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Post 4 — &lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">PostgreSQL + Qdrant en la etapa de ingestión&lt;/a>&lt;/strong> — primer post que aplica el patrón &amp;ldquo;estás aquí&amp;rdquo; sobre la etapa Data. Patrones de sincronización (outbox + CDC), arquitectura de microservicios, manifests de despliegue.&lt;/li>
&lt;li>&lt;strong>Próximos posts&lt;/strong> — pendientes de decidir: el cluster como plataforma multi-tenant, Constitutional AI / alignment runtime, fine-tuning continuo en profundidad, edge LLMs.&lt;/li>
&lt;li>En cualquier post posterior de esta o futuras series, el &lt;strong>mini-mapa &amp;ldquo;estás aquí&amp;rdquo;&lt;/strong> te dirá en qué etapa del ciclo encaja el tema. Si lees un post sobre quantization, sabrás que estás en Deploy. Si lees uno sobre evaluator ensembles, sabrás que estás en Eval. Si lees uno sobre RAG sobre Iceberg, sabrás que estás en Data.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>Foundations:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/@sanjeebmeister/the-complete-mlops-llmops-roadmap-for-2026-building-production-grade-ai-systems-bdcca5ed2771">The Complete MLOps/LLMOps Roadmap for 2026 (Sanjeeb Panda)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://hyscaler.com/insights/mlops-in-2026-guide/">MLOps in 2026: Architecture, Trends &amp;amp; Strategy (Hyscaler)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Por etapa (entradas de la serie del blog):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Data&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka — arquitectura técnica&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Tune&lt;/strong>: cubierto parcialmente en &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama 2026&lt;/a>; profundización en post 4 si se elige fine-tuning continuo.&lt;/li>
&lt;li>&lt;strong>Eval&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals: la capa después del tracing&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Deploy&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a> y &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Observe&lt;/strong>: serie eBPF entera y serie post-tracing entera.&lt;/li>
&lt;li>&lt;strong>Retrain&lt;/strong>: cubierto en este post; profundización pendiente.&lt;/li>
&lt;/ul>
&lt;p>Componentes transversales:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prompt versioning&lt;/strong>: en deep dive de Langfuse dentro del &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">post de AgentSight&lt;/a>.&lt;/li>
&lt;li>&lt;strong>MCP&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability profunda&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Drift detection&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF + drift detection&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Inferencia local&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention deep dive&lt;/a>, &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Frameworks y herramientas referenciadas:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://mlflow.org/">MLflow&lt;/a>, &lt;a href="https://wandb.ai/">W&amp;amp;B&lt;/a>, &lt;a href="https://www.kubeflow.org/">Kubeflow&lt;/a>, &lt;a href="https://www.zenml.io/">ZenML&lt;/a>, &lt;a href="https://www.bentoml.com/">BentoML&lt;/a>, &lt;a href="https://metaflow.org/">Metaflow&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/huggingface/peft">HuggingFace PEFT&lt;/a>, &lt;a href="https://github.com/huggingface/trl">TRL&lt;/a>, &lt;a href="https://github.com/axolotl-ai-cloud/axolotl">Axolotl&lt;/a>, &lt;a href="https://github.com/unslothai/unsloth">Unsloth&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dvc.org/">DVC&lt;/a> + &lt;a href="https://lakefs.io/">lakeFS&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/">Langfuse&lt;/a>, &lt;a href="https://www.evidentlyai.com/">Evidently AI&lt;/a>, &lt;a href="https://phoenix.arize.com/">Phoenix&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack&lt;/a>, &lt;a href="https://kserve.github.io/website/">KServe&lt;/a>, &lt;a href="https://github.com/ome-projects/ome">OME&lt;/a>, &lt;a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo&lt;/a>, &lt;a href="https://github.com/llm-d/llm-d">llm-d&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>RAG sobre Kafka: arquitectura técnica de referencia para datalakes en streaming, con embeddings frescos y vector stores siempre al día</title><link>https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/</link><pubDate>Thu, 21 May 2026 06:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La pieza que más bloquea proyectos GenAI empresariales en 2026 no es el modelo, ni siquiera los guardrails: es la &lt;strong>ingestión de datos para RAG&lt;/strong>. Las empresas tienen información valiosa en bases de datos OLTP, en logs operacionales, en sistemas SaaS, y todo eso está silenciosamente cambiando cada segundo. Los RAG batch que se reindexan cada noche llegan tarde —la respuesta del modelo está respaldada en un snapshot de hace 18 horas— y dan paso a alucinaciones operacionales aunque el retriever sea perfecto. La respuesta dominante en producción en 2026 es montar la &lt;strong>pieza RAG sobre Kafka como source-of-truth&lt;/strong>: log inmutable, throughput masivo, schema evolution gestionada, y un ecosistema de stream processing maduro (Flink, Kafka Streams, RisingWave) que permite &lt;strong>transformar y embedder eventos a medida que ocurren&lt;/strong>, llevándolos en milisegundos a vector stores (Milvus, Qdrant, Weaviate, pgvector). El patrón canónico: &lt;strong>origen → CDC con Debezium → topics Kafka → Flink SQL con embedding UDF → sink connector a vector store → serving con vLLM o equivalente&lt;/strong>. Las novedades 2026 que cambian el juego: &lt;strong>Confluent Tableflow&lt;/strong> convierte topics Kafka en tablas Iceberg/Delta automáticamente (lectura desde Snowflake/Databricks/Trino sin ETL, 30-50% menos TCO); &lt;strong>Flink SQL nativo&lt;/strong> trae &lt;code>openai_embedding()&lt;/code> y vector search integrado con Cosmos DB y Amazon S3 Vectors; el &lt;strong>MCP server oficial de Confluent&lt;/strong> permite a agentes IA consultar Kafka/Flink/Tableflow en lenguaje natural. Este post desarrolla la arquitectura end-to-end con manifests, código Flink SQL y números concretos.&lt;/p>
&lt;blockquote>
&lt;p>Este es el &lt;strong>segundo post de la serie MLOps específico para LLMs&lt;/strong>. El primero (&lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama 2026&lt;/a>) estableció el marco. Aquí bajamos a la pieza más operacional del stack: cómo se conecta un sistema empresarial real a un agente LLM &lt;strong>manteniendo el RAG fresco&lt;/strong> sin caer en complejidad explosiva.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-analogía-kafka-como-el-single-source-of-truth">La analogía: Kafka como el &amp;ldquo;single source of truth&amp;rdquo;&lt;/h2>
&lt;p>Quien lleva tiempo en sistemas distribuidos ha visto el patrón una y otra vez: &lt;strong>un log inmutable, append-only, replicado, ordenado en el tiempo&lt;/strong> se ha vuelto la primitiva canónica para reconstruir sistemas complejos. Los DBAs lo conocen como &lt;strong>write-ahead log&lt;/strong> (PostgreSQL WAL, MySQL binlog). Los desarrolladores de sistemas de eventos lo conocen como &lt;strong>event sourcing&lt;/strong>. Los arquitectos de datos lo conocen como &lt;strong>Kappa architecture&lt;/strong>. Kafka es la implementación masiva, distribuida y madura de esa primitiva: un log que vive en disco, particionado para escalar, replicado para durabilidad, retenido por tiempo o tamaño, &lt;strong>legible desde cualquier punto histórico&lt;/strong>.&lt;/p>
&lt;p>Cuando se piensa en RAG, esto es &lt;strong>exactamente&lt;/strong> lo que se necesita. Un sistema RAG bien diseñado tiene dos preguntas críticas: ¿cómo se mantiene fresco el índice? y ¿cómo se reconstruye el índice cuando algo se rompe? Las dos las contesta Kafka de manera natural: &lt;strong>fresco&lt;/strong> porque cada cambio en el origen se publica como evento al log y el pipeline lo procesa en milisegundos; &lt;strong>reconstruible&lt;/strong> porque el log entero está ahí: borras el vector store, dispones del topic Kafka desde el offset 0 y vuelves a construir el índice tal como estaba.&lt;/p>
&lt;p>Hay además una segunda capa de analogía. Kafka, para una arquitectura GenAI moderna, juega el papel del &lt;strong>WAL del sistema entero&lt;/strong>. Igual que el WAL de Postgres es el evangelio del estado de la base de datos —si pierdes la DB pero conservas el WAL, puedes reconstruirla—, el log de Kafka es el evangelio del estado del &lt;strong>conjunto del negocio&lt;/strong>: pedidos, usuarios, transacciones, documentos. Conectar tu agente IA a Kafka es conectarlo al pulso real del sistema, no a snapshots obsoletos.&lt;/p>
&lt;h2 id="el-problema-del-rag-estático">El problema del RAG estático&lt;/h2>
&lt;p>Antes de presentar la arquitectura, vale la pena fijar &lt;strong>qué problema concreto&lt;/strong> estamos resolviendo. El antipattern que tropieza a la mayoría de proyectos GenAI:&lt;/p>
&lt;ol>
&lt;li>Equipo construye RAG sobre un dataset estático: vuelca documentos de Confluence, PDFs de productos, snapshots de base de datos.&lt;/li>
&lt;li>Lo embedea con un cron nocturno que regenera el índice cada 24 horas.&lt;/li>
&lt;li>Lanza el producto.&lt;/li>
&lt;li>&lt;strong>Día 2&lt;/strong>: usuario pregunta sobre un cambio que ocurrió hace dos horas. El RAG no lo tiene; el modelo responde sobre la versión vieja.&lt;/li>
&lt;li>Equipo añade lógica frágil: &amp;ldquo;si la query menciona una fecha reciente, escalar a un agente humano&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Día 30&lt;/strong>: el dataset se ha movido tanto que media RAG está desactualizado. El equipo decide refactor y migrar a streaming.&lt;/li>
&lt;/ol>
&lt;p>Es la historia repetida de tantos proyectos que el ecosistema ha aprendido la lección: &lt;strong>streaming desde el día 1&lt;/strong>, aunque el volumen sea bajo. La complejidad operacional de un pipeline streaming bien diseñado es &lt;strong>constante&lt;/strong>; la complejidad de migrar de batch a streaming en proyecto vivo es &lt;strong>enorme&lt;/strong>.&lt;/p>
&lt;h2 id="del-lambda-al-kappa-al-streaming-rag">Del Lambda al Kappa al Streaming RAG&lt;/h2>
&lt;p>Tres arquitecturas en orden histórico:&lt;/p>
&lt;p>&lt;strong>Lambda (clásica de big data 2014)&lt;/strong>: dos pipelines paralelos, uno batch para precisión y uno streaming para freshness. La consulta combina ambos. Funciona pero exige mantener dos pipelines.&lt;/p>
&lt;p>&lt;strong>Kappa (Jay Kreps 2014, mainstream desde 2020)&lt;/strong>: solo un pipeline streaming. El batch es un caso particular del streaming (reprocesar desde el principio). Simplifica mucho.&lt;/p>
&lt;p>&lt;strong>Streaming RAG (emergente 2025-2026)&lt;/strong>: variante específica de Kappa donde el output del pipeline son &lt;strong>embeddings indexados en un vector store&lt;/strong> que el LLM consulta en runtime. El log Kafka es la &lt;strong>fuente de verdad&lt;/strong>, el vector store es un &lt;strong>proyección consultable&lt;/strong>.&lt;/p>
&lt;p>La conversión mental: piensa en el vector store como la &lt;strong>vista materializada&lt;/strong> del log Kafka. Si la vista se corrompe, la reconstruyes desde el log. Si quieres una vista nueva (otro embedding model, otro chunking strategy), creas otro consumer del log y construyes una segunda vista en paralelo.&lt;/p>
&lt;h2 id="la-arquitectura-de-referencia">La arquitectura de referencia&lt;/h2>
&lt;p>Vamos al diagrama. Voy a presentar la arquitectura canónica que se ha estabilizado en 2026, mostrando dónde encaja cada componente:&lt;/p>
&lt;pre tabindex="0">&lt;code>[OLTP DB (Postgres)] [Otros origenes]
│ │
│ WAL via logical decoding │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ Debezium / Kafka Connect (Sources) │
└──────────────────────────────────────────────────────────┘
│
▼ produce eventos
┌──────────────────────────────────────────────────────────┐
│ Kafka cluster │
│ ┌───────────────────────────────────────────────────┐ │
│ │ topic: orders.raw (3 particiones, RF=3) │ │
│ │ topic: users.raw (3 particiones, RF=3) │ │
│ │ topic: documents.raw (6 particiones, RF=3) │ │
│ └───────────────────────────────────────────────────┘ │
│ + Schema Registry (Avro/Protobuf) │
└──────────────────────────────────────────────────────────┘
│
▼ consume y transforma
┌──────────────────────────────────────────────────────────┐
│ Flink SQL streaming jobs │
│ - chunking text │
│ - llamadas a embedding model (UDF) │
│ - enriquecimiento con metadata │
│ - sink a topic curado: documents.embedded │
└──────────────────────────────────────────────────────────┘
│
┌───────────┼────────────────────┐
▼ ▼ ▼
[Vector store] [Tableflow] [Iceberg/Delta]
Milvus/Qdrant auto-convert para analytics
/pgvector/ topics →
Weaviate tables
│
▼ consultado en runtime
┌──────────────────────────────────────────────────────────┐
│ LLM serving (vLLM / SGLang) + Retriever │
│ - recibe query del agente │
│ - busca top-K en vector store │
│ - construye prompt + contexto │
│ - genera respuesta con citas │
└──────────────────────────────────────────────────────────┘
&lt;/code>&lt;/pre>&lt;p>Las &lt;strong>cinco capas&lt;/strong> que ves —&lt;strong>fuente, ingestión (CDC), transporte (Kafka), procesamiento (Flink), almacenamiento (vector + tablas)&lt;/strong>— son las que estructuran cualquier RAG sobre datalake serio en 2026. Vamos a cada una.&lt;/p>
&lt;h2 id="capa-1--fuentes-tu-oltp-como-punto-de-partida">Capa 1 — Fuentes: tu OLTP como punto de partida&lt;/h2>
&lt;p>La fuente típica es una &lt;strong>base de datos OLTP&lt;/strong> (Postgres, MySQL, SQL Server). Es donde vive el estado vivo del negocio. La técnica para extraer cambios en tiempo real es &lt;strong>Change Data Capture (CDC)&lt;/strong>: leer el log de transacciones de la base de datos (PostgreSQL WAL, MySQL binlog) y convertir cada commit en un evento Kafka.&lt;/p>
&lt;p>El estándar OSS es &lt;strong>&lt;a href="https://debezium.io/">Debezium&lt;/a>&lt;/strong>. Soporta Postgres, MySQL, SQL Server, MongoDB, Oracle, Cassandra y otros. Despliegue típico como cluster Kafka Connect con conectores Debezium.&lt;/p>
&lt;p>Ejemplo de configuración Debezium para PostgreSQL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres-orders-connector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector.class&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.connector.postgresql.PostgresConnector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tasks.max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.hostname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres.prod.internal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5432&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;debezium&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.password&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;${secret:postgres-creds}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.dbname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ecommerce&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.server.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ecommerce-prod&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;table.include.list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;public.orders,public.users,public.products&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;publication.autocreate.mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;filtered&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;slot.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;debezium_slot&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugin.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;pgoutput&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;topic.prefix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ecommerce&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.confluent.connect.avro.AvroConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.confluent.connect.avro.AvroConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter.schema.registry.url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://schema-registry:8081&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter.schema.registry.url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://schema-registry:8081&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto produce, por cada commit en la base de datos, un evento Avro al topic correspondiente (&lt;code>ecommerce.public.orders&lt;/code>, &lt;code>ecommerce.public.users&lt;/code>, etc.) con el cambio: tipo (INSERT/UPDATE/DELETE), valores antes y después, timestamp del commit, posición en el WAL.&lt;/p>
&lt;p>&lt;strong>Alternativa más simple para 2026&lt;/strong>: &lt;a href="https://risingwave.com/">RisingWave&lt;/a> puede leerse el WAL de Postgres &lt;strong>directamente, sin Debezium ni Kafka Connect intermedio&lt;/strong>. Cuando el caso es solo CDC sin más fuentes, es operacionalmente más simple. Para arquitecturas con múltiples fuentes (CDC + APIs + scrapers + logs), Debezium sigue siendo la pieza estándar.&lt;/p>
&lt;h2 id="capa-2--kafka-como-transporte-y-persistencia">Capa 2 — Kafka como transporte y persistencia&lt;/h2>
&lt;p>El cluster Kafka es donde aterrizan todos los eventos. Decisiones operativas clave:&lt;/p>
&lt;h3 id="topics-raw-vs-curated">Topics: raw vs curated&lt;/h3>
&lt;p>Convención que se ha establecido en 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>*.raw&lt;/code>&lt;/strong>: el evento crudo tal como llegó. CDC sin transformar, log de aplicación sin parsear.&lt;/li>
&lt;li>&lt;strong>&lt;code>*.cleaned&lt;/code>&lt;/strong>: tras dedup, validación de schema, normalización de tipos.&lt;/li>
&lt;li>&lt;strong>&lt;code>*.enriched&lt;/code>&lt;/strong>: tras añadir metadatos (geolocalización, identificadores cruzados, etc.).&lt;/li>
&lt;li>&lt;strong>&lt;code>*.embedded&lt;/code>&lt;/strong>: el evento con su vector embedding ya calculado.&lt;/li>
&lt;/ul>
&lt;p>Multi-stage topics permite &lt;strong>debug por capa&lt;/strong> y &lt;strong>reprocesamiento parcial&lt;/strong>: si cambias el embedding model, descartar &lt;code>*.embedded&lt;/code> y reconstruir desde &lt;code>*.enriched&lt;/code> cuesta horas; reconstruir desde &lt;code>*.raw&lt;/code> cuesta días.&lt;/p>
&lt;h3 id="schema-registry">Schema Registry&lt;/h3>
&lt;p>Sin &lt;strong>schema registry&lt;/strong>, los topics se rompen silenciosamente cuando alguien cambia el schema en origen. &lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html">&lt;strong>Confluent Schema Registry&lt;/strong>&lt;/a> o el OSS &lt;a href="https://www.apicur.io/registry/">Apicurio&lt;/a> son las opciones dominantes.&lt;/p>
&lt;p>Formatos comunes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Avro&lt;/strong>: schema versionado, evolution rules estrictas. El default histórico.&lt;/li>
&lt;li>&lt;strong>Protobuf&lt;/strong>: compatible con stacks gRPC, buena performance.&lt;/li>
&lt;li>&lt;strong>JSON Schema&lt;/strong>: textual, debuggable a ojo, menos eficiente.&lt;/li>
&lt;/ul>
&lt;p>Para RAG sobre Kafka recomendamos &lt;strong>Avro&lt;/strong> por defecto. Schema evolution es importante porque las tablas origen cambian con el tiempo, y un esquema sin versión rompe consumidores aguas abajo.&lt;/p>
&lt;h3 id="particiones-replicación-y-retención">Particiones, replicación y retención&lt;/h3>
&lt;p>Decisiones operativas para topics de RAG:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Particiones&lt;/strong>: típicamente 3-12. Más particiones = más paralelismo en consumer Flink, pero más overhead. La regla del pulgar: &lt;strong>particiones = pico esperado de eventos/s ÷ 1000&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Replication factor&lt;/strong>: 3 mínimo en producción. La replicación protege contra fallo de broker; con RAG el coste de perder un topic puede ser semanas de re-embedding.&lt;/li>
&lt;li>&lt;strong>Retención&lt;/strong>: para topics que alimentan RAG, &lt;strong>retención larga&lt;/strong> o &lt;strong>compactada por key&lt;/strong>. Si el documento &lt;code>doc-42&lt;/code> cambia 100 veces, compactación solo guarda el último estado por key, dejando un log más pequeño y reconstruible. Para datos que no se actualizan (logs históricos), retención por tiempo (90 días, 1 año).&lt;/li>
&lt;/ul>
&lt;h3 id="replicación-cross-cluster">Replicación cross-cluster&lt;/h3>
&lt;p>Para deployments multi-región o multi-cloud, &lt;strong>MirrorMaker 2&lt;/strong> o &lt;strong>&lt;a href="https://docs.confluent.io/platform/current/multi-dc-deployments/cluster-linking/index.html">Cluster Linking&lt;/a>&lt;/strong> (Confluent) replican topics entre clusters Kafka. El RAG puede consultar el cluster local sin tener que cruzar región.&lt;/p>
&lt;h2 id="capa-3--flink-como-procesador-streaming">Capa 3 — Flink como procesador streaming&lt;/h2>
&lt;p>&lt;a href="https://flink.apache.org/">Apache Flink&lt;/a> es la pieza dominante de stream processing en 2026. Apache 2.0, distribución mature, ecosistema amplio. La alternativa principal es Kafka Streams (más simple, Java-only); RisingWave es la opción emergente para casos SQL puros.&lt;/p>
&lt;p>Lo que Flink añade a Kafka:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Stateful streaming&lt;/strong>: agregaciones temporales, joins entre streams, sesiones.&lt;/li>
&lt;li>&lt;strong>Exactly-once semantics&lt;/strong>: con checkpoint coordination.&lt;/li>
&lt;li>&lt;strong>Watermarks&lt;/strong>: handling correcto de eventos out-of-order.&lt;/li>
&lt;li>&lt;strong>UDFs en Python/Java&lt;/strong>: incluyendo llamadas a modelos LLM.&lt;/li>
&lt;/ul>
&lt;h3 id="flink-sql-la-pieza-más-operacional">Flink SQL: la pieza más operacional&lt;/h3>
&lt;p>Flink SQL es la pieza más usable de Flink para data engineers que no son streaming experts. Veamos un ejemplo realista de pipeline RAG:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- 1. Definir la fuente: topic Kafka con eventos CDC de documentos
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_raw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMP_LTZ&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ENFORCED&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;connector&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;upsert-kafka&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;topic&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ecommerce.public.documents&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;properties.bootstrap.servers&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;kafka:9092&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;key.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;avro-confluent&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;value.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;avro-confluent&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;value.fields-include&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;EXCEPT_KEY&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 2. Definir el sink: vector store via Kafka topic intermedio
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_embedded&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">ARRAY&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">FLOAT&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedded_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMP_LTZ&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ENFORCED&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;connector&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;upsert-kafka&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;topic&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;rag.documents.embedded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;properties.bootstrap.servers&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;kafka:9092&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;key.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;json&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;value.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;json&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 3. UDF para chunking (definida en Python o Java)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- CREATE TEMPORARY FUNCTION chunk_text AS &amp;#39;com.example.ChunkingUDF&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 4. Pipeline: chunkear, embedder, escribir al sink
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_embedded&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_idx&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">OPENAI_EMBEDDING&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunk&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;text-embedding-3-small&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">CURRENT_TIMESTAMP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedded_at&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_raw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CROSS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UNNEST&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">512&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">64&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ORDINALITY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunk&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_idx&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lo que pasa aquí, línea a línea:&lt;/p>
&lt;ul>
&lt;li>La tabla &lt;code>documents_raw&lt;/code> lee el topic CDC en modo &lt;strong>upsert-kafka&lt;/strong> (cada nuevo evento por la misma key reemplaza el anterior). Esto refleja correctamente la semántica &amp;ldquo;esta es la última versión del doc 42&amp;rdquo;.&lt;/li>
&lt;li>La tabla &lt;code>documents_embedded&lt;/code> será el topic intermedio donde Flink escribe los chunks embedded.&lt;/li>
&lt;li>La UDF &lt;code>chunk_text&lt;/code> (definida en Python o Java) divide cada doc en chunks de 512 tokens con overlap de 64.&lt;/li>
&lt;li>La consulta &lt;code>INSERT INTO&lt;/code> se ejecuta continuamente: cada evento nuevo en &lt;code>documents_raw&lt;/code> se chunkea, cada chunk se embedea con &lt;code>OPENAI_EMBEDDING&lt;/code> (función built-in de Flink SQL en Confluent Cloud 2026), y se escribe al topic embedded.&lt;/li>
&lt;/ul>
&lt;p>&lt;code>OPENAI_EMBEDDING&lt;/code> puede sustituirse por una función custom que llame a un modelo self-hosted (vLLM con un encoder), a SentenceTransformers, o a un servicio managed. La sintaxis es la misma; cambias el provider.&lt;/p>
&lt;h3 id="watermarks-y-late-events">Watermarks y late events&lt;/h3>
&lt;p>Para casos donde un evento puede llegar tarde (eg el WAL de Postgres se retrasa porque hubo un network blip), Flink permite definir &lt;strong>watermarks&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_raw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMP_LTZ&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">WATERMARK&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTERVAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;5&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">MINUTE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(...)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto le dice a Flink &amp;ldquo;asume que ningún evento llega más de 5 minutos tarde respecto al timestamp del evento&amp;rdquo;. Para joins y agregaciones temporales, Flink usa el watermark para decidir cuándo &amp;ldquo;cerrar&amp;rdquo; una ventana.&lt;/p>
&lt;h2 id="capa-4--sinks-a-vector-stores">Capa 4 — Sinks a vector stores&lt;/h2>
&lt;p>El último paso es indexar los embeddings en un vector store. Tres patrones en 2026:&lt;/p>
&lt;h3 id="patrón-a--kafka-connect-sink-directo">Patrón A — Kafka Connect sink directo&lt;/h3>
&lt;p>Cada vector store tiene su connector oficial:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://github.com/zilliztech/kafka-connect-milvus">Milvus&lt;/a>&lt;/strong>: sink connector oficial de Zilliz. Soporta named/unnamed dense/sparse vectors.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/qdrant/qdrant-kafka">Qdrant&lt;/a>&lt;/strong>: sink connector oficial. Soporta dense, sparse, multi-vector.&lt;/li>
&lt;li>&lt;strong>pgvector&lt;/strong>: no tiene connector dedicado, pero se usa el &lt;a href="https://www.confluent.io/hub/confluentinc/kafka-connect-jdbc">JDBC Sink Connector&lt;/a> con SQL custom.&lt;/li>
&lt;li>&lt;strong>Weaviate&lt;/strong>: connector community.&lt;/li>
&lt;li>&lt;strong>LanceDB&lt;/strong>: connector community.&lt;/li>
&lt;/ul>
&lt;p>Ejemplo de configuración Milvus sink:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;milvus-rag-embeddings-sink&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector.class&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;com.milvus.io.kafka.MilvusSinkConnector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tasks.max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;topics&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rag.documents.embedded&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.host&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;milvus.prod.internal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;19530&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.collection.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.collection.dim&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1536&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.collection.partition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.storage.StringConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.json.JsonConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter.schemas.enable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tres tasks en paralelo (&lt;code>tasks.max: 3&lt;/code>) consumen el topic embedded y escriben a la colección Milvus. La latencia desde &amp;ldquo;evento en Kafka&amp;rdquo; hasta &amp;ldquo;vector indexable en Milvus&amp;rdquo; es típicamente &lt;strong>&amp;lt;5 segundos&lt;/strong>.&lt;/p>
&lt;h3 id="patrón-b--pgvector-con-cdc-pipe-directo">Patrón B — pgvector con CDC pipe directo&lt;/h3>
&lt;p>Para equipos que ya viven en PostgreSQL, &lt;strong>pgvector&lt;/strong> es la opción de menor fricción. Patrón: el mismo cluster Postgres origen tiene una segunda DB para embeddings con extensión pgvector activada; el pipeline Flink escribe directamente vía JDBC.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- En el cluster Postgres con pgvector activado
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXISTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">document_embeddings&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1536&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedded_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TIMESTAMP&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">document_embeddings&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hnsw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector_cosine_ops&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ef_construction&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">64&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ventajas: tu mismo DBA opera todo, transactionality cross-tables, joins con metadatos relacionales triviales. Limitación: a &amp;gt;10M vectores, el rendimiento de pgvector empieza a ceder respecto a sistemas dedicados.&lt;/p>
&lt;h3 id="patrón-c--confluent-tableflow--iceberg--vector-search-flink-sql">Patrón C — Confluent Tableflow → Iceberg + vector search Flink SQL&lt;/h3>
&lt;p>Esta es la novedad 2026 que cambia la mecánica. &lt;a href="https://www.confluent.io/product/tableflow/">Confluent Tableflow&lt;/a> materializa &lt;strong>automáticamente&lt;/strong> topics Kafka como tablas &lt;strong>Apache Iceberg&lt;/strong> o &lt;strong>Delta Lake&lt;/strong>. Características:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Sin pipeline ETL&lt;/strong>: no escribes Flink/Spark jobs para mover Kafka a tabla. Lo hace Tableflow.&lt;/li>
&lt;li>&lt;strong>Schema evolution automática&lt;/strong>: cambios en el schema del topic se reflejan en la tabla.&lt;/li>
&lt;li>&lt;strong>Catálogo unificado&lt;/strong>: la tabla aparece en Glue, Unity Catalog, Snowflake, Databricks. Cualquier motor analítico la consulta sin copiar datos.&lt;/li>
&lt;li>&lt;strong>CDC nativo&lt;/strong>: maneja inserts, updates, deletes correctamente.&lt;/li>
&lt;li>&lt;strong>30-50% menos TCO&lt;/strong> según las cifras que Confluent publica vs pipelines tradicionales.&lt;/li>
&lt;/ul>
&lt;p>Y desde 2026, Tableflow + Flink SQL ofrecen &lt;strong>vector search nativo integrado con Cosmos DB y Amazon S3 Vectors&lt;/strong>. La consulta RAG se puede hacer directamente en Flink SQL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_embedded&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VECTOR_SEARCH&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">OPENAI_EMBEDDING&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;query del usuario&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;text-embedding-3-small&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">top_k&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VECTOR_SEARCH_SCORE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto unifica capas que antes eran separadas (vector store + analytics). Para muchos casos, &lt;strong>elimina&lt;/strong> la necesidad de mantener un vector store dedicado.&lt;/p>
&lt;h2 id="el-mcp-server-oficial-de-confluent">El MCP server oficial de Confluent&lt;/h2>
&lt;p>Una pieza añadida en 2026 que merece mención: Confluent ha publicado &lt;strong>un MCP server oficial&lt;/strong> que expone Kafka, Flink y Tableflow como tools accesibles a agentes IA vía MCP. Cualquier MCP client (Claude Desktop, Cursor, agentes propios) puede:&lt;/p>
&lt;ul>
&lt;li>Listar topics, leer mensajes recientes, publicar a topics.&lt;/li>
&lt;li>Ejecutar queries Flink SQL en lenguaje natural (&amp;ldquo;dame las órdenes de las últimas 24 horas con valor &amp;gt; 1000€&amp;rdquo;).&lt;/li>
&lt;li>Consultar tablas Tableflow Iceberg.&lt;/li>
&lt;li>Gestionar conectores Kafka Connect.&lt;/li>
&lt;/ul>
&lt;p>Esto cierra el círculo: tu agente IA, además de &lt;strong>leer datos&lt;/strong> del datalake vía RAG (con vector search), puede &lt;strong>escribir datos&lt;/strong> al log (vía MCP) y disparar transformaciones (vía Flink SQL en natural language). Es el punto de fusión más profundo entre LLM ops y data ops del año.&lt;/p>
&lt;p>Conexión con la serie anterior: este MCP server emite traces con las OpenTelemetry GenAI MCP semantic conventions que cubrimos en el &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">post de MCP observability&lt;/a>. Los spans aparecen en Langfuse, Phoenix o tu OTel backend con la cardinalidad correcta. Cero código de instrumentación.&lt;/p>
&lt;h2 id="vector-stores-comparativa-2026">Vector stores: comparativa 2026&lt;/h2>
&lt;p>Las cinco opciones dominantes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Vector store&lt;/th>
&lt;th>Licencia&lt;/th>
&lt;th>Operación&lt;/th>
&lt;th>Cuándo encaja&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>pgvector&lt;/strong>&lt;/td>
&lt;td>Postgres ext, OSS&lt;/td>
&lt;td>Tu DBA&lt;/td>
&lt;td>&amp;lt;10M vectores, equipo Postgres-heavy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Qdrant&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Self-host o managed&lt;/td>
&lt;td>Mid-scale, foco performance&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Milvus&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Self-host o Zilliz Cloud&lt;/td>
&lt;td>Large-scale, foco escalabilidad&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Weaviate&lt;/strong>&lt;/td>
&lt;td>BSD-3&lt;/td>
&lt;td>Self-host o managed&lt;/td>
&lt;td>Hybrid search nativo, semantic rich&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LanceDB&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Embedded o serverless&lt;/td>
&lt;td>Small-medium, simplicidad&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La selección depende de:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Escala&lt;/strong>: pgvector se queda corto &amp;gt;10M vectores. Milvus y Qdrant escalan a billones.&lt;/li>
&lt;li>&lt;strong>Hybrid search&lt;/strong>: Weaviate trae lexical + vector nativo. Otros lo soportan pero menos integrado.&lt;/li>
&lt;li>&lt;strong>Operación&lt;/strong>: pgvector si ya tienes Postgres operado. Qdrant si quieres simplicidad. Milvus si necesitas máxima escala.&lt;/li>
&lt;li>&lt;strong>Cloud managed&lt;/strong>: Zilliz Cloud para Milvus, Qdrant Cloud para Qdrant, Pinecone si quieres SaaS puro (sin OSS detrás).&lt;/li>
&lt;/ul>
&lt;h2 id="freshness-vs-accuracy-el-trade-off-operativo">Freshness vs accuracy: el trade-off operativo&lt;/h2>
&lt;p>Una decisión crítica que cualquier sistema RAG sobre Kafka debe responder: &lt;strong>¿cuándo se considera que un nuevo documento está &amp;ldquo;live&amp;rdquo; en el índice?&lt;/strong>&lt;/p>
&lt;p>Tres opciones:&lt;/p>
&lt;p>&lt;strong>Streaming síncrono&lt;/strong>: el evento llega a Kafka, Flink lo embedea, el sink lo escribe al vector store, y solo entonces se considera live. &lt;strong>Latencia típica: 1-5 segundos&lt;/strong>. La mejor freshness. Pero si el embedding model falla o el vector store es lento, los eventos se acumulan en el topic.&lt;/p>
&lt;p>&lt;strong>Streaming asíncrono con baseline&lt;/strong>: el evento se considera live inmediatamente; un proceso de fondo lo embedea cuando puede. Mientras tanto, queries que pidan ese documento no lo encuentran. &lt;strong>Latencia típica: 5-60 segundos&lt;/strong>. Aceptable para la mayoría de aplicaciones.&lt;/p>
&lt;p>&lt;strong>Batch micro&lt;/strong>: se procesa en mini-batches cada 1-5 minutos. Menos eficiente que streaming continuo pero más estable bajo carga variable. &lt;strong>Latencia: 1-5 minutos&lt;/strong>.&lt;/p>
&lt;p>La decisión depende del SLA del producto. Para chatbots de soporte al cliente, 5-60 segundos es aceptable. Para sistemas que reaccionan a eventos críticos (precios financieros, alarmas), streaming síncrono es necesario.&lt;/p>
&lt;h2 id="schema-evolution-y-reembedding">Schema evolution y reembedding&lt;/h2>
&lt;p>Cuando el embedding model cambia (cambias de &lt;code>text-embedding-3-small&lt;/code> a &lt;code>text-embedding-3-large&lt;/code>, o pasas de OpenAI a Cohere), los vectores existentes en el índice son &lt;strong>incompatibles&lt;/strong>: dimensiones distintas, espacios semánticos distintos. La distancia entre un vector viejo y uno nuevo no significa nada.&lt;/p>
&lt;p>Patrón estándar para handle de esto: &lt;strong>dual-index&lt;/strong> durante la migración.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>T0&lt;/strong>: índice activo es V1 (embedding model A).&lt;/li>
&lt;li>&lt;strong>T1&lt;/strong>: empieza pipeline paralelo que escribe a un índice V2 (embedding model B), consumiendo el topic desde offset 0 (reprocesar todo el log).&lt;/li>
&lt;li>&lt;strong>T2&lt;/strong>: V2 ha caught-up al presente.&lt;/li>
&lt;li>&lt;strong>T3&lt;/strong>: cambias el retriever para que use V2.&lt;/li>
&lt;li>&lt;strong>T4&lt;/strong>: una semana después, descartas V1.&lt;/li>
&lt;/ol>
&lt;p>El log de Kafka hace este patrón factible porque es &lt;strong>inmutable y reproducible&lt;/strong>. Sin el log, este patrón se vuelve un proyecto de migración de datos de semanas.&lt;/p>
&lt;h2 id="trampas-operativas">Trampas operativas&lt;/h2>
&lt;h3 id="topics-sin-retención-adecuada">Topics sin retención adecuada&lt;/h3>
&lt;p>Configurar topics con retención de 7 días pensando &amp;ldquo;ya tengo el vector store&amp;rdquo; lleva a perder la capacidad de reconstruir si el vector store falla. &lt;strong>Retención larga (90+ días) o compactada por key&lt;/strong> para topics que alimentan RAG.&lt;/p>
&lt;h3 id="cdc-pesado-en-cargas-pico">CDC pesado en cargas pico&lt;/h3>
&lt;p>Debezium leyendo el WAL en horas pico puede impactar performance de la base de datos origen. &lt;strong>Replica de lectura dedicada&lt;/strong> para Debezium, no la primaria de producción. O usar &lt;strong>logical replication&lt;/strong> específica solo para las tablas necesarias.&lt;/p>
&lt;h3 id="embedding-cost-run-away">Embedding cost run-away&lt;/h3>
&lt;p>&lt;code>OPENAI_EMBEDDING&lt;/code> en cada evento de un topic con millones de mensajes/día son &lt;strong>miles de USD/mes&lt;/strong>. Estrategias: filtrar antes de embedder (solo embedder lo que aporta valor); deduplicar por hash de contenido; usar embedding models open-source self-hosted (BGE, E5, GTE) cuando el coste cloud sea prohibitivo.&lt;/p>
&lt;h3 id="reembedding-lento-por-throughput-limitado">Reembedding lento por throughput limitado&lt;/h3>
&lt;p>Recalcular 10M embeddings con OpenAI API a 3000 req/min tarda &lt;strong>55 horas&lt;/strong>. Si esperas a un incidente para reembeder, son dos días sin servicio. &lt;strong>Embedding throughput es un capacity planning explícito&lt;/strong>; reservar capacity o tener un job offline pre-arrancable.&lt;/p>
&lt;h3 id="schema-breaks-aguas-abajo">Schema breaks aguas abajo&lt;/h3>
&lt;p>Un cambio en el schema del topic raw rompe Flink jobs aguas abajo. &lt;strong>Schema Registry con compatibility BACKWARD obligatoria&lt;/strong>; nunca ALLOW_ALL. Y test schema evolution en CI.&lt;/p>
&lt;h3 id="vector-store-sin-backup">Vector store sin backup&lt;/h3>
&lt;p>Tu vector store tiene 50M vectores. Es la única copia (los topics expiraron). Un fallo lo borra. &lt;strong>Vector stores deben ser backed up&lt;/strong> igual que cualquier persistencia primaria. Para Milvus/Qdrant: snapshots periódicos. Para pgvector: el propio pg_dump.&lt;/p>
&lt;h3 id="multi-region-sin-replicación-cross-cluster">Multi-region sin replicación cross-cluster&lt;/h3>
&lt;p>Tu RAG sirve a usuarios en US y EU. El vector store está en US-east. Latencia desde EU = 100ms+ por query. &lt;strong>MirrorMaker o Cluster Linking&lt;/strong> para replicar topics y vector stores en ambas regiones.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Hybrid search en producción&lt;/strong>: combinar BM25/lexical + vector + reranker. Tema de su propio post.&lt;/li>
&lt;li>&lt;strong>Multimodal RAG&lt;/strong>: indexar imágenes, audio, vídeo además de texto. Embeddings multimodales (CLIP, Imagebind), arquitectura específica.&lt;/li>
&lt;li>&lt;strong>GraphRAG&lt;/strong>: usar conocimiento estructurado (knowledge graphs) además de vector retrieval. Microsoft GraphRAG, LlamaIndex KnowledgeGraphQueryEngine.&lt;/li>
&lt;li>&lt;strong>RAG con ACL multi-tenant&lt;/strong>: filtrar por permisos en runtime. Patrón con metadatos en el vector store + filtros server-side.&lt;/li>
&lt;li>&lt;strong>Query rewriting con LLM&lt;/strong>: usar un primer LLM para expandir la query antes del retrieval (HyDE, multi-query, step-back prompting).&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>Kafka y stream processing:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://kafka.apache.org/">Apache Kafka&lt;/a> y &lt;a href="https://debezium.io/">Debezium&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html">Confluent Schema Registry&lt;/a> y &lt;a href="https://www.apicur.io/registry/">Apicurio Registry&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://flink.apache.org/">Apache Flink&lt;/a> y &lt;a href="https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/table/sql/overview/">Flink SQL docs&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://risingwave.com/">RisingWave&lt;/a> — alternativa SQL streaming con embedding built-in.&lt;/li>
&lt;/ul>
&lt;p>Vector store connectors:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/zilliztech/kafka-connect-milvus">Milvus Sink Connector (Zilliz, GitHub)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://milvus.io/docs/kafka-connect-milvus.md">Connect Apache Kafka with Milvus (docs)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/qdrant/qdrant-kafka">Qdrant Kafka Sink (GitHub)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://callsphere.ai/blog/vector-database-benchmarks-2026-pgvector-qdrant-weaviate-milvus-lancedb">Vector Database Benchmarks 2026&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://streamkap.com/resources-and-guides/streaming-vector-databases-platform-comparison">Streaming to Vector Databases (Streamkap)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Tableflow y arquitectura 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.confluent.io/product/tableflow/">Tableflow — Confluent&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.confluent.io/blog/tableflow-ga-kafka-snowflake-iceberg/">Tableflow GA: Real-Time Kafka to Iceberg (Confluent Blog)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.confluent.io/blog/tableflow-delta-lake-databricks-unity-catalog-ga/">Tableflow + Databricks Unity Catalog (Confluent Blog)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.confluent.io/blog/data-lake-governance-tableflow/">Better-Governed Data Lake Architectures with Tableflow (Confluent Blog)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kai-waehner.de/blog/2025/12/10/top-trends-for-data-streaming-with-apache-kafka-and-flink-in-2026/">Top Trends for Data Streaming with Kafka and Flink in 2026 (Kai Waehner)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>RAG streaming:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://risingwave.com/blog/rag-architecture-2026/">RAG Architecture in 2026: How to Keep Retrieval Actually Fresh (RisingWave)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://streamkap.com/resources-and-guides/streaming-to-vector-databases">Streaming CDC Events to Vector Databases (Streamkap)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kai-waehner.de/blog/2023/11/08/apache-kafka-flink-vector-database-llm-real-time-genai/">Apache Kafka + Vector Database + LLM = Real-Time GenAI (Kai Waehner)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/pdf/2508.05662">From Static to Dynamic: A Streaming RAG Approach (arxiv 2508.05662)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://developer.confluent.io/confluent-tutorials/gen-ai-vector-embedding/flinksql/">How to generate vector embeddings for RAG with Flink SQL (Confluent Developer)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dasroot.net/posts/2026/03/event-driven-architectures-ai-pipelines-kafka-flink/">Event-Driven Architectures for AI Pipelines (dasroot)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Post anterior: &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">MLOps específico para LLMs en 2026: el panorama&lt;/a>.&lt;/li>
&lt;li>Serie post-tracing: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>, &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF + drift&lt;/a>.&lt;/li>
&lt;li>Serie eBPF: &lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF de cero a Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/posts/tetragon-runtime-security/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/posts/hubble-observabilidad-ebpf/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>MLOps específico para LLMs en 2026: el panorama de tres modalidades, seis etapas y diez herramientas que las hacen funcionar</title><link>https://blog.lo0.es/posts/mlops-llms-panorama-2026/</link><pubDate>Thu, 21 May 2026 05:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/mlops-llms-panorama-2026/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Esta es la cuarta serie del blog y se llama &lt;strong>MLOps específico para LLMs&lt;/strong>. Toma el oficio operativo de MLOps tradicional —pipelines reproducibles, model registries, dataset versioning, eval gates, despliegues controlados— y lo redibuja para un mundo donde el modelo es &lt;strong>probabilístico&lt;/strong>, las salidas son &lt;strong>subjetivas&lt;/strong>, las dependencias incluyen &lt;strong>vendors externos que actualizan pesos sin avisar&lt;/strong>, y la &amp;ldquo;aplicación&amp;rdquo; no es un modelo sino una &lt;strong>orquestación de modelos, embeddings, retrievers, guardrails y routers&lt;/strong>. Gartner predice que más del 50% de los despliegues GenAI empresariales fracasarán antes de que acabe 2026, y la causa principal no es el modelo: es que se aplicaron &lt;strong>suposiciones de software determinístico&lt;/strong> a sistemas probabilísticos. Este post abre la serie con el marco: las &lt;strong>siete diferencias estructurales&lt;/strong> entre LLMOps y MLOps clásico; el &lt;strong>pipeline de seis etapas&lt;/strong> (data → tune → eval → deploy → observe → retrain); las &lt;strong>tres modalidades&lt;/strong> de preparar un modelo (fine-tuning continuo, RAG sobre datalakes, agent training) con su matriz de decisión —el 60% de despliegues 2025-2026 usa &lt;strong>hybrid&lt;/strong> porque cada modalidad resuelve un problema distinto: &amp;ldquo;fine-tune para behavior, RAG para conocimiento volátil&amp;rdquo;—; y el &lt;strong>panorama de herramientas 2026&lt;/strong> que ya forma capas razonablemente estables: MLflow 3.10 (marzo 2026) como registry GenAI-aware, W&amp;amp;B Weave y ZenML para tracing y pipelines, Kubeflow + KServe vLLM 0.8.1+ para serving, BentoML para flexibilidad, DVC + lakeFS (unidos desde noviembre 2025) para data, Langfuse para prompts y observabilidad. Los tres posts siguientes bajarán al detalle de las piezas más críticas.&lt;/p>
&lt;blockquote>
&lt;p>Esta es la apertura de la &lt;strong>serie 4: MLOps para LLMs&lt;/strong>. Continúa la tradición de las series previas: &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">inferencia LLM&lt;/a> (la primera), &lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF&lt;/a> (la segunda) y &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">post-tracing&lt;/a> (la tercera). Aquí entramos en la disciplina que ata todas las piezas: cómo se opera un sistema LLM en producción durante meses, no solo se despliega una vez.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-analogía-el-oficio-del-sre-redibujado">La analogía: el oficio del SRE redibujado&lt;/h2>
&lt;p>Quien lleva años trabajando como SRE o como ingeniero de plataforma reconoce los pilares clásicos: &lt;strong>reproducibilidad&lt;/strong> (mismo código + misma data + misma config = mismo resultado), &lt;strong>observabilidad&lt;/strong> (lo que pasa se puede medir), &lt;strong>rollback seguro&lt;/strong> (si algo va mal, vuelvo atrás en minutos), &lt;strong>gradual rollout&lt;/strong> (lo nuevo entra al 1% antes que al 100%). Estos pilares no son negociables. La pregunta es si &lt;strong>se sostienen&lt;/strong> cuando el componente central es un LLM.&lt;/p>
&lt;p>La respuesta es: &lt;strong>mismos pilares, mecánica radicalmente distinta&lt;/strong>. Reproducibilidad: ya no basta con versionar código y datos; hay que versionar &lt;strong>prompts, configuraciones de retrieval, snapshots del modelo del vendor&lt;/strong> (que cambian sin avisar). Observabilidad: ya no basta con métricas de error y latencia; hay que medir &lt;strong>calidad subjetiva&lt;/strong> vía LLM-as-judge y drift de embeddings. Rollback: ya no basta con bajar la versión del binario; hay que &lt;strong>mantener el modelo viejo cacheado&lt;/strong> porque cargar uno nuevo tarda minutos. Gradual rollout: ya no basta con un % de tráfico; hay que decidir qué % de &lt;strong>qué tipo de queries&lt;/strong> por segmento.&lt;/p>
&lt;p>Es el mismo oficio, ejercido con herramientas y reflejos parcialmente nuevos. &lt;strong>MLOps específico para LLMs&lt;/strong> —o &amp;ldquo;LLMOps&amp;rdquo;, como el campo se ha autobautizado— es la disciplina que codifica esos reflejos.&lt;/p>
&lt;h2 id="las-siete-diferencias-estructurales-entre-llmops-y-mlops-tradicional">Las siete diferencias estructurales entre LLMOps y MLOps tradicional&lt;/h2>
&lt;p>Antes de bajar al pipeline, fijemos las diferencias que hacen este territorio nuevo, no una mera continuación. Cada una tiene consecuencias prácticas concretas.&lt;/p>
&lt;h3 id="1-salidas-no-determinísticas">1. Salidas no-determinísticas&lt;/h3>
&lt;p>MLOps tradicional: el modelo recibe input estructurado, devuelve &lt;strong>una predicción acotada y reproducible&lt;/strong>. Mismo input → mismo output. Tests unitarios funcionan.&lt;/p>
&lt;p>LLMOps: mismo input → output &lt;strong>distinto cada vez&lt;/strong> (por sampling, por temperature, por orden de tools invocadas, por el contexto retrieval que cambió). La idea de &amp;ldquo;test unitario&amp;rdquo; se rompe.&lt;/p>
&lt;p>&lt;strong>Consecuencia operativa&lt;/strong>: tests sobre &lt;strong>propiedades&lt;/strong> (¿se mantuvo el tono?, ¿menciona la fuente?, ¿respeta el JSON schema?), no sobre igualdad. Evals estadísticos sobre distribución, no sobre muestras.&lt;/p>
&lt;h3 id="2-métricas-behavior-no-statistical-accuracy">2. Métricas behavior, no statistical accuracy&lt;/h3>
&lt;p>MLOps tradicional: F1, accuracy, AUC, RMSE. Métricas con un número claro.&lt;/p>
&lt;p>LLMOps: &lt;strong>rubric scores&lt;/strong> subjetivos (G-Eval, faithfulness, helpfulness, toxicity), &lt;strong>judge LLMs&lt;/strong>, &lt;strong>human feedback&lt;/strong>. El &amp;ldquo;número&amp;rdquo; depende de quién juzga.&lt;/p>
&lt;p>&lt;strong>Consecuencia operativa&lt;/strong>: las plataformas tienen que tratar evals como &lt;strong>artifacts versionados&lt;/strong> —no solo &amp;ldquo;el modelo v3 sacó 0.87&amp;rdquo;, sino &amp;ldquo;el modelo v3 evaluado con el judge claude-3-5-sonnet-20251022 sobre el dataset gold-rag-v7 con el prompt judge-v2 sacó 0.87&amp;rdquo;—. Versionar el judge es tan importante como versionar el modelo evaluado.&lt;/p>
&lt;h3 id="3-el-modelo-es-dependencia-externa-no-asset-interno">3. El modelo es dependencia externa, no asset interno&lt;/h3>
&lt;p>MLOps tradicional: el modelo lo entrenas tú, vive en tu registry, no cambia hasta que lo cambies.&lt;/p>
&lt;p>LLMOps: el modelo base es de Anthropic, OpenAI, Google, Meta. &lt;strong>Te lo cambian sin avisar&lt;/strong>. La versión &lt;code>claude-3-5-sonnet&lt;/code> que respondía bien ayer responde algo distinto hoy.&lt;/p>
&lt;p>&lt;strong>Consecuencia operativa&lt;/strong>: &lt;strong>drift detection&lt;/strong> se vuelve mucho más crítico (&lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">post anterior&lt;/a>). Pinning a snapshots específicos (&lt;code>claude-3-5-sonnet-20251022&lt;/code>) cuando el vendor lo permite. Para apps de alto compromiso, &lt;strong>self-host del modelo base&lt;/strong> para garantizar reproducibilidad.&lt;/p>
&lt;h3 id="4-la-aplicación-es-una-orquestación-no-un-modelo">4. La aplicación es una orquestación, no un modelo&lt;/h3>
&lt;p>MLOps tradicional: una app llama un modelo y consume su output.&lt;/p>
&lt;p>LLMOps 2026: una app conecta &lt;strong>foundation model + adapters LoRA + retrievers + vector stores + guardrails + routers + tool servers (MCP) + evaluators&lt;/strong>, todos componiendo el comportamiento final. Cualquier componente puede degradar el resultado.&lt;/p>
&lt;p>&lt;strong>Consecuencia operativa&lt;/strong>: el &lt;strong>debugging cross-componente&lt;/strong> requiere tracing distribuido con OTel (cubierto en posts previos). El registry no solo guarda &amp;ldquo;el modelo&amp;rdquo; sino la &lt;strong>composición&lt;/strong>: qué versión del prompt + qué adapter + qué vector store + qué retriever config.&lt;/p>
&lt;h3 id="5-coste-por-inferencia-no-por-training">5. Coste por inferencia, no por training&lt;/h3>
&lt;p>MLOps tradicional: el coste alto es entrenar; servir es barato. Optimizas training.&lt;/p>
&lt;p>LLMOps: el coste alto es &lt;strong>servir&lt;/strong> (cada token cuesta, cada llamada al vendor se paga, las GPUs que sirven están encendidas 24/7). Optimizas inferencia.&lt;/p>
&lt;p>&lt;strong>Consecuencia operativa&lt;/strong>: cost accountability por tenant, por agente, por tool. Métricas como &lt;code>gen_ai.usage.input_tokens&lt;/code> agregadas a nivel cliente y producto. Decisiones de modelo según coste por query, no solo según calidad.&lt;/p>
&lt;h3 id="6-infra-gpu-pesada-con-primitivas-específicas">6. Infra GPU-pesada con primitivas específicas&lt;/h3>
&lt;p>MLOps tradicional: CPU + algo de GPU para entrenamiento. Kubernetes estándar.&lt;/p>
&lt;p>LLMOps: GPUs Hopper/Blackwell SXM, NVLink/NVSwitch, tensor parallel, paged attention, KV cache. Infra que solo encaja en Kubernetes con primitivas como &lt;strong>LeaderWorkerSet, GPU Operator, KEDA con métricas LLM&lt;/strong> (cubierto en &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a>).&lt;/p>
&lt;p>&lt;strong>Consecuencia operativa&lt;/strong>: la pila de orquestación incluye operadores especializados (OME, vLLM Production Stack, NVIDIA Dynamo, llm-d, ver &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a>) que el MLOps tradicional no contempla.&lt;/p>
&lt;h3 id="7-rlhf-y-feedback-humano-como-ciudadano-de-primera">7. RLHF y feedback humano como ciudadano de primera&lt;/h3>
&lt;p>MLOps tradicional: el feedback humano es etiquetar datos antes del training.&lt;/p>
&lt;p>LLMOps: el feedback humano vive &lt;strong>dentro del modelo en producción&lt;/strong>, ya sea por RLHF de los foundation models (Anthropic, OpenAI), por RLAIF, por DPO, o por feedback explícito de usuarios que se reincorpora al fine-tuning.&lt;/p>
&lt;p>&lt;strong>Consecuencia operativa&lt;/strong>: pipelines bidireccionales producción → training. Datasets crecen con incidentes reales. Las decisiones de modelo se toman con feedback continuo, no en un proyecto de training cada N meses.&lt;/p>
&lt;h2 id="por-qué-gartner-predice-50-de-fracasos">Por qué Gartner predice 50%+ de fracasos&lt;/h2>
&lt;p>&lt;a href="https://medium.com/@sanjeebmeister/the-complete-mlops-llmops-roadmap-for-2026-building-production-grade-ai-systems-bdcca5ed2771">Gartner publicó que más del 50% de los despliegues GenAI empresariales fracasarán antes de 2026&lt;/a>. Las causas no son técnicas sobre el modelo sino sobre el sistema:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hallucinated outputs por mal grounding&lt;/strong>: RAG mal diseñado, retrieval pobre, contexto insuficiente.&lt;/li>
&lt;li>&lt;strong>Arquitecturas de datos no preparadas&lt;/strong>: las empresas tienen datos en silos, sin schemas estables, sin freshness controlado. Conectar un LLM a estos datos sin pipeline serio produce respuestas erráticas.&lt;/li>
&lt;li>&lt;strong>Falta de workflows estructurados&lt;/strong> para sistemas prompt-driven: equipos que tratan los prompts como código en strings hardcodeados, sin versionado, sin tests, sin gates.&lt;/li>
&lt;/ul>
&lt;p>La conclusión que el campo extrae: &lt;strong>LLMOps no es opcional&lt;/strong>. Las empresas que despliegan GenAI sin disciplina operacional caen en uno de los tres modos de fracaso. Las que la aplican —MLflow/W&amp;amp;B para tracking, DVC/lakeFS para datos, Langfuse para prompts y evals, KServe o vLLM Production Stack para serving, drift detection en producción— son las que mantienen el sistema funcionando seis meses después del primer release.&lt;/p>
&lt;h2 id="el-pipeline-llmops-de-seis-etapas">El pipeline LLMOps de seis etapas&lt;/h2>
&lt;p>Vamos al pipeline. Las seis etapas que cualquier sistema LLM serio recorre, en orden:&lt;/p>
&lt;pre tabindex="0">&lt;code>[1. Data] → [2. Tune] → [3. Eval] → [4. Deploy] → [5. Observe] → [6. Retrain]
│
└─→ vuelve a 1
&lt;/code>&lt;/pre>&lt;p>Cada etapa es un dominio operacional propio con sus herramientas y trampas:&lt;/p>
&lt;h3 id="etapa-1--data">Etapa 1 — Data&lt;/h3>
&lt;p>&lt;strong>Qué pasa&lt;/strong>: ingestión, limpieza, curación, versionado, indexación del corpus. Es donde más se sufre en proyectos reales porque las empresas tienen datos en silos heterogéneos.&lt;/p>
&lt;p>&lt;strong>Sub-tareas típicas&lt;/strong>: extracción desde origen (CDC sobre Kafka, batch desde data lakes, scraping), limpieza (PII removal, dedup, formato), curación (labeling para fine-tuning, golden datasets para eval), versionado (DVC + lakeFS), indexación (embeddings + vector store para RAG).&lt;/p>
&lt;p>&lt;strong>Trampas&lt;/strong>: drift de schema en el origen, PII no detectada, dedup pobre que mete redundancia en training, vector store que no se actualiza.&lt;/p>
&lt;h3 id="etapa-2--tune">Etapa 2 — Tune&lt;/h3>
&lt;p>&lt;strong>Qué pasa&lt;/strong>: preparar el modelo para tu caso de uso. Tres modalidades (las profundizamos en breve): fine-tuning, RAG, agent training.&lt;/p>
&lt;p>&lt;strong>Sub-tareas típicas&lt;/strong>: selección de modelo base, preparación del adapter (LoRA, QLoRA), training loop con eval continuo, hyperparameter sweep (Optuna, W&amp;amp;B Sweeps), guardado del checkpoint.&lt;/p>
&lt;p>&lt;strong>Trampas&lt;/strong>: catastrophic forgetting si el fine-tuning es muy agresivo, overfitting al dataset golden, sin validation set independiente.&lt;/p>
&lt;h3 id="etapa-3--eval">Etapa 3 — Eval&lt;/h3>
&lt;p>&lt;strong>Qué pasa&lt;/strong>: validar que el modelo + adapters + RAG configuration es aceptable antes de promotar. Cubierto en &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Sub-tareas típicas&lt;/strong>: ejecución de eval framework (DeepEval, Promptfoo, Ragas) contra golden dataset, judge LLM evaluations, human review sobre muestreo, gates con thresholds.&lt;/p>
&lt;p>&lt;strong>Trampas&lt;/strong>: golden dataset que envejece, judge no calibrado, evals que pasan en CI pero fallan en producción por shift de distribución.&lt;/p>
&lt;h3 id="etapa-4--deploy">Etapa 4 — Deploy&lt;/h3>
&lt;p>&lt;strong>Qué pasa&lt;/strong>: pasar de &amp;ldquo;el modelo se evaluó bien&amp;rdquo; a &amp;ldquo;el modelo sirve tráfico real&amp;rdquo;. Cubierto en &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Sub-tareas típicas&lt;/strong>: serving con vLLM/SGLang/TRT-LLM, configuración del runtime, rollout gradual (canary, shadow, blue-green), routing entre modelos (LiteLLM, OpenRouter, LangChain routers).&lt;/p>
&lt;p>&lt;strong>Trampas&lt;/strong>: rolling update naive que corta sesiones, autoscaling por CPU% que no responde a métricas LLM (cubierto), modelo nuevo que rinde peor en producción que en eval.&lt;/p>
&lt;h3 id="etapa-5--observe">Etapa 5 — Observe&lt;/h3>
&lt;p>&lt;strong>Qué pasa&lt;/strong>: ver lo que está pasando en tiempo real. Cubierto en la serie post-tracing entera y la serie eBPF.&lt;/p>
&lt;p>&lt;strong>Sub-tareas típicas&lt;/strong>: tracing (Langfuse, LangSmith, Phoenix, OpenLLMetry), métricas (TTFT, TPOT, queue depth, cost per query), guardrails activos (NeMo, Llama Guard), drift detection (Evidently, NannyML, WhyLabs).&lt;/p>
&lt;p>&lt;strong>Trampas&lt;/strong>: explosión de cardinalidad en métricas, evals batch sin tail-sampling sobre traces reales, drift que se ignora hasta que el incidente lo materializa.&lt;/p>
&lt;h3 id="etapa-6--retrain">Etapa 6 — Retrain&lt;/h3>
&lt;p>&lt;strong>Qué pasa&lt;/strong>: cerrar el bucle. El feedback de producción (incidentes, casos peor evaluados, drift detectado) genera nuevos datos para volver a la etapa 1.&lt;/p>
&lt;p>&lt;strong>Sub-tareas típicas&lt;/strong>: extracción de logs problemáticos, labeling humano de la muestra, incorporación al dataset golden, re-fine-tuning si aplica, decisión sobre nuevo release.&lt;/p>
&lt;p>&lt;strong>Trampas&lt;/strong>: bucle &amp;ldquo;abierto&amp;rdquo; donde producción no informa nunca al dataset, feedback humano que se pierde, falta de cadencia clara de retrain.&lt;/p>
&lt;h2 id="las-tres-modalidades-de-preparar-el-modelo">Las tres modalidades de &amp;ldquo;preparar el modelo&amp;rdquo;&lt;/h2>
&lt;p>La etapa 2 (Tune) es donde más confusión hay. En 2026 conviven &lt;strong>tres modalidades&lt;/strong>, cada una resolviendo un problema distinto:&lt;/p>
&lt;h3 id="fine-tuning">Fine-tuning&lt;/h3>
&lt;p>&lt;strong>Qué hace&lt;/strong>: modificar los pesos del modelo (o de un adapter LoRA/QLoRA encima) para que aprenda &lt;strong>patrones de comportamiento&lt;/strong> específicos: tono, estructura de output, decisiones idiomáticas del dominio.&lt;/p>
&lt;p>&lt;strong>Cuándo&lt;/strong>: cuando tu fallo principal es &lt;strong>inconsistencia de comportamiento&lt;/strong> entre llamadas. El modelo a veces responde formal, a veces no; a veces estructura el JSON, a veces no; a veces sigue las convenciones de la empresa, a veces inventa. Fine-tuning lo estabiliza.&lt;/p>
&lt;p>&lt;strong>Cuándo NO&lt;/strong>: cuando lo que necesitas es &lt;strong>conocimiento actualizado&lt;/strong>. Fine-tuning fija conocimiento en pesos congelados; al día siguiente del fine-tuning, el modelo no sabe nada nuevo.&lt;/p>
&lt;h3 id="rag-retrieval-augmented-generation">RAG (Retrieval-Augmented Generation)&lt;/h3>
&lt;p>&lt;strong>Qué hace&lt;/strong>: dejar el modelo intacto y, en cada llamada, &lt;strong>recuperar contexto fresco&lt;/strong> de un knowledge base (vector store + lexical search típicamente) y pasárselo al modelo para que responda basándose en él.&lt;/p>
&lt;p>&lt;strong>Cuándo&lt;/strong>: cuando el conocimiento que necesitas es &lt;strong>dinámico o muy grande&lt;/strong>. Documentación que cambia, catálogo de productos que se actualiza, knowledge base interna que crece.&lt;/p>
&lt;p>&lt;strong>Cuándo NO&lt;/strong>: cuando el problema es behavioral (RAG no enseña al modelo a comportarse, solo le da información). O cuando el retrieval es tan ruidoso que el contexto que llega es peor que nada.&lt;/p>
&lt;h3 id="agent-training">Agent training&lt;/h3>
&lt;p>&lt;strong>Qué hace&lt;/strong>: ir más allá del fine-tuning convencional con técnicas de Reinforcement Learning. RFT (Reinforcement Fine-Tuning de OpenAI), RLHF clásico, RLAIF (con AI feedback), DPO (Direct Preference Optimization) sobre datasets de pares (good, bad).&lt;/p>
&lt;p>&lt;strong>Cuándo&lt;/strong>: cuando el modelo necesita aprender &lt;strong>trayectorias multistep complejas&lt;/strong> —cuando elegir cada tool, cómo descomponer una tarea, cuándo pedir confirmación al usuario—. Es lo que está convirtiendo a Claude, Gemini, GPT en agentes capaces de tareas largas.&lt;/p>
&lt;p>&lt;strong>Cuándo NO&lt;/strong>: cuando tu caso es chat simple o RAG. Es overkill, caro y complicado para problemas que las modalidades anteriores resuelven.&lt;/p>
&lt;h3 id="matriz-de-decisión">Matriz de decisión&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Problema observado&lt;/th>
&lt;th>Modalidad&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Respuestas inconsistentes en tono/estructura&lt;/td>
&lt;td>Fine-tuning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo inventa cosas (alucina)&lt;/td>
&lt;td>RAG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Conocimiento desactualizado (&amp;gt;1 año)&lt;/td>
&lt;td>RAG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo elige mal las tools&lt;/td>
&lt;td>Agent training (RLAIF/RFT)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Behavior + conocimiento mixto&lt;/td>
&lt;td>&lt;strong>Hybrid (fine-tune + RAG)&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-step trajectory falla&lt;/td>
&lt;td>Agent training&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Idioma/estilo regional concreto&lt;/td>
&lt;td>Fine-tuning&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="el-veredicto-2026-hybrid-es-el-default">El veredicto 2026: hybrid es el default&lt;/h3>
&lt;p>&lt;a href="https://www.scalacode.com/blog/rag-vs-fine-tuning/">Múltiples reports&lt;/a> coinciden en que en 2025-2026, &lt;strong>alrededor del 60% de proyectos productivos usan hybrid&lt;/strong>: fine-tuning para behavior + RAG para knowledge. El insight clave:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Fine-tune para comportamiento (brand voice, decision protocol, output structure); usa RAG para conocimiento volátil que necesitas que el modelo cite. No fuerces una sola herramienta a hacer ambos trabajos.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>Una observación práctica: las mejoras de calidad más grandes de 2025-2026 vienen de &lt;strong>mejor reranking en RAG&lt;/strong> (cross-encoders), no de mejores embeddings. Los rerankers añaden 15-35% de calidad con poca complejidad.&lt;/p>
&lt;p>Sobre coste: combined fine-tuning + RAG suele ser &lt;strong>30-50% más barato&lt;/strong> que RAG puro con frontier models a volumen alto, porque el modelo finetuneado puede ser más pequeño y barato manteniendo calidad equivalente.&lt;/p>
&lt;h2 id="el-panorama-de-herramientas-2026">El panorama de herramientas 2026&lt;/h2>
&lt;p>Vamos a las piezas concretas, agrupadas por función. El campo ha madurado lo suficiente para que cada pieza tenga 2-3 opciones razonables y un par de líderes.&lt;/p>
&lt;h3 id="experiment-tracking-y-model-registry">Experiment tracking y model registry&lt;/h3>
&lt;p>&lt;strong>&lt;a href="https://mlflow.org/">MLflow&lt;/a>&lt;/strong> sigue siendo el estándar de facto, ahora con tracción específica LLM. &lt;strong>MLflow 3&lt;/strong> se publicó en junio 2025; la versión 3.10.1 (marzo 2026) añadió:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>GenAI Overview dashboard&lt;/strong> con métricas pre-hechas para LLM apps.&lt;/li>
&lt;li>&lt;strong>Multi-workspace support&lt;/strong> para equipos grandes.&lt;/li>
&lt;li>&lt;strong>Cost tracking en traces&lt;/strong> (gen_ai.usage.* agregados por experimento).&lt;/li>
&lt;li>&lt;strong>MemAlign&lt;/strong>: nuevo algoritmo de eval específico.&lt;/li>
&lt;li>&lt;strong>OpenTelemetry tracing nativo&lt;/strong> integrado.&lt;/li>
&lt;li>Soporte de primera para &lt;strong>LangChain, LlamaIndex, AutoGen&lt;/strong> como frameworks.&lt;/li>
&lt;/ul>
&lt;p>MLflow trata prompts y agents como &lt;strong>ciudadanos de primera clase&lt;/strong> junto a los modelos clásicos. Es el cambio mayor respecto a MLflow 2.x.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://wandb.ai/">Weights &amp;amp; Biases (W&amp;amp;B)&lt;/a>&lt;/strong> con su producto &lt;strong>Weave&lt;/strong> específico para LLM ofrece tracing + eval + debug con UI muy pulida. Más comercial, menos self-host friendly, pero excelente UX.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://www.zenml.io/">ZenML&lt;/a>&lt;/strong> es la pieza que más limpia integra &amp;ldquo;MLOps clásico + LLMOps emergente&amp;rdquo; en un solo framework. Su artifact versioning &lt;strong>automático&lt;/strong> captura prompt templates, retrieval chunks, agent conversation histories sin trabajo extra. Open-source. La opción de unificación más completa que existe.&lt;/p>
&lt;h3 id="dataset-versioning">Dataset versioning&lt;/h3>
&lt;p>&lt;strong>&lt;a href="https://dvc.org/">DVC&lt;/a>&lt;/strong> sigue siendo el estándar OSS. Extiende Git a archivos grandes y pipelines. &lt;strong>Noticia importante de noviembre 2025&lt;/strong>: &lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS adquirió DVC&lt;/a>, consolidando los dos proyectos OSS de versionado de datos bajo una organización. La hoja de ruta combinada está orientada a LLM training y RAG datalakes específicamente.&lt;/p>
&lt;p>&lt;strong>Patrón típico&lt;/strong>: Git para código + DVC para data/modelos + MLflow o W&amp;amp;B para experiment tracking + registry. Pocas teams usan uno solo; la &lt;strong>combinación&lt;/strong> es lo que cubre el ciclo.&lt;/p>
&lt;h3 id="prompt-versioning-y-observability">Prompt versioning y observability&lt;/h3>
&lt;p>Cubierto en profundidad en el &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">post de AgentSight&lt;/a> donde profundizamos en &lt;strong>Langfuse&lt;/strong> como referencia OSS. Resumen aquí:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://langfuse.com/">Langfuse&lt;/a>&lt;/strong>: MIT, self-host, prompt management con versionado v1/v2/v3 + labels + cache + linkage con traces.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.langchain.com/langsmith">LangSmith&lt;/a>&lt;/strong>: si tu stack es LangChain.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a>&lt;/strong>: ELv2, OTel-native.&lt;/li>
&lt;/ul>
&lt;h3 id="pipeline-orchestration">Pipeline orchestration&lt;/h3>
&lt;p>Para los pasos del pipeline LLMOps, las opciones dominantes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://www.kubeflow.org/docs/components/pipelines/">Kubeflow Pipelines&lt;/a>&lt;/strong>: el estándar K8s-native. KServe (la parte de serving de Kubeflow) tiene &lt;strong>vLLM runtime upgraded a v0.8.1+&lt;/strong> con soporte para reasoning models, tool calling, embeddings, reranking, Llama 4 y Qwen 3.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.zenml.io/">ZenML&lt;/a>&lt;/strong>: ya mencionado; también orquestador de pipelines.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://metaflow.org/">Metaflow&lt;/a>&lt;/strong> (Netflix-originated): pipelines Python-first, menos LLM-específico pero workable.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://argoproj.github.io/workflows/">Argo Workflows&lt;/a>&lt;/strong>: alternativa OSS pura K8s.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://flyte.org/">Flyte&lt;/a>&lt;/strong>: Kubernetes-native, OSS.&lt;/li>
&lt;/ul>
&lt;h3 id="serving">Serving&lt;/h3>
&lt;p>Cubierto en profundidad en &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en Kubernetes&lt;/a> y &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a>. Resumen:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack&lt;/a>&lt;/strong>: Helm chart curado.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://kserve.github.io/website/">KServe vLLM runtime&lt;/a>&lt;/strong>: K8s-native, vLLM 0.8.1+ con soporte agentic completo.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.bentoml.com/">BentoML&lt;/a>&lt;/strong>: serving flexible, popular en startups por su simplicidad.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo&lt;/a>&lt;/strong>: el sucesor de Triton.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/llm-d/llm-d">llm-d&lt;/a>&lt;/strong>: CNCF Sandbox.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/ome-projects/ome">OME&lt;/a>&lt;/strong>: LMSYS operator con SGLang nativo.&lt;/li>
&lt;/ul>
&lt;h3 id="evals-y-guardrails">Evals y guardrails&lt;/h3>
&lt;p>Cubierto en &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a> y &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails&lt;/a>. Resumen ultra-corto:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Evals CI&lt;/strong>: DeepEval, Promptfoo, Ragas.&lt;/li>
&lt;li>&lt;strong>Evals platform&lt;/strong>: Langfuse, LangSmith, Phoenix, Braintrust.&lt;/li>
&lt;li>&lt;strong>Guardrails&lt;/strong>: NeMo Guardrails, Llama Guard 4, Llama Prompt Guard 2, LLM Guard, Lakera.&lt;/li>
&lt;/ul>
&lt;h3 id="drift-detection-y-observability">Drift detection y observability&lt;/h3>
&lt;p>Cubierto en el &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">post de cierre eBPF&lt;/a>. Resumen:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Drift&lt;/strong>: Evidently AI, NannyML, WhyLabs.&lt;/li>
&lt;li>&lt;strong>Tracing&lt;/strong>: Langfuse, OpenLLMetry, Phoenix.&lt;/li>
&lt;li>&lt;strong>eBPF&lt;/strong>: AgentSight, Hubble, Tetragon, ProfInfer.&lt;/li>
&lt;/ul>
&lt;h3 id="la-tabla-de-stack-típico-2026">La tabla de stack típico 2026&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Etapa&lt;/th>
&lt;th>Pieza dominante&lt;/th>
&lt;th>Alternativas&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Data ingestión + versioning&lt;/td>
&lt;td>DVC + lakeFS (unificadas Nov 2025)&lt;/td>
&lt;td>Pachyderm, Quilt&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector store / RAG index&lt;/td>
&lt;td>Milvus, Qdrant, pgvector, Weaviate&lt;/td>
&lt;td>LanceDB, Pinecone, Chroma&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Experiment tracking&lt;/td>
&lt;td>MLflow 3.10&lt;/td>
&lt;td>W&amp;amp;B Weave, Neptune&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pipeline orchestration&lt;/td>
&lt;td>Kubeflow + Argo&lt;/td>
&lt;td>ZenML, Metaflow, Flyte&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model registry&lt;/td>
&lt;td>MLflow registry&lt;/td>
&lt;td>W&amp;amp;B Models, KServe ModelMesh&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prompt versioning&lt;/td>
&lt;td>Langfuse&lt;/td>
&lt;td>LangSmith, MLflow Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Serving&lt;/td>
&lt;td>vLLM Production Stack&lt;/td>
&lt;td>KServe, BentoML, Dynamo, llm-d, OME&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Evals CI&lt;/td>
&lt;td>DeepEval, Ragas&lt;/td>
&lt;td>Promptfoo, OpenAI Evals&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Evals platform&lt;/td>
&lt;td>Langfuse, Phoenix&lt;/td>
&lt;td>LangSmith, Braintrust&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Guardrails&lt;/td>
&lt;td>NeMo + Llama Guard&lt;/td>
&lt;td>LLM Guard, Lakera&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tracing&lt;/td>
&lt;td>OpenLLMetry + Langfuse&lt;/td>
&lt;td>Phoenix, LangSmith&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift detection&lt;/td>
&lt;td>Evidently AI&lt;/td>
&lt;td>NannyML, WhyLabs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>eBPF observability&lt;/td>
&lt;td>AgentSight + Tetragon + Hubble&lt;/td>
&lt;td>(territorio nuevo, pocas alternativas)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>13 piezas. Ninguna org usa todas; cualquier org seria usa al menos seis. &lt;strong>Esto es el LLMOps stack actual&lt;/strong>.&lt;/p>
&lt;h2 id="la-realidad-operativa-nadie-usa-una-sola-herramienta">La realidad operativa: nadie usa una sola herramienta&lt;/h2>
&lt;p>&lt;a href="https://medium.com/@kanerika/mlflow-vs-kubeflow-vs-w-b-which-mlops-tool-fits-your-stack-b59007460b25">Múltiples comparativas&lt;/a> coinciden en algo: &lt;strong>los equipos que ganan combinan&lt;/strong>. Patrones recurrentes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>ZenML para orquestar + MLflow para tracking + KServe para serving&lt;/strong>: el stack OSS más popular en empresas que vienen de MLOps clásico.&lt;/li>
&lt;li>&lt;strong>Kubeflow + W&amp;amp;B + BentoML&lt;/strong>: para equipos con foco en research.&lt;/li>
&lt;li>&lt;strong>Langfuse + DeepEval + Phoenix + LiteLLM&lt;/strong>: para equipos LLM-puros sin background MLOps clásico.&lt;/li>
&lt;li>&lt;strong>MLflow + DVC + Argo + KServe&lt;/strong>: stack idiomático cloud-native sin LLM-specifics adicionales (con sus limitaciones).&lt;/li>
&lt;/ul>
&lt;p>La elección depende del background del equipo, del modelo de licencia que pueden permitirse, del nivel de self-hosting que necesitan, y de qué fricciones les bloquearon más en proyectos previos. &lt;strong>No hay &amp;ldquo;una respuesta correcta&amp;rdquo;&lt;/strong>; hay un meta-patrón estable de capas que conviene cubrir.&lt;/p>
&lt;h2 id="trampas-operativas-comunes">Trampas operativas comunes&lt;/h2>
&lt;h3 id="tratar-el-prompt-como-texto-en-código">Tratar el prompt como texto en código&lt;/h3>
&lt;p>Hardcodear prompts en strings en el repo. Cambiarlos requiere PR + redeploy. Resultado: equipos que no iteran sobre prompts porque cada cambio cuesta horas de pipeline. &lt;strong>Solución&lt;/strong>: prompt management externalizado (Langfuse, MLflow Prompts) con versionado, etiquetas, hot-reload.&lt;/p>
&lt;h3 id="saltarse-el-dataset-versioning">Saltarse el dataset versioning&lt;/h3>
&lt;p>&amp;ldquo;DVC es complicado, ya lo metemos después&amp;rdquo;. Resultado: dos meses después, nadie sabe qué dataset entrenó qué modelo. Imposible reproducir incidentes. &lt;strong>Solución&lt;/strong>: DVC + lakeFS desde el día 1, aunque sea con un subset pequeño.&lt;/p>
&lt;h3 id="mezclar-capas-en-el-mismo-pipeline">Mezclar capas en el mismo pipeline&lt;/h3>
&lt;p>Equipos que meten ingestión, fine-tuning, eval, deploy en un único pipeline gigante. Cuando algo falla, todo el pipeline falla. &lt;strong>Solución&lt;/strong>: pipelines independientes por etapa, con artifacts versionados como interfaces entre ellos.&lt;/p>
&lt;h3 id="tracking-sin-estructura">Tracking sin estructura&lt;/h3>
&lt;p>Loguear todo en stdout y &amp;ldquo;ya lo veremos en CloudWatch&amp;rdquo;. Resultado: imposible correlar, comparar, debugear. &lt;strong>Solución&lt;/strong>: OTel desde el día 1 con &lt;code>gen_ai.*&lt;/code> semantic conventions.&lt;/p>
&lt;h3 id="evals-que-no-bloquean-nada">Evals que no bloquean nada&lt;/h3>
&lt;p>Tienes evals, los corres, los miras, pero &lt;strong>no impiden el deploy&lt;/strong> si bajan. Eventualmente baja gradualmente y nadie lo nota. &lt;strong>Solución&lt;/strong>: eval gates en CI/CD que &lt;strong>bloquean merge&lt;/strong> si métricas críticas regresan más de X%.&lt;/p>
&lt;h3 id="sin-retrain-cadence">Sin retrain cadence&lt;/h3>
&lt;p>Lanzas v1 y nunca vuelves al modelo. Seis meses después, drift lo ha degradado pero el equipo está en otros proyectos. &lt;strong>Solución&lt;/strong>: cadencia formal de retrain (mensual, trimestral) ligada a la cola de incidentes de producción.&lt;/p>
&lt;h3 id="vendor-lock-in-invisible">Vendor lock-in invisible&lt;/h3>
&lt;p>Empiezas con OpenAI API + LangSmith + Pinecone. Cuando quieres self-host, &lt;strong>descubres&lt;/strong> que migrar es un proyecto de 3 meses. &lt;strong>Solución&lt;/strong>: capas de abstracción (LiteLLM, OpenLLMetry) y vendor-neutrality desde el principio.&lt;/p>
&lt;h2 id="lo-que-viene-en-los-siguientes-posts-de-la-serie">Lo que viene en los siguientes posts de la serie&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Post 2 — &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre datalakes con Kafka: arquitectura técnica end-to-end&lt;/a>&lt;/strong> — el más hands-on. Kafka como source-of-truth, Flink CDC, embedding pipelines, indexación continua en Milvus/Qdrant, ejemplo completo con números reales y manifests.&lt;/li>
&lt;li>&lt;strong>Post 3 — &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas: arquitectura global&lt;/a>&lt;/strong> — el mapa maestro del sistema completo con SVG reutilizable de &amp;ldquo;estás aquí&amp;rdquo; para los siguientes posts. Deep dive en cada una de las seis etapas (Data, Tune, Eval, Deploy, Observe, Retrain).&lt;/li>
&lt;li>&lt;strong>Post 4 — &lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">PostgreSQL + Qdrant en la etapa de ingestión&lt;/a>&lt;/strong> — patrones de sincronización (dual-write, outbox + CDC, event-driven), arquitectura de microservicios completa, manifest de Qdrant cluster.&lt;/li>
&lt;li>&lt;strong>Próximos posts&lt;/strong> — pendientes de decidir: el cluster como plataforma multi-tenant, Constitutional AI / alignment runtime, fine-tuning continuo en profundidad, edge LLMs.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>LLMOps vs MLOps:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/@sanjeebmeister/the-complete-mlops-llmops-roadmap-for-2026-building-production-grade-ai-systems-bdcca5ed2771">The Complete MLOps/LLMOps Roadmap for 2026 (Sanjeeb Panda)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.iamraghuveer.com/posts/mlops-vs-llmops-what-changes/">MLOps vs LLMOps: What Changes (Raghuveer)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://hyscaler.com/insights/mlops-in-2026-guide/">MLOps in 2026: Architecture, Trends &amp;amp; Strategy (Hyscaler)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.ideas2it.com/blogs/llmops-vs-mlops-key-differences-and-evolution">LLMOps vs MLOps: Differences and Evolution (Ideas2IT)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dev.to/apprecode/llmops-vs-mlops-whats-different-whats-the-same-and-how-to-run-both-in-production-2o52">LLMOps vs MLOps in production (DEV/Apprecode)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Herramientas:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://mlflow.org/">MLflow&lt;/a> — registry + tracking + serving.&lt;/li>
&lt;li>&lt;a href="https://wandb.ai/site/weave">Weights &amp;amp; Biases Weave&lt;/a> — LLM tracing.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/">ZenML&lt;/a> — pipeline orchestration MLOps + LLMOps.&lt;/li>
&lt;li>&lt;a href="https://www.kubeflow.org/">Kubeflow&lt;/a> — K8s-native MLOps.&lt;/li>
&lt;li>&lt;a href="https://kserve.github.io/website/">KServe&lt;/a> — model serving K8s.&lt;/li>
&lt;li>&lt;a href="https://www.bentoml.com/">BentoML&lt;/a> — serving flexible.&lt;/li>
&lt;li>&lt;a href="https://metaflow.org/">Metaflow&lt;/a> — Netflix&amp;rsquo;s pipelines.&lt;/li>
&lt;li>&lt;a href="https://dvc.org/">DVC&lt;/a> — dataset versioning.&lt;/li>
&lt;li>&lt;a href="https://lakefs.io/">lakeFS&lt;/a> — data versioning enterprise, adquirió DVC en Nov 2025.&lt;/li>
&lt;/ul>
&lt;p>Comparativas 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/@kanerika/mlflow-vs-kubeflow-vs-w-b-which-mlops-tool-fits-your-stack-b59007460b25">MLflow vs Kubeflow vs W&amp;amp;B (Kanerika)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/blog/mlflow-alternatives">9 MLflow alternatives tested (ZenML)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/blog/metaflow-vs-kubeflow">Metaflow vs Kubeflow vs ZenML (ZenML)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/blog/mlops-tools">12 Best MLOps Tools for Agentic AI (ZenML)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/mlops-pipeline-gpu-cloud-kubeflow-zenml-metaflow-2026/">MLOps Pipeline on GPU Cloud 2026 (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://northflank.com/blog/top-7-kubeflow-alternatives">Top 7 Kubeflow alternatives 2026 (Northflank)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.sganalytics.com/blog/mlops-tools/">Top 20 MLOps Tools 2026 (SG Analytics)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>RAG vs Fine-Tuning:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.scalacode.com/blog/rag-vs-fine-tuning/">RAG Vs Fine-Tuning In 2026 (ScalaCode)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.arxiv.org/pdf/2510.01375">Fine-Tuning with RAG (ICLR 2026, arxiv)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dev.to/tyson_cung/rag-vs-fine-tuning-what-actually-works-in-production-2026-20jg">RAG vs Fine-Tuning — What Actually Works in Production 2026 (DEV)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kapa.ai/blog/how-to-build-a-rag-pipeline-from-scratch-in-2026">How to Build a RAG Pipeline from Scratch in 2026 (kapa.ai)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references (las tres series previas):&lt;/p>
&lt;ul>
&lt;li>Serie inferencia LLM: &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/posts/vllm-kubernetes/">vLLM en K8s&lt;/a>, &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention&lt;/a>, &lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators LLM K8s&lt;/a>.&lt;/li>
&lt;li>Serie eBPF: &lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF de cero a Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/posts/tetragon-runtime-security/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/posts/hubble-observabilidad-ebpf/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a>.&lt;/li>
&lt;li>Serie post-tracing: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>, &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF + drift&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>