<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Entrenamiento-Llm on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/entrenamiento-llm/</link><description>Recent content in Entrenamiento-Llm on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Fri, 22 May 2026 07:45:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/entrenamiento-llm/index.xml" rel="self" type="application/rss+xml"/><item><title>Retrain: cerrar el bucle entre el incidente en producción y el adapter que lo arregla</title><link>https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/</link><pubDate>Fri, 22 May 2026 07:45:00 +0200</pubDate><guid>https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La etapa &lt;strong>Retrain&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">pipeline LLMOps de seis etapas&lt;/a> es la que cierra el ciclo. Sin ella, el sistema desplegado es un proyecto que termina; con ella, es una práctica viva que mejora cada trimestre. La mecánica a primer nivel encaja en cinco sub-procesos secuenciales: &lt;strong>capturar feedback&lt;/strong> (explícito vía thumbs + implícito vía latencia, abandonment, retries), &lt;strong>triajar incidentes&lt;/strong> por causa raíz (model issue, retrieval issue, prompt issue, infra issue), &lt;strong>enriquecer el dataset&lt;/strong> con los casos donde el sistema falló y la respuesta correcta etiquetada por humano, &lt;strong>decidir cadencia&lt;/strong> (scheduled trimestral por defecto + incident-driven cuando un patrón supera threshold), y &lt;strong>promocionar&lt;/strong> el candidato pasándolo por Tune → Eval → Deploy con gates contra el modelo en producción. Las herramientas que el mercado ha consolidado en 2026: Langfuse para feedback collection en la UI, Argilla y Label Studio para anotación humana del dataset enriquecido, MLflow stages para promotion. La trampa más letal —y la más común— es el &lt;strong>bucle abierto&lt;/strong>: tener todas las piezas pero sin canal estructurado que las conecte, con lo que la etapa Retrain se reduce a &amp;ldquo;ya retrenamos cuando haga falta&amp;rdquo; y por tanto nunca.&lt;/p>
&lt;h2 id="estás-aquí-retrain-cierra-el-ciclo-hacia-data">Estás aquí: Retrain (cierra el ciclo hacia Data)&lt;/h2>
&lt;p>Este post entra al detalle de la &lt;strong>etapa 6&lt;/strong> del pipeline LLMOps. Lo que sigue desmonta los cinco sub-procesos de Retrain a primer nivel completo, sin bajar a la mecánica interna de Tune (cubierta en el &lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">post de fine-tuning continuo&lt;/a>) ni a la implementación de las suites de eval (cubierta en el &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">post de evals&lt;/a>).&lt;/p>
&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(#rtm)}.cyc{stroke:#c66;stroke-width:2;fill:none;stroke-dasharray:4 2;marker-end:url(#rtm)}&lt;/style>
&lt;defs>&lt;marker id="rtm" 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 · cierra el ciclo de Observe a 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;h2 id="la-analogía-maestra-el-comité-de-mortalidad-del-hospital">La analogía maestra: el comité de mortalidad del hospital&lt;/h2>
&lt;p>Un hospital serio celebra reuniones periódicas de &lt;strong>morbidity &amp;amp; mortality&lt;/strong> (M&amp;amp;M): los médicos revisan, sin culpa pero sin omitir nada, los casos donde un paciente murió o tuvo una complicación grave. Buscan causa raíz, identifican patrones, ajustan protocolos, y dejan registro. El comité no se reúne cuando &amp;ldquo;se acuerdan&amp;rdquo;; está calendarizado y es obligatorio. Y cuando hay un incidente catastrófico fuera de ciclo, se convoca M&amp;amp;M extraordinario en 48 h.&lt;/p>
&lt;p>La etapa &lt;strong>Retrain&lt;/strong> es exactamente eso para un sistema LLM:&lt;/p>
&lt;ul>
&lt;li>El &lt;strong>morbidity&lt;/strong> son los incidentes leves: respuestas que el usuario marcó con thumbs-down, sesiones donde reintentó la misma pregunta tres veces, ejemplos donde el eval score bajó pero no por debajo del threshold de alerta.&lt;/li>
&lt;li>El &lt;strong>mortality&lt;/strong> son los incidentes graves: el sistema dio una respuesta peligrosa, un cliente clave canceló por una serie de errores, el agente ejecutó una tool que no debía.&lt;/li>
&lt;li>Las &lt;strong>reuniones periódicas&lt;/strong> son el &lt;strong>scheduled retrain&lt;/strong> trimestral: se mira la acumulación de feedback, se prioriza, se decide qué entra al dataset enriquecido para el próximo entrenamiento.&lt;/li>
&lt;li>Los &lt;strong>M&amp;amp;M extraordinarios&lt;/strong> son los &lt;strong>incident-driven retrain&lt;/strong>: ante un patrón problemático que supera threshold, se dispara un mini-ciclo fuera de cadencia.&lt;/li>
&lt;/ul>
&lt;p>Sin esta disciplina, los incidentes son anécdotas que se olvidan y el sistema no aprende.&lt;/p>
&lt;h2 id="sub-proceso-1--captura-de-feedback">Sub-proceso 1 — Captura de feedback&lt;/h2>
&lt;p>El primer eslabón del bucle es &lt;strong>observar lo que el sistema hace mal&lt;/strong>. Hay dos familias de feedback, complementarias.&lt;/p>
&lt;h3 id="feedback-explícito">Feedback explícito&lt;/h3>
&lt;p>El usuario te dice directamente que la respuesta fue mala. Mecanismos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Thumbs up/down&lt;/strong> en la UI: el clásico, baja latencia (1 click). Cobertura: 1-5 % del tráfico típicamente. Sesgo: los usuarios votan más cuando están molestos que cuando están contentos.&lt;/li>
&lt;li>&lt;strong>Anotación por usuarios power&lt;/strong>: clientes internos o expertos que dejan comentarios estructurados (&amp;ldquo;la respuesta es correcta pero el formato no respeta nuestra guía de estilo&amp;rdquo;). Cobertura mucho menor pero calidad alta.&lt;/li>
&lt;li>&lt;strong>Formularios de &amp;ldquo;¿qué falló?&amp;rdquo;&lt;/strong> cuando el thumbs-down se clica: opciones predefinidas (alucinación, formato, tono, incompleta, fuera de tema) + texto libre opcional. Permite triaging automatizado.&lt;/li>
&lt;li>&lt;strong>Re-edición&lt;/strong>: si el sistema escribe un borrador (correo, código) y el usuario lo edita antes de enviarlo, esa edición es feedback rico. Diff entre lo generado y lo enviado = señal explícita del fallo.&lt;/li>
&lt;/ul>
&lt;p>Todos los feedbacks explícitos viajan etiquetados con &lt;code>trace_id&lt;/code>, &lt;code>prompt_version&lt;/code>, &lt;code>model&lt;/code>, &lt;code>user_id&lt;/code> (anonimizado si toca), &lt;code>timestamp&lt;/code>, y entran al store de feedback. Langfuse, Phoenix y LangSmith tienen UI built-in para esto; lo importante es que &lt;strong>cada thumbs-down se materialice como una fila en una tabla&lt;/strong>, no como un evento que se pierde.&lt;/p>
&lt;h3 id="feedback-implícito">Feedback implícito&lt;/h3>
&lt;p>El usuario no te dice nada pero su comportamiento delata el problema. Señales típicas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Latencia anómala&lt;/strong>: el TTFT del sistema fue 8 s cuando la media es 800 ms. Indica overload, retrieval pesado, prefill grande inesperado. Cubierto a primer nivel 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/ebpf-on-device-inference-drift/">ebpf+drift&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Abandonment rate&lt;/strong>: el usuario abandona la sesión antes de leer la respuesta completa. Si el ratio sube de 5 % a 15 % en un segmento, algo va mal.&lt;/li>
&lt;li>&lt;strong>Retries del usuario&lt;/strong>: el usuario hace la misma pregunta (o muy similar) 2-3 veces. Indica que la primera respuesta no le sirvió.&lt;/li>
&lt;li>&lt;strong>Sesiones abortadas&lt;/strong>: el usuario cierra el chat antes de que el modelo termine de generar. En streaming, ratio elevado de aborts es indicador fuerte.&lt;/li>
&lt;li>&lt;strong>Salida del workflow&lt;/strong>: en un agente, el usuario cancela el plan antes de la ejecución. La trayectoria del agente no convenció.&lt;/li>
&lt;li>&lt;strong>Drift estadístico&lt;/strong> en distribución de inputs o outputs (KS test, PSI, embedding-space shift). Cubierto a primer nivel en &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF + drift&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Las señales implícitas son más ruidosas pero &lt;strong>cubren el 100 % del tráfico&lt;/strong>, no el 1-5 % del feedback explícito. Combinarlas con el feedback explícito da el panorama completo.&lt;/p>
&lt;h3 id="patrón-típico-de-almacenamiento">Patrón típico de almacenamiento&lt;/h3>
&lt;p>Todo el feedback —explícito e implícito— acaba en una tabla común con schema mínimo:&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">feedback_signals&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">signal_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="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">trace_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">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">request_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">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">signal_type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&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;thumbs&amp;#39;, &amp;#39;retry&amp;#39;, &amp;#39;abandon&amp;#39;, &amp;#39;drift&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">signal_value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- payload del feedback (texto del thumbs-down, latency, etc.)
&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">prompt_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&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">prompt_version&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">model&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&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">user_segment&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- tenant, plan, geo
&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">occurred_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">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">triaged&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BOOLEAN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FALSE&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">triage_label&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- llenado en sub-proceso 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&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>Postgres es más que suficiente para volúmenes razonables (millones de filas al mes). Langfuse usa Postgres por debajo. Para volúmenes altos puedes derivar a ClickHouse o BigQuery, pero rara vez merece la pena complicar.&lt;/p>
&lt;h2 id="sub-proceso-2--triage-por-causa-raíz">Sub-proceso 2 — Triage por causa raíz&lt;/h2>
&lt;p>Tener feedback no es suficiente. Hay que &lt;strong>categorizar cada incidente&lt;/strong> por su causa raíz antes de decidir qué hacer con él. Sin triage, el dataset enriquecido es un cajón desastre y el siguiente retrain no arregla nada en concreto.&lt;/p>
&lt;p>Las cuatro categorías canónicas:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Categoría&lt;/th>
&lt;th>Significa&lt;/th>
&lt;th>Acción típica&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Model issue&lt;/strong>&lt;/td>
&lt;td>El modelo respondió mal a algo que sí estaba en su capacidad teórica.&lt;/td>
&lt;td>Caso candidato a dataset enriquecido para el siguiente Tune.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Retrieval issue&lt;/strong>&lt;/td>
&lt;td>El RAG no recuperó el contexto correcto. El modelo respondió razonablemente a partir de contexto pobre.&lt;/td>
&lt;td>Ajustar reranker, chunking, indexing — etapa Data, no Tune.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Prompt issue&lt;/strong>&lt;/td>
&lt;td>El system prompt no cubre el caso o lo cubre mal.&lt;/td>
&lt;td>Nueva versión del prompt (etapa transversal de &lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">prompt versioning&lt;/a>).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Infra issue&lt;/strong>&lt;/td>
&lt;td>Latencia, timeout, error 5xx, overload.&lt;/td>
&lt;td>Ajustar capacidad / autoscaler — etapa Deploy.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El triage puede hacerse:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Manual&lt;/strong>: un humano (typically: el equipo MLE / data scientist) revisa el feedback en la UI de Langfuse / Phoenix / LangSmith, mira el trace completo, etiqueta. Coste: 2-5 min por incidente. Sostenible hasta unos 50-100 incidentes/semana por persona.&lt;/li>
&lt;li>&lt;strong>Asistido por LLM-as-classifier&lt;/strong>: un LLM clasifica el incidente en una de las cuatro categorías con un prompt estructurado. Cobertura del 80-90 % automatizada, el resto se escala a humano. Estado del arte 2026: GPT-5, Claude 4, Llama 3 70B-instruct con prompt cuidado dan F1 &amp;gt; 0.85 sobre rúbricas internas calibradas.&lt;/li>
&lt;li>&lt;strong>Reglas heurísticas para los obvios&lt;/strong>: error 5xx siempre es infra; latencia &amp;gt; 5σ siempre es infra; thumbs-down sobre RAG con &lt;code>context_relevance &amp;lt; 0.3&lt;/code> es retrieval. Captura el 30-50 % del volumen con coste cero.&lt;/li>
&lt;/ul>
&lt;p>El patrón productivo es: &lt;strong>reglas → LLM classifier → humano&lt;/strong>, en cascada, escalando sólo lo que el nivel anterior no resuelve con confianza.&lt;/p>
&lt;pre tabindex="0">&lt;code>Feedback nuevo
│
▼
[reglas heurísticas]
│
├── confianza alta → etiqueta automática
│
▼ (resto)
[LLM-as-classifier]
│
├── confianza alta → etiqueta sugerida
│
▼ (resto, o discrepancia con reglas)
[revisión humana]
│
└── etiqueta final → feedback_signals.triage_label
&lt;/code>&lt;/pre>&lt;h2 id="sub-proceso-3--dataset-enrichment">Sub-proceso 3 — Dataset enrichment&lt;/h2>
&lt;p>Una vez triajeados los incidentes con etiqueta &lt;code>model issue&lt;/code>, esos casos son candidatos a entrar al &lt;strong>dataset enriquecido&lt;/strong> que alimentará el siguiente Tune. Pero no entran tal cual: hace falta &lt;strong>la respuesta correcta etiquetada por humano&lt;/strong>.&lt;/p>
&lt;h3 id="cómo-se-construye-un-caso-enriquecido">Cómo se construye un caso enriquecido&lt;/h3>
&lt;p>Cada caso enriquecido es una tupla mínima:&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="nt">case_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enrich-2026-05-22-0142&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">source_trace_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">trace-xyz&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">prompt_input&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">system&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Eres un asistente de soporte...&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">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Cancelé mi pedido el martes pero sigo viendo el cargo&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">prompt_version_at_failure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">customer_support_v3@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">model_at_failure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-3-70b-instruct&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">failure_response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Lamento las molestias. El cargo debería revertirse en 5-7 días hábiles.&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">human_corrected_response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Lamento las molestias. He verificado tu cuenta y veo que el reembolso se procesó el miércoles. Aparecerá en tu cuenta en 24-48 h adicionales según tu banco. Aquí está el ID del reembolso: ABC123.&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">labeler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;agente_soporte_M3&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">labeled_at&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;2026-05-22T09:30:00Z&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">quality_score&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 1-5, eval por segundo humano antes de promover al dataset&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">notes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;El modelo dio respuesta genérica sin consultar el estado real del reembolso. Necesita el tool de account_lookup.&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lo importante es que el caso enriquecido tiene &lt;strong>suficiente contexto para reproducirse&lt;/strong>: prompt original, prompt version, modelo, respuesta fallada, respuesta correcta. Sin esto, el caso es un dato suelto inútil para entrenar.&lt;/p>
&lt;h3 id="herramientas-de-anotación">Herramientas de anotación&lt;/h3>
&lt;p>Tres opciones dominantes en 2026:&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://argilla.io/">Argilla&lt;/a>&lt;/strong> (OSS, mantenido por Hugging Face desde 2024). Diseñado específicamente para datasets de LLM: anotación de pares (input, output), preference data (DPO/RLHF), instruction tuning. UI Python-friendly. Integración nativa con datasets de HuggingFace y con MLflow.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://labelstud.io/">Label Studio&lt;/a>&lt;/strong> (OSS de Heartex). Más generalista, también sirve para LLM. UI rica, configurable, multi-modal. Mejor cuando el equipo ya lo usa para otras tareas.&lt;/p>
&lt;p>&lt;strong>Langfuse UI built-in&lt;/strong>. Permite anotar traces existentes directamente con thumbs + texto + categorical labels. Útil para feedback ligero; para construir datasets serios de preference o instruction tuning, Argilla y Label Studio son más adecuados.&lt;/p>
&lt;p>Patrón típico: &lt;strong>Langfuse para feedback de tráfico&lt;/strong> + &lt;strong>Argilla para construir el dataset enriquecido formal&lt;/strong> que va al pipeline de Tune. Los traces marcados como candidates en Langfuse se exportan periódicamente a Argilla, donde un humano produce la respuesta correcta y valida calidad.&lt;/p>
&lt;h3 id="validación-de-calidad-antes-de-promover">Validación de calidad antes de promover&lt;/h3>
&lt;p>No todo caso anotado entra al dataset. Una buena disciplina exige:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Doble anotación&lt;/strong> en al menos el 10-20 % de los casos críticos (dos anotadores independientes; si discrepan, un tercero resuelve).&lt;/li>
&lt;li>&lt;strong>Quality score&lt;/strong> por caso (1-5 o equivalente) — sólo casos con score ≥ 4 entran al dataset.&lt;/li>
&lt;li>&lt;strong>Versionado del dataset&lt;/strong> con &lt;a href="https://blog.lo0.es/posts/data-versioning-dvc-lakefs/">DVC + lakeFS&lt;/a> o equivalente, igual que el resto de datasets de la etapa Data.&lt;/li>
&lt;li>&lt;strong>Holdout reservado&lt;/strong>: una porción del dataset enriquecido se aparta para evaluar el adapter retraído, &lt;strong>sin que entre al training&lt;/strong>. Si el dataset se enriquece con casos donde el modelo falló y el mismo dataset se usa para evaluar, se mide memorización, no aprendizaje.&lt;/li>
&lt;/ul>
&lt;h2 id="sub-proceso-4--cadencias-scheduled-vs-incident-driven">Sub-proceso 4 — Cadencias: scheduled vs incident-driven&lt;/h2>
&lt;p>Una vez se acumula dataset enriquecido, queda decidir &lt;strong>cuándo se lanza el retrain&lt;/strong>. Hay dos cadencias complementarias.&lt;/p>
&lt;h3 id="scheduled-retrain-trimestral-por-defecto">Scheduled retrain (trimestral por defecto)&lt;/h3>
&lt;p>Un proceso establecido en el calendario. Cada trimestre, en una semana específica, el equipo:&lt;/p>
&lt;ol>
&lt;li>Cierra el ciclo de captura de feedback acumulado.&lt;/li>
&lt;li>Cuenta los casos enriquecidos disponibles (típicamente decenas a cientos por trimestre).&lt;/li>
&lt;li>Lanza el pipeline de fine-tuning con el dataset agregado (golden dataset + casos enriquecidos del trimestre).&lt;/li>
&lt;li>Evalúa el candidato contra suite completa + holdout enriquecido.&lt;/li>
&lt;li>Promociona si pasa eval gates.&lt;/li>
&lt;/ol>
&lt;p>Ventajas: capacity planning predecible, presupuesto cerrado, riesgo controlado, equipo no quemado. El default.&lt;/p>
&lt;h3 id="incident-driven-retrain">Incident-driven retrain&lt;/h3>
&lt;p>Cuando un incidente serio supera threshold, se dispara un mini-ciclo fuera de cadencia. Triggers típicos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Drift detectado&lt;/strong> en distribución de inputs/outputs sobre threshold (KS p-value &amp;lt; 0.01, PSI &amp;gt; 0.25, embedding-space shift &amp;gt; 2σ).&lt;/li>
&lt;li>&lt;strong>Segmento que falla&lt;/strong>: un cluster de usuarios o un tipo de pregunta muestra tasa de error 3× sobre baseline durante &amp;gt; 48 h.&lt;/li>
&lt;li>&lt;strong>Ataque de prompt injection o jailbreak&lt;/strong> con éxito que supera severity threshold (cubierto en &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">guardrails&lt;/a>).&lt;/li>
&lt;li>&lt;strong>Cambio de dominio externo&lt;/strong>: el cliente cambia política, sale una nueva regulación, etc. El modelo entrenado contra la versión vieja deja de ser válido.&lt;/li>
&lt;/ul>
&lt;p>Mini-ciclo típico: feedback de los últimos 7-14 días, dataset focalizado en el segmento problemático, fine-tuning rápido sobre el adapter existente (no full retrain), eval gate específico al segmento, despliegue canary, promoción si pasa.&lt;/p>
&lt;p>Coste: ~3-7 días de trabajo del equipo según severidad. &lt;strong>No es opcional para casos críticos&lt;/strong>: si el segmento que falla es regulatorio o reputacional, el coste de no responder rápido es mucho mayor que el del mini-ciclo.&lt;/p>
&lt;h3 id="anti-patrón-ya-retrenamos-cuando-haga-falta">Anti-patrón: &amp;ldquo;ya retrenamos cuando haga falta&amp;rdquo;&lt;/h3>
&lt;p>La frase más letal en LLMOps. Sin calendarización explícita, el scheduled nunca llega; sin thresholds explícitos, el incident-driven tampoco se dispara. El sistema acumula deuda silenciosa hasta que un incidente catastrófico fuerza el retrain ya tarde.&lt;/p>
&lt;p>La disciplina mínima: &lt;strong>fecha en calendario para el próximo scheduled + 3-5 thresholds de incident-driven explícitos por escrito&lt;/strong>. Sin esto, la etapa Retrain es teatro.&lt;/p>
&lt;h2 id="sub-proceso-5--promotion-el-candidato-entra-a-producción">Sub-proceso 5 — Promotion: el candidato entra a producción&lt;/h2>
&lt;p>Una vez el adapter candidato existe, no entra a producción directamente. Pasa por &lt;strong>el mismo flow que cualquier release&lt;/strong>: Tune → Eval → Deploy con gates.&lt;/p>
&lt;pre tabindex="0">&lt;code>Adapter candidato (de Tune)
│
▼
[Eval suite completa]
- golden dataset histórico
- holdout enriquecido del trimestre
- regression vs producción
│
pasa? → no → bloqueo + alerta
│
sí
▼
[Eval gate de no-regresión]
- asegurar que no degrada
segmentos que ya funcionaban
│
pasa? → no → bloqueo + alerta
│
sí
▼
[Despliegue canary]
- 5-10% del tráfico al adapter
nuevo durante 24-72 h
- métricas online vs producción
│
métricas OK? → no → rollback
│
sí
▼
[Promotion full]
- mover label en model registry
- MLflow stages: Staging → Production
- El anterior pasa a Archived (preserva
reproducibilidad histórica)
&lt;/code>&lt;/pre>&lt;p>Las herramientas del registry:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MLflow Model Registry stages&lt;/strong> (Staging, Production, Archived) es el patrón canónico. La promotion es una llamada API: &lt;code>mlflow.models.transition_stage(name, version, &amp;quot;Production&amp;quot;)&lt;/code>. Auditado, revertible.&lt;/li>
&lt;li>&lt;strong>Hugging Face Hub privado&lt;/strong> con repo per adapter es el equivalente &amp;ldquo;Git for models&amp;rdquo; — versionado por commit hash, branches para staging/production, deploy via PR.&lt;/li>
&lt;li>&lt;strong>vLLM multi-LoRA hot-swap&lt;/strong> (descrito en &lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">fine-tuning continuo&lt;/a>) carga el adapter nuevo sin reiniciar el servidor — la promotion física dura segundos.&lt;/li>
&lt;/ul>
&lt;h2 id="aplicado-a-hardware-on-premise-típico">Aplicado a hardware on-premise típico&lt;/h2>
&lt;p>Retrain como etapa &lt;strong>no necesita hardware grande&lt;/strong>. El cálculo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Feedback collection&lt;/strong>: una pequeña tabla en Postgres. Trivial en cualquier nodo.&lt;/li>
&lt;li>&lt;strong>Triage manual / asistido&lt;/strong>: el LLM-as-classifier corre en el mismo motor de inferencia que sirve producción, en horas de baja demanda, con prioridad spot. Decenas de miles de incidentes al mes consumen del orden de minutos de GPU por día.&lt;/li>
&lt;li>&lt;strong>Dataset enrichment&lt;/strong>: anotación humana, sin coste GPU. Storage despreciable.&lt;/li>
&lt;li>&lt;strong>Tune (mini-ciclo o trimestral)&lt;/strong>: aquí sí hay coste. Fine-tuning de un adapter LoRA sobre Llama 3 70B con un dataset de pocos miles de ejemplos cuesta del orden de 2-8 horas en una H100 single. Sobre 4 H100 con tensor parallel: 30-90 min. Cabe holgadamente en cualquier ventana nocturna de baja demanda.&lt;/li>
&lt;li>&lt;strong>Eval suite completa&lt;/strong>: minutos en un motor con prefix caching activo (cubierto en &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">pagedattention deep-dive&lt;/a>).&lt;/li>
&lt;li>&lt;strong>Despliegue canary&lt;/strong>: cero coste adicional — el adapter nuevo convive en el mismo motor vía multi-LoRA hot-swap.&lt;/li>
&lt;/ul>
&lt;p>Para una &lt;strong>RTX 4090&lt;/strong> sirviendo Llama 3 8B con equipo pequeño: scheduled retrain mensual o trimestral en una noche, dataset enriquecido con 50-100 casos por ciclo, anotación con Argilla autohospedado en el mismo nodo. Bastante.&lt;/p>
&lt;p>Para un &lt;strong>cluster 4×H100 SXM&lt;/strong> sirviendo a varios tenants: dataset enriquecido segregado por tenant (cada uno con su propio holdout y eval suite), pipeline de retrain orquestado con Argo Workflows o equivalente, MLflow registry centralizado, multi-LoRA hot-swap por tenant.&lt;/p>
&lt;h2 id="trampas-operativas-comunes">Trampas operativas comunes&lt;/h2>
&lt;p>&lt;strong>El bucle abierto.&lt;/strong> El sistema captura feedback, lo guarda en una tabla, y ahí muere. Nadie triajea, nadie enriquece, nadie retrena. El modelo deployed envejece silenciosamente. &lt;strong>Solución&lt;/strong>: SLO interno explícito (por ejemplo, &amp;ldquo;todo feedback &amp;gt;1 semana sin triajear se reporta en standup&amp;rdquo;), dueño asignado.&lt;/p>
&lt;p>&lt;strong>Feedback humano que se pierde.&lt;/strong> Thumbs-down sin captura estructurada (el evento se loggea pero el motivo no), o el motivo se loggea pero nadie lo indexa para queries. &lt;strong>Solución&lt;/strong>: schema explícito como el de arriba, dashboard semanal de &amp;ldquo;top motivos de thumbs-down&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Cadence sin definir.&lt;/strong> &amp;ldquo;Ya retrenamos cuando haga falta&amp;rdquo; — nunca. &lt;strong>Solución&lt;/strong>: fecha en calendario + 3-5 thresholds escritos.&lt;/p>
&lt;p>&lt;strong>Sin holdout test set.&lt;/strong> El dataset enriquecido se mezcla con el golden dataset para entrenar Y para evaluar. El adapter parece haber mejorado porque &amp;ldquo;memorizó&amp;rdquo; los casos enriquecidos, pero generaliza mal a nuevos casos similares. &lt;strong>Solución&lt;/strong>: holdout reservado &lt;strong>antes&lt;/strong> de entrenar, eval contra holdout es la métrica que decide promotion.&lt;/p>
&lt;p>&lt;strong>Triage ad-hoc por persona.&lt;/strong> El data scientist senior triajea cuando puede; en vacaciones se acumula; vuelve y abandona porque hay 400 incidentes esperando. &lt;strong>Solución&lt;/strong>: automatizar con LLM-as-classifier el 70-80 %, dejar humano sólo lo difícil; rotar el &amp;ldquo;oncall de triage&amp;rdquo; para no saturar a una persona.&lt;/p>
&lt;p>&lt;strong>Promotion sin canary.&lt;/strong> El adapter pasa eval offline y se despliega al 100 % directamente. Una regresión en producción tarda en detectarse hasta que las métricas online lo evidencian — para entonces el daño está hecho. &lt;strong>Solución&lt;/strong>: canary 5-10 % durante 24-72 h obligatorio.&lt;/p>
&lt;p>&lt;strong>Sin reproducibilidad del incidente original.&lt;/strong> El equipo va a investigar por qué el modelo falló en el incidente del 22 de mayo y descubre que el prompt era distinto (se cambió hace dos semanas), el modelo también, y los logs no guardaron el contexto RAG. &lt;strong>Solución&lt;/strong>: trazabilidad fuerte (cubierta en &lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">prompt versioning&lt;/a> y &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>). Sin reproducibilidad, retrain es adivinanza.&lt;/p>
&lt;p>&lt;strong>El dataset enriquecido contamina los datos de Eval.&lt;/strong> El equipo confunde &amp;ldquo;casos donde falló&amp;rdquo; (que entran al training enriquecido) con &amp;ldquo;golden dataset de regresión&amp;rdquo; (que tiene que permanecer estable para detectar drift). Mezclarlos invalida el eval. &lt;strong>Solución&lt;/strong>: dos datasets distintos, dos rutas distintas.&lt;/p>
&lt;h2 id="patrón-operativo-recomendado-el-ciclo-trimestral-en-una-pantalla">Patrón operativo recomendado: el ciclo trimestral en una pantalla&lt;/h2>
&lt;p>Un equipo serio con Retrain bien implementado tiene este flujo cada 3 meses:&lt;/p>
&lt;p>&lt;strong>Semana 1 (cierre de ciclo)&lt;/strong>: bloqueo de captura nueva para el ciclo, snapshot de feedback acumulado. Reporte automatizado: cuántos thumbs-down, cuántos incidentes triajeados, distribución por categoría, top patrones.&lt;/p>
&lt;p>&lt;strong>Semana 2 (triage y anotación)&lt;/strong>: el equipo MLE+anotadores procesa los casos &lt;code>model issue&lt;/code> no triajeados. Anotación humana en Argilla. Validación cruzada en muestras.&lt;/p>
&lt;p>&lt;strong>Semana 3 (training y eval)&lt;/strong>: pipeline lanzado con dataset = golden + enriquecido_de_este_trimestre - holdout. Fine-tuning del adapter en una noche. Eval contra suite completa + holdout. Si pasa gates, candidato &lt;code>v_new&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Semana 4 (canary y promotion)&lt;/strong>: deploy del candidato como adapter alternativo en vLLM, routing del 5-10 % del tráfico al candidato durante 48-72 h. Métricas online: latencia, tasa de queja, eval implícito en producción. Si todo OK, promotion full; si no, rollback y análisis.&lt;/p>
&lt;p>&lt;strong>Semana 5+ (siguiente ciclo)&lt;/strong>: el adapter &lt;code>v_new&lt;/code> ahora es &lt;code>production&lt;/code>. Empieza la captura de feedback del próximo trimestre. El anterior &lt;code>v_old&lt;/code> pasa a &lt;code>Archived&lt;/code> pero queda accesible para reproducibilidad histórica.&lt;/p>
&lt;p>Trimestralmente, ese ciclo más los mini-ciclos incident-driven que aparezcan en medio. Operacional, predecible, auditable.&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>Online DPO y aprendizaje continuo on-policy&lt;/strong>: cómo se acorta el ciclo a horas o días (Fast-Slow Chasing, RLOO iterativo). Estado del arte 2026 — todavía emergente en producción.&lt;/li>
&lt;li>&lt;strong>Machine unlearning para GDPR&lt;/strong>: cuando un usuario ejerce derecho al olvido y sus interacciones formaron parte del dataset enriquecido de un adapter en producción. Negative LoRA, retrain selectivo.&lt;/li>
&lt;li>&lt;strong>Constitutional AI runtime&lt;/strong>: alignment continuo que sustituye o complementa retrain periódico.&lt;/li>
&lt;li>&lt;strong>Eval gates con metamorphic testing&lt;/strong>: evaluación de robustez frente a perturbaciones del input (typos, paraphrasing, idioma) como parte del gate de promotion.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro donde Retrain es la etapa 6. Este post entra al detalle de esa caja.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — la mecánica de Tune que ejecuta el adapter nuevo del ciclo descrito aquí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals: la capa después del tracing&lt;/a> — las suites de eval que sirven de gate en el sub-proceso 5 de promotion.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">Prompt versioning con Langfuse y MLflow Prompts&lt;/a> — el componente transversal que asegura reproducibilidad del incidente original cuando se va a triajear.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/data-versioning-dvc-lakefs/">Data versioning para LLMOps: DVC, lakeFS y golden dataset reproducible&lt;/a> — el sub-proceso 3 de Retrain enriquece un dataset; este post entra al detalle de cómo versionarlo, su schema y su lineage.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF en inferencia local y detección estadística de drift&lt;/a> — las señales de drift que disparan el incident-driven retrain.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails y safety en LLMs&lt;/a> — los incidentes de safety / jailbreak que también disparan incident-driven retrain.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP por dentro y su observabilidad profunda&lt;/a> — el tracing OTel &lt;code>gen_ai.*&lt;/code> que liga cada feedback con su trace completo, condición necesaria para triagear bien.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Argilla documentation, &lt;em>Building Datasets for LLM Fine-Tuning&lt;/em>: &lt;a href="https://argilla.io/docs">https://argilla.io/docs&lt;/a>.&lt;/li>
&lt;li>Label Studio documentation, &lt;em>LLM Annotation&lt;/em>: &lt;a href="https://labelstud.io/templates/llm">https://labelstud.io/templates/llm&lt;/a>.&lt;/li>
&lt;li>Langfuse documentation, &lt;em>User Feedback and Dataset Management&lt;/em>: &lt;a href="https://langfuse.com/docs/scores/user-feedback">https://langfuse.com/docs/scores/user-feedback&lt;/a>.&lt;/li>
&lt;li>MLflow Model Registry stages: &lt;a href="https://mlflow.org/docs/latest/model-registry.html">https://mlflow.org/docs/latest/model-registry.html&lt;/a>.&lt;/li>
&lt;li>Ethayarajh et al., &lt;em>KTO: Model Alignment as Prospect Theoretic Optimization&lt;/em> (2024) — referencia para el ciclo de feedback como señal de alineamiento.&lt;/li>
&lt;li>Google Cloud, &lt;em>Continuous Training and MLOps for GenAI&lt;/em> (2025).&lt;/li>
&lt;li>DataRobot, &lt;em>MLOps Best Practices: Closing the Loop&lt;/em> (2025).&lt;/li>
&lt;li>Eugene Yan, &lt;em>Feedback Loops in LLM Systems&lt;/em> (blog, 2025).&lt;/li>
&lt;/ul></description></item><item><title>Fine-tuning continuo en producción: del tráfico real al adapter desplegado</title><link>https://blog.lo0.es/posts/fine-tuning-continuo-produccion/</link><pubDate>Thu, 21 May 2026 10:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/fine-tuning-continuo-produccion/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Fine-tuning continuo no es &amp;ldquo;entrenar el modelo cada cierto tiempo&amp;rdquo;. Es un &lt;strong>ciclo cerrado&lt;/strong> donde el tráfico real de producción genera los datasets, un pipeline corto entrena un adapter LoRA, una batería de evaluaciones decide si promociona, y vLLM lo carga &lt;strong>sin reiniciar&lt;/strong>. El estado del arte en mayo de 2026 ha fragmentado el stack: ya no es DPO contra todo, sino una elección entre SFT, DPO, KTO, ORPO y SimPO según el tipo de señal que captura tu producto. Lo que ha consolidado el patrón es la combinación PostgreSQL 18 + pgvector 0.8 como &lt;strong>sistema nervioso del pipeline&lt;/strong> —captura de tráfico, dataset versioning, eval results, registry de adapters—, junto a vLLM multi-LoRA hot-swap que convierte el despliegue en una llamada HTTP. Este artículo desmonta el ciclo con esquemas concretos, queries reales, y los números que cuestan en una RTX 4090 frente a un cluster 4×H100.&lt;/p>
&lt;h2 id="estás-aquí-tune--retrain">Estás aquí: Tune + Retrain&lt;/h2>
&lt;p>Este post cruza dos etapas del &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">pipeline LLMOps de seis etapas&lt;/a>: la decisión de &lt;strong>entrenar un adapter nuevo&lt;/strong> (etapa &lt;strong>Tune&lt;/strong>) está disparada por las señales de &lt;strong>Observe&lt;/strong> que viajan por la etapa &lt;strong>Retrain&lt;/strong> hasta cerrar el bucle. El post desmonta el circuito completo entre las dos cajas.&lt;/p>
&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 + Retrain">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.active2{fill:#fff5b0;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(#ftm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#ftm)}&lt;/style>
&lt;defs>&lt;marker id="ftm" 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 + RETRAIN · ciclo continuo de adapters disparado por el tráfico real&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 active2"/>&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-analogía-el-restaurante-que-afina-su-carta">La analogía: el restaurante que afina su carta&lt;/h2>
&lt;p>Imagina un restaurante de barrio con un plato estrella que funciona, pero el chef sabe que se puede afinar. Cada noche pasan cosas:&lt;/p>
&lt;ul>
&lt;li>Algunos comensales &lt;strong>dejan parte del plato&lt;/strong>: señal débil de que algo no acabó de encajar.&lt;/li>
&lt;li>Otros piden &lt;strong>otra versión&lt;/strong> (&amp;quot;¿podrías ponerle menos sal?&amp;quot;): señal explícita y direccional.&lt;/li>
&lt;li>Otros &lt;strong>terminan el plato y vuelven la semana siguiente&lt;/strong>: la única señal que de verdad importa, pero llega tarde.&lt;/li>
&lt;li>Y un grupo selecto opina sin que se les pregunte, normalmente para mal.&lt;/li>
&lt;/ul>
&lt;p>El chef no rehace su carta cada noche. Hace algo más interesante: anota en una libreta los platos servidos, las devoluciones, los cambios pedidos, las propinas. Cada cierto tiempo, &lt;strong>lee la libreta entera&lt;/strong>, decide ajustes mínimos en una receta, prueba la nueva versión en mesa privada con su personal, y solo si la prueban favorablemente la incorpora a la carta del día siguiente. A veces incluso sirve dos versiones distintas del plato a distintas mesas durante una semana, mide qué pasa, y elige.&lt;/p>
&lt;p>Eso es &lt;strong>fine-tuning continuo&lt;/strong>. La libreta es Postgres. El plato es el modelo base. Las anotaciones son señales de feedback —explícitas y implícitas—. El &amp;ldquo;ajuste mínimo&amp;rdquo; es un LoRA adapter de 30 MB. La mesa privada es la batería de evaluaciones automatizadas. La carta del día siguiente es vLLM con multi-LoRA hot-swap, que carga el nuevo adapter sin reiniciar el servicio. El servir dos versiones a distintas mesas es A/B testing con tráfico real.&lt;/p>
&lt;p>La analogía es exacta en un punto crítico: &lt;strong>el chef no tira la receta original&lt;/strong>. Mantiene la receta base y guarda una libreta separada con las &amp;ldquo;modificaciones que dan buen resultado para los habituales del barrio&amp;rdquo;. Esa libreta es el adapter LoRA: encima del modelo base, no en su lugar.&lt;/p>
&lt;h2 id="el-ciclo-desmontado">El ciclo, desmontado&lt;/h2>
&lt;p>Antes de entrar en componentes, conviene fijar el flujo completo. Estos siete pasos son lo que cualquier equipo serio replica con variaciones:&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Ciclo cerrado de fine-tuning continuo">
&lt;style>
.node { fill: #f4f4f4; stroke: #333; stroke-width: 1.5; }
.n-serve { fill: #d6eaff; }
.n-data { fill: #ffe9d6; }
.n-train { fill: #d9f5d6; }
.n-eval { fill: #f4e1ff; }
.lbl { font: 600 12px sans-serif; fill: #222; }
.lbl-sm { font: 10.5px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.4; fill: none; marker-end: url(#ah2); }
.arr-dim { stroke: #888; stroke-width: 1.2; fill: none; stroke-dasharray: 4,3; marker-end: url(#ah2); }
&lt;/style>
&lt;defs>
&lt;marker id="ah2" 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="360" y="22" text-anchor="middle" class="lbl">El ciclo cerrado de fine-tuning continuo&lt;/text>
&lt;rect x="280" y="40" width="160" height="50" rx="8" class="node n-serve"/>
&lt;text x="360" y="62" text-anchor="middle" class="lbl">1 · vLLM serving&lt;/text>
&lt;text x="360" y="78" text-anchor="middle" class="lbl-sm">base + adapters activos&lt;/text>
&lt;rect x="500" y="120" width="170" height="50" rx="8" class="node n-data"/>
&lt;text x="585" y="142" text-anchor="middle" class="lbl">2 · Captura tráfico&lt;/text>
&lt;text x="585" y="158" text-anchor="middle" class="lbl-sm">prompts, respuestas, feedback&lt;/text>
&lt;rect x="500" y="210" width="170" height="50" rx="8" class="node n-data"/>
&lt;text x="585" y="232" text-anchor="middle" class="lbl">3 · Curación&lt;/text>
&lt;text x="585" y="248" text-anchor="middle" class="lbl-sm">dedup, PII, balanceo, snapshot&lt;/text>
&lt;rect x="280" y="290" width="160" height="50" rx="8" class="node n-train"/>
&lt;text x="360" y="312" text-anchor="middle" class="lbl">4 · Training LoRA&lt;/text>
&lt;text x="360" y="328" text-anchor="middle" class="lbl-sm">SFT / DPO / KTO / ORPO / SimPO&lt;/text>
&lt;rect x="50" y="210" width="170" height="50" rx="8" class="node n-eval"/>
&lt;text x="135" y="232" text-anchor="middle" class="lbl">5 · Eval gates&lt;/text>
&lt;text x="135" y="248" text-anchor="middle" class="lbl-sm">3 etapas: PR, full, canary&lt;/text>
&lt;rect x="50" y="120" width="170" height="50" rx="8" class="node n-train"/>
&lt;text x="135" y="142" text-anchor="middle" class="lbl">6 · Adapter registry&lt;/text>
&lt;text x="135" y="158" text-anchor="middle" class="lbl-sm">status: canary | prod | retired&lt;/text>
&lt;rect x="50" y="40" width="170" height="50" rx="8" class="node n-serve"/>
&lt;text x="135" y="62" text-anchor="middle" class="lbl">7 · Hot-swap&lt;/text>
&lt;text x="135" y="78" text-anchor="middle" class="lbl-sm">POST /v1/load_lora_adapter&lt;/text>
&lt;path class="arr" d="M440,75 C480,80 495,100 510,120"/>
&lt;path class="arr" d="M585,170 L585,210"/>
&lt;path class="arr" d="M500,250 C460,270 440,280 440,300"/>
&lt;path class="arr" d="M280,315 C240,290 230,275 220,260"/>
&lt;path class="arr" d="M135,210 L135,170"/>
&lt;path class="arr" d="M135,120 L135,90"/>
&lt;path class="arr" d="M220,65 L280,65"/>
&lt;rect x="280" y="170" width="160" height="80" rx="10" fill="#fffae6" stroke="#d4a52a" stroke-width="2"/>
&lt;text x="360" y="200" text-anchor="middle" class="lbl">PostgreSQL 18&lt;/text>
&lt;text x="360" y="218" text-anchor="middle" class="lbl-sm">+ pgvector 0.8&lt;/text>
&lt;text x="360" y="234" text-anchor="middle" class="lbl-sm">single source of truth&lt;/text>
&lt;path class="arr-dim" d="M500,145 L440,180"/>
&lt;path class="arr-dim" d="M500,235 L440,225"/>
&lt;path class="arr-dim" d="M280,225 L220,235"/>
&lt;path class="arr-dim" d="M280,200 L220,160"/>
&lt;path class="arr-dim" d="M360,290 L360,250"/>
&lt;/svg>
&lt;/div>
&lt;p>El ciclo dura entre 1 y 4 semanas en producción real. Lo que cambia entre equipos es el ritmo (más rápido en chat asistente, más lento en banca regulada) y los detalles de cada paso. La estructura es la misma.&lt;/p>
&lt;h2 id="por-qué-fine-tuning-continuo-y-por-qué-no-es-rag">Por qué fine-tuning continuo (y por qué no es RAG)&lt;/h2>
&lt;p>Antes de profundizar, una distinción que se sigue confundiendo. &lt;strong>Fine-tuning sirve para forma, no para hechos.&lt;/strong> Si tu problema es que el modelo no conoce las tarifas del cliente o el catálogo actualizado, no fine-tunees: usa RAG. Si tu problema es que el modelo responde con un tono que no encaja, no respeta tu formato JSON, rechaza casos legítimos o se inventa estructura, ahí sí es fine-tuning.&lt;/p>
&lt;p>En 2026 el límite ya está bien establecido por la práctica de la comunidad:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Problema observado&lt;/th>
&lt;th>Solución&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>El modelo no sabe X (X cambia semanalmente)&lt;/td>
&lt;td>RAG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo conoce X pero responde mal de tono o formato&lt;/td>
&lt;td>Fine-tuning SFT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hay dos formas de responder y prefiero una sobre otra&lt;/td>
&lt;td>Fine-tuning con preferencias (DPO/KTO/ORPO/SimPO)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo razona mal en un dominio verificable (código, mates)&lt;/td>
&lt;td>RL con recompensa verificable (GRPO/DAPO)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>El modelo es competente, solo necesita memoria de hechos&lt;/td>
&lt;td>RAG, no fine-tuning&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Fine-tuning continuo es la versión disciplinada del segundo y tercer caso. La palabra clave es &lt;strong>continuo&lt;/strong>: no es un evento puntual de &amp;ldquo;alineamos el modelo&amp;rdquo;, es un proceso que toca cada vez que la distribución del tráfico se desvía lo suficiente, o que aparecen nuevos casos de uso.&lt;/p>
&lt;h2 id="las-cuatro-técnicas-según-la-señal-que-captures">Las cuatro técnicas según la señal que captures&lt;/h2>
&lt;p>El cambio más importante de los últimos 12 meses ha sido el fin del monopolio de DPO. En 2024 todo equipo que hacía alineamiento usaba DPO con pares &lt;code>(chosen, rejected)&lt;/code>. En 2026 la elección es más fina y depende de &lt;strong>cómo es la señal que recoges en tu producto&lt;/strong>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Señal real en producto&lt;/th>
&lt;th>Técnica recomendada&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Ejemplos correctos etiquetados (input → output esperado)&lt;/td>
&lt;td>&lt;strong>SFT + LoRA&lt;/strong>&lt;/td>
&lt;td>Sigue siendo la base. 500-5.000 ejemplos bastan para estilo.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pares explícitos &lt;code>(chosen, rejected)&lt;/code>&lt;/td>
&lt;td>&lt;strong>DPO&lt;/strong> o &lt;strong>SimPO&lt;/strong>&lt;/td>
&lt;td>SimPO elimina el modelo de referencia → 50 % menos VRAM en entrenamiento.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>👍 / 👎 sueltos sobre respuestas&lt;/td>
&lt;td>&lt;strong>KTO&lt;/strong>&lt;/td>
&lt;td>El método que más naturalmente encaja con la telemetría real.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SFT y preferencias en una sola pasada&lt;/td>
&lt;td>&lt;strong>ORPO&lt;/strong>&lt;/td>
&lt;td>Un solo modelo en memoria, evita el drift entre fases.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Recompensa verificable (tests, soluciones)&lt;/td>
&lt;td>&lt;strong>GRPO&lt;/strong> / &lt;strong>DAPO&lt;/strong>&lt;/td>
&lt;td>Razonamiento, no chat. Otro mundo.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La regla práctica: &lt;strong>diseña la captura de feedback en producto pensando en qué método podrás usar después&lt;/strong>. Si tu UI sólo tiene 👍/👎, fuerzas el camino a KTO. Si añades un botón &amp;ldquo;regenerar respuesta&amp;rdquo;, desbloqueas DPO desde el regenerate-as-rejected (lo veremos abajo). Si añades un botón &amp;ldquo;editar respuesta&amp;rdquo;, la respuesta editada se convierte en SFT directo de alta calidad.&lt;/p>
&lt;p>Hay un detalle de coste que se publicita poco. DPO necesita mantener en memoria &lt;strong>dos modelos&lt;/strong>: el que entrenas y el de referencia. SimPO elimina ese segundo modelo. ORPO también. Para un Llama 3 8B en BF16 esto es la diferencia entre necesitar ~32 GB de VRAM activos durante entrenamiento (DPO) o ~16 GB (SimPO/ORPO). Es la diferencia entre que el entrenamiento quepa en una RTX 4090 con QLoRA agresivo, o no quepa sin offload.&lt;/p>
&lt;h2 id="postgres-como-sistema-nervioso-del-pipeline">Postgres como sistema nervioso del pipeline&lt;/h2>
&lt;p>Aquí está la opinión técnica fuerte de este artículo, y es la que conviene defender con datos: &lt;strong>Postgres 18 + pgvector 0.8 + un bucket S3/MinIO para los pesos es suficiente para todo el pipeline&lt;/strong>. No hace falta MLflow, no hace falta lakeFS, no hace falta DVC.&lt;/p>
&lt;p>No se trata de minimalismo ideológico. Se trata de tres ventajas concretas que ningún stack alternativo iguala en el escenario on-premise con compliance:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Una sola fuente de verdad, un solo modelo de autorización.&lt;/strong> Las ACL que ya tienes para Postgres cubren los datos de entrenamiento, los resultados de eval, el registry de adapters y el log de auditoría. No multiplicas planos de control.&lt;/li>
&lt;li>&lt;strong>SQL como lenguaje universal del pipeline.&lt;/strong> El query que genera el dataset, el predicado del eval gate, la asignación de tráfico A/B, la decisión de promoción: todo es SQL. Tu equipo ya sabe SQL.&lt;/li>
&lt;li>&lt;strong>Audit y reproducibilidad criptográfica gratis.&lt;/strong> Las extensiones &lt;code>pg_audit&lt;/code> y &lt;code>pgcrypto&lt;/code>, combinadas con &lt;code>set_hash&lt;/code> sobre el dataset, te dan trazabilidad criptográfica sin código adicional. Es un terreno que da para artículo propio.&lt;/li>
&lt;/ol>
&lt;h3 id="esquema-concreto">Esquema concreto&lt;/h3>
&lt;p>Empezamos por la tabla de tráfico, particionada por semanas para que el &lt;code>DROP PARTITION&lt;/code> sea barato:&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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&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">BIGSERIAL&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">request_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">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">tenant_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&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">user_hash&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BYTEA&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- pseudonimización GDPR
&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">adapter_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">-- ej. &amp;#34;support-es-v4.1&amp;#34;
&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">experiment&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 class="c1">-- ej. &amp;#34;rerank-v2-canary&amp;#34;
&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">variant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">CHAR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;A&amp;#39; | &amp;#39;B&amp;#39; | NULL
&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">messages&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="k">completion&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">ttft_ms&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">tokens_in&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">tokens_out&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="c1">-- señales de feedback
&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">fb_explicit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SMALLINT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- -1/0/+1 (KTO-ready)
&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">fb_regen&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BOOLEAN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- usuario regeneró → DPO-rejected
&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">fb_edited&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BOOLEAN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">false&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- usuario editó → SFT golden
&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">parent_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BIGINT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- autoreferencia regenerate
&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="c1">-- vector y meta
&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">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HALFVEC&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1024&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- pgvector 0.8, mitad de RAM
&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">pii_flags&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SMALLINT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- bitmask
&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">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 class="n">PARTITION&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">RANGE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">created_at&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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log_2026w21&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&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">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2026-05-18&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2026-05-25&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>&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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log_2026w21&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">halfvec_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">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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log_2026w21&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="n">tenant_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&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>Tres decisiones merecen una nota:&lt;/p>
&lt;p>&lt;strong>&lt;code>HALFVEC(1024)&lt;/code>.&lt;/strong> Vectores en FP16 nativos de pgvector 0.8. La mitad de RAM y disco con pérdida de precisión irrelevante para deduplicación semántica. Esto solo, a escala de millones de filas, ahorra entre 4 y 8 GB.&lt;/p>
&lt;p>&lt;strong>Particionado semanal por rango temporal.&lt;/strong> A los 90 días, &lt;code>DROP TABLE obs.inference_log_2026wXX&lt;/code> libera espacio en milisegundos sin bloqueo prolongado. Autovacuum nunca vuelve a tocar particiones congeladas.&lt;/p>
&lt;p>&lt;strong>&lt;code>parent_id&lt;/code> autoreferenciado.&lt;/strong> El usuario regenera la respuesta → se inserta una nueva fila con &lt;code>parent_id&lt;/code> apuntando a la anterior. Eso nos dará un dataset DPO sin tocar la UX.&lt;/p>
&lt;h3 id="el-registry-de-adapters">El registry de adapters&lt;/h3>
&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">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter&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="nb">TEXT&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="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">base_model&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">rank&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 class="n">alpha&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">target_modules&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&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">method&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 class="c1">-- &amp;#39;sft&amp;#39;|&amp;#39;dpo&amp;#39;|&amp;#39;kto&amp;#39;|&amp;#39;orpo&amp;#39;|&amp;#39;simpo&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">training_run_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">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="n">dataset_snapshot_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">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="n">weights_uri&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 class="c1">-- s3://.../v4.2.safetensors
&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">eval_summary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&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">status&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;training&amp;#39;|&amp;#39;canary&amp;#39;|&amp;#39;prod&amp;#39;|&amp;#39;retired&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">traffic_pct&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">NUMERIC&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&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">promoted_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMPTZ&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>El router de vLLM lee esta tabla con TTL de pocos segundos. Un &lt;code>UPDATE serve.adapter SET status='prod', traffic_pct=100 WHERE id='v4.2'&lt;/code> es una promoción. Un &lt;code>UPDATE ... SET status='retired'&lt;/code> es un rollback. La auditoría de quién hizo qué y cuándo la da &lt;code>pg_audit&lt;/code> sin escribir una línea de código adicional.&lt;/p>
&lt;h2 id="generar-datasets-dpo-y-kto-desde-tráfico-real">Generar datasets DPO y KTO desde tráfico real&lt;/h2>
&lt;p>Aquí es donde la elegancia del esquema paga. El dataset no es un fichero estático: es una &lt;strong>vista materializada&lt;/strong> que se construye con SQL sobre &lt;code>obs.inference_log&lt;/code>.&lt;/p>
&lt;h3 id="dataset-kto-desde-">Dataset KTO desde 👍/👎&lt;/h3>
&lt;p>KTO es el método que mejor encaja con la señal que captura cualquier producto de chat decente. La query:&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="n">MATERIALIZED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VIEW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kto_v3_candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&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">messages&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">prompt&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">completion&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">response&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">CASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_explicit&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="w"> &lt;/span>&lt;span class="k">THEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ELSE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">false&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&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">label&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">adapter_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&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">fb_explicit&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&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">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&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="n">now&lt;/span>&lt;span class="p">()&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;60 days&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pii_flags&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&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">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tenant_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tenant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">consent_training&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>Simple. Cada fila con feedback explícito se convierte en un ejemplo &lt;code>(prompt, response, deseable_sí_no)&lt;/code>. KTO entrena directamente sobre esta señal, sin necesidad de construir pares.&lt;/p>
&lt;h3 id="dataset-dpo-desde-regenerar">Dataset DPO desde &amp;ldquo;regenerar&amp;rdquo;&lt;/h3>
&lt;p>El truco que vale por sí solo este artículo. Cuando el usuario pulsa &amp;ldquo;regenerar respuesta&amp;rdquo;, está dando una señal extraordinariamente fuerte: la primera respuesta no le valió. Si la segunda no se regenera ni se valora negativamente, asumimos que sí. Eso es un par DPO sin un solo clic adicional en la UI:&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="n">MATERIALIZED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VIEW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dpo_v3_candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&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">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">messages&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">prompt&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">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">completion&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">chosen&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">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">completion&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">rejected&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">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter_id&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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&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">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cho&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">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">parent_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&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">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">fb_regen&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">fb_explicit&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="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">-- mitigación de length bias en DPO clásico
&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cho&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tokens_out&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BETWEEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tokens_out&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&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">5&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">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rej&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tokens_out&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">5&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>La cláusula sobre longitudes es la cura barata al &lt;strong>length bias&lt;/strong> documentado en DPO. Sin ella, el modelo aprende que &amp;ldquo;más largo = mejor&amp;rdquo; porque las respuestas que el usuario acepta tienden a ser ligeramente más largas. Con SimPO o ORPO este filtro es opcional; con DPO clásico es necesario.&lt;/p>
&lt;h3 id="deduplicación-semántica-con-pgvector">Deduplicación semántica con pgvector&lt;/h3>
&lt;p>Antes de entrenar, dedup. Dos prompts casi idénticos en el dataset es ruido que sesga el modelo:&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">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ranked&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&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="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&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="n">row_number&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OVER&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">PARTITION&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">hashtext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="p">::&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="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">fb_explicit&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 class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&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">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rn&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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&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">created_at&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="n">now&lt;/span>&lt;span class="p">()&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;60 days&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 class="k">DELETE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kto_v3_candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">kto&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">ranked&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">r&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">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">rn&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">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">kto&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&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>Y para los duplicados semánticos (paráfrasis) usamos directamente pgvector 0.8 con &lt;code>iterative index scan&lt;/code>:&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">-- Buscar near-duplicates de un ejemplo cualquiera
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&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">dist&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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&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">created_at&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="n">now&lt;/span>&lt;span class="p">()&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;60 days&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&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">05&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">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&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">LIMIT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">50&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>El &lt;code>iterative scan&lt;/code> es una mejora clave de pgvector 0.8: antes, el índice HNSW podía devolver menos resultados de los pedidos cuando había filtros adicionales (&lt;code>WHERE&lt;/code>); ahora itera hasta cumplir el límite. Sin esa mejora, las queries de curación sobre datasets de millones de filas eran inviables sin un pre-filtro brutal.&lt;/p>
&lt;h2 id="eval-gates-tres-etapas-todo-sql">Eval gates: tres etapas, todo SQL&lt;/h2>
&lt;p>El error más común al implementar fine-tuning continuo es saltarse o aligerar los eval gates. Eso convierte el ciclo en una ruleta. El patrón que funciona en 2026 son &lt;strong>tres etapas&lt;/strong>, cada una con un trade-off latencia/cobertura distinto:&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Tres etapas de eval gates">
&lt;style>
.stage { stroke: #333; stroke-width: 1.5; }
.s1 { fill: #d6eaff; }
.s2 { fill: #d9f5d6; }
.s3 { fill: #ffe9d6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.6; fill: none; marker-end: url(#ah3); }
&lt;/style>
&lt;defs>
&lt;marker id="ah3" 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="360" y="22" text-anchor="middle" class="lbl">Tres etapas de eval gates&lt;/text>
&lt;rect x="30" y="50" width="200" height="110" rx="10" class="stage s1"/>
&lt;text x="130" y="78" text-anchor="middle" class="lbl">Stage 1 · PR&lt;/text>
&lt;text x="130" y="98" text-anchor="middle" class="lbl-sm">&amp;lt; 90 segundos&lt;/text>
&lt;text x="130" y="118" text-anchor="middle" class="lbl-sm">schema-lint + prompt-lint&lt;/text>
&lt;text x="130" y="135" text-anchor="middle" class="lbl-sm">+ 50 casos mini-eval&lt;/text>
&lt;rect x="260" y="50" width="200" height="110" rx="10" class="stage s2"/>
&lt;text x="360" y="78" text-anchor="middle" class="lbl">Stage 2 · pre-merge&lt;/text>
&lt;text x="360" y="98" text-anchor="middle" class="lbl-sm">&amp;lt; 20 minutos&lt;/text>
&lt;text x="360" y="118" text-anchor="middle" class="lbl-sm">200-500 casos golden&lt;/text>
&lt;text x="360" y="135" text-anchor="middle" class="lbl-sm">+ LLM-as-judge&lt;/text>
&lt;rect x="490" y="50" width="200" height="110" rx="10" class="stage s3"/>
&lt;text x="590" y="78" text-anchor="middle" class="lbl">Stage 3 · canary&lt;/text>
&lt;text x="590" y="98" text-anchor="middle" class="lbl-sm">24-72 horas&lt;/text>
&lt;text x="590" y="118" text-anchor="middle" class="lbl-sm">1-5 % tráfico real&lt;/text>
&lt;text x="590" y="135" text-anchor="middle" class="lbl-sm">métricas online + feedback&lt;/text>
&lt;path class="arr" d="M230,105 L260,105"/>
&lt;path class="arr" d="M460,105 L490,105"/>
&lt;/svg>
&lt;/div>
&lt;p>Y aquí es donde Postgres vuelve a brillar: el gate de promoción se expresa como un predicado SQL. Nada más:&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">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">eval_result&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">adapter_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">REFERENCES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">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">suite_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 class="c1">-- &amp;#39;safety-es&amp;#39;, &amp;#39;support-helpfulness&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">metric&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">score&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">NUMERIC&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">judge_model&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">judged_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="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">adapter_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">suite_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metric&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">OR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">REPLACE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FUNCTION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">can_promote&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">candidate&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 class="k">current&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="k">RETURNS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BOOLEAN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$$&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 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="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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&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">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">eval_result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&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">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">train&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">eval_result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">suite_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metric&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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">candidate&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">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">current&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">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">suite_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;safety-es&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;support-helpfulness&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;refusal-rate&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&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">98&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- tolerancia 2 %
&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="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="err">$$&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LANGUAGE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">sql&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">STABLE&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>Una función SQL como gate. Aplicable desde el CI con &lt;code>psql -c &amp;quot;SELECT serve.can_promote('v4.2','v4.1')&amp;quot;&lt;/code> y un exit code 0/1. No hace falta un orquestador, no hace falta una UI específica. La auditoría queda en el log de Postgres.&lt;/p>
&lt;h2 id="vllm-multi-lora-el-deploy-es-un-post-http">vLLM multi-LoRA: el deploy es un POST HTTP&lt;/h2>
&lt;p>Hace dos años, desplegar un fine-tune nuevo era rotar pods de inferencia. Hoy es una llamada HTTP. vLLM 0.7+ soporta cargar y descargar adapters LoRA &lt;strong>en caliente&lt;/strong>, manteniendo varios residentes en VRAM y eligiendo el correcto por petición.&lt;/p>
&lt;p>Configuración del servidor:&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">vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-lora &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-loras &lt;span class="m">4&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> --max-lora-rank &lt;span class="m">64&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> --env &lt;span class="nv">VLLM_ALLOW_RUNTIME_LORA_UPDATING&lt;/span>&lt;span class="o">=&lt;/span>True
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Despliegue de un adapter nuevo:&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://localhost:8000/v1/load_lora_adapter &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;Content-Type: application/json&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;lora_name&amp;#34;: &amp;#34;support-es-v4.2&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;lora_path&amp;#34;: &amp;#34;/mnt/adapters/support-es-v4.2&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>A partir de ese momento, las peticiones que incluyen &lt;code>&amp;quot;model&amp;quot;: &amp;quot;support-es-v4.2&amp;quot;&lt;/code> se sirven con ese adapter aplicado sobre el modelo base. El switch entre adapters tiene latencia despreciable (la investigación más reciente sobre Activated LoRA lleva esto a niveles donde el coste de cambio es invisible).&lt;/p>
&lt;p>Esto cambia la operación de forma sustancial. &lt;strong>El despliegue de un fine-tune nuevo deja de ser un evento de infraestructura para convertirse en un cambio de estado en Postgres&lt;/strong>. El router consulta la tabla &lt;code>serve.adapter&lt;/code>, ve que &lt;code>v4.2&lt;/code> está en &lt;code>canary&lt;/code> con &lt;code>traffic_pct=5&lt;/code>, y dirige el 5 % de peticiones al nuevo adapter. La ruta exacta del 5 % se decide con hashing determinístico del &lt;code>user_id&lt;/code> para que un mismo usuario siempre vea la misma variante (sticky):&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">-- Sin tabla de asignación, sin estado adicional
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- el variant se calcula in-place en SQL o en el router:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&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="k">CASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hashtext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">experiment&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">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 class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">traffic_pct&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">adapter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">candidate&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">THEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="n">candidate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ELSE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="k">current&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&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">adapter_id&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;h2 id="ab-con-tráfico-real-medir-o-vivir-engañado">A/B con tráfico real: medir o vivir engañado&lt;/h2>
&lt;p>Los eval gates miden contra benchmarks fijos. Eso es necesario pero insuficiente. La realidad solo se mide con tráfico real. Una vez el adapter está en canary, lo que importa son las &lt;strong>métricas online&lt;/strong> medidas sobre &lt;code>obs.inference_log&lt;/code> para cada variante:&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">adapter_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="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&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">n&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">AVG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fb_explicit&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">mean_score&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">STDDEV&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fb_explicit&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SQRT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&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">sem&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">AVG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ttft_ms&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">ttft_avg&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">percentile_cont&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WITHIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">GROUP&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="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">ttft_ms&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">ttft_p50&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">percentile_cont&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">95&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WITHIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">GROUP&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="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">ttft_ms&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">ttft_p95&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">AVG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">CASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fb_regen&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">THEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ELSE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&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">regen_rate&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">obs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">inference_log&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">experiment&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&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">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&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="n">now&lt;/span>&lt;span class="p">()&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;7 days&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="k">GROUP&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">adapter_id&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 se mira: feedback explícito, latencia (TTFT, p50, p95), tasa de regeneración. Un adapter que sube el feedback medio pero también sube la tasa de regeneración es sospechoso —probablemente está respondiendo de forma más vistosa pero menos útil—. Un adapter que baja la latencia pero baja el feedback puede merecer estudio: puede que esté siendo más conciso de la cuenta.&lt;/p>
&lt;p>La promoción a &lt;code>prod&lt;/code> ocurre cuando, después de 24-72 horas en canary, el adapter candidato supera al actual en al menos una métrica clave sin degradar las demás. Otra vez: es un &lt;code>UPDATE&lt;/code> en Postgres.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico">Aplicado a hardware on-premise típico&lt;/h2>
&lt;p>Bajemos a dos configuraciones representativas, una de iteración y otra de producción.&lt;/p>
&lt;h3 id="caso-1--rtx-4090-24-gb-para-iteración-de-desarrollo">Caso 1 — RTX 4090 (24 GB) para iteración de desarrollo&lt;/h3>
&lt;p>Una RTX 4090 con QLoRA 4-bit puede entrenar adapters sobre un modelo 8B sin sobresalto. El presupuesto de VRAM combina cuatro componentes; el &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a> durante las evaluaciones intermedias no es despreciable y conviene reservarle margen explícito:&lt;/p>
&lt;pre tabindex="0">&lt;code>Modelo base 8B en 4-bit: ~5 GB
Activations + gradientes: ~8 GB (depende de batch y context)
Optimizer state (LoRA r=16): ~0.5 GB
KV cache durante eval: ~2 GB
Margen de seguridad: ~8 GB
&lt;/code>&lt;/pre>&lt;p>Tiempos típicos (estimación basada en benchmarks comunitarios; conviene medir con el lab antes de prometer):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dataset&lt;/th>
&lt;th>Técnica&lt;/th>
&lt;th style="text-align:right">Adapter rank&lt;/th>
&lt;th style="text-align:right">Tiempo aproximado&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1.000 ejemplos SFT&lt;/td>
&lt;td>LoRA r=16&lt;/td>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">20-40 min&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5.000 ejemplos SFT&lt;/td>
&lt;td>LoRA r=32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">2-4 h&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2.000 pares DPO&lt;/td>
&lt;td>LoRA r=16&lt;/td>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">1-2 h&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5.000 ejemplos KTO&lt;/td>
&lt;td>LoRA r=32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">3-5 h&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Esto pone el ciclo de iteración —cambio en dataset, retrain, eval, ver número— en franja de una &lt;strong>jornada de trabajo&lt;/strong>. Suficiente para validar hipótesis antes de mover nada al cluster de producción.&lt;/p>
&lt;h3 id="caso-2--cluster-4h100-sxm-320-gb-nvlink-para-producción">Caso 2 — Cluster 4×H100 SXM (320 GB, NVLink) para producción&lt;/h3>
&lt;p>Con un cluster de este orden todo el escenario cambia. Se puede:&lt;/p>
&lt;ul>
&lt;li>Entrenar &lt;strong>LoRA sobre 70B en BF16 sin quantización&lt;/strong> con tensor parallel = 4.&lt;/li>
&lt;li>Hacer &lt;strong>DPO completo con modelo de referencia residente&lt;/strong> cuando se cuantiza la referencia a FP8, o pasarse a &lt;strong>SimPO / ORPO&lt;/strong> que eliminan ese modelo intermedio y simplifican la planificación de VRAM (ver tabla de técnicas más arriba).&lt;/li>
&lt;li>Soportar &lt;strong>multi-tenant fine-tuning&lt;/strong>: varios adapters de clientes entrenándose en paralelo en pipelines lógicos separados, cada uno aislado en una partición distinta de Postgres con sus propias ACLs.&lt;/li>
&lt;li>Servir &lt;strong>multi-LoRA con &lt;code>--max-loras 8&lt;/code>&lt;/strong> sobre el modelo base sin que la concurrencia baje el throughput de forma perceptible.&lt;/li>
&lt;/ul>
&lt;p>La regla práctica de presupuesto: en horizonte de 12 meses, un equipo con este cluster puede ejecutar &lt;strong>~150-200 ciclos de fine-tuning continuo&lt;/strong> (training + eval + canary + promoción o descarte) si la disciplina del dataset y de los eval gates es estricta. Si no lo es, ejecutará el doble pero con la mitad de utilidad.&lt;/p>
&lt;h2 id="posición-dentro-de-la-arquitectura-lo-que-cubre-este-artículo-y-lo-que-no">Posición dentro de la arquitectura: lo que cubre este artículo y lo que no&lt;/h2>
&lt;p>Para situar el alcance: el ciclo dibujado al principio tiene siete cajas, todas ellas cubiertas aquí en su mecánica. Quedan &lt;strong>deliberadamente fuera&lt;/strong> tres capas transversales que son las que terminan separando un pipeline que funciona técnicamente de uno que sobrevive a una auditoría:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Provenance criptográfico y trazabilidad.&lt;/strong> Hemos mencionado &lt;code>dataset_snapshot&lt;/code> y &lt;code>pg_audit&lt;/code>, pero la mecánica completa —el &lt;code>set_hash&lt;/code> sobre los ejemplos, la integración con EU AI Act, el &lt;code>query_sql&lt;/code> congelado como prueba de qué entrenó al modelo— da para análisis entero.&lt;/li>
&lt;li>&lt;strong>Calibración del juez.&lt;/strong> Hemos asumido que LLM-as-judge funciona. Hace falta calibrarlo contra rúbrica humana en, al menos, 100 casos por suite crítica antes de fiarse. Sin esa calibración, los eval gates son teatro.&lt;/li>
&lt;li>&lt;strong>El problema del olvido.&lt;/strong> ¿Qué pasa si un usuario ejerce su derecho al olvido GDPR y sus interacciones formaron parte del dataset de un adapter ya en producción? No hay solución limpia. Hay opciones —retrain incremental, machine unlearning a nivel de muestra, negative LoRA— y conviene conocerlas antes de que un cliente pregunte.&lt;/li>
&lt;/ol>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Provenance criptográfico sobre Postgres&lt;/strong>: cómo &lt;code>set_hash&lt;/code> y &lt;code>query_sql&lt;/code> congelado componen una cadena de custodia auditable bajo EU AI Act.&lt;/li>
&lt;li>&lt;strong>Judge calibration honesta&lt;/strong>: por qué &lt;code>score &amp;gt; 0.85&lt;/code> no significa nada sin baseline humana, y cómo construir esa baseline sin que cueste un mes de trabajo.&lt;/li>
&lt;li>&lt;strong>El problema del olvido en adapters&lt;/strong>: machine unlearning a nivel de muestra, retrain incremental y otras técnicas para responder a GDPR sin tirar el adapter.&lt;/li>
&lt;li>&lt;strong>Online DPO y aprendizaje continuo on-policy&lt;/strong>: estado de la investigación 2026 (Fast-Slow Chasing, RLOO, iterative on-policy) y por qué todavía no es producción.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro al que pertenecen las etapas Tune y Retrain. Este post es el deep-dive operativo de ese ciclo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">MLOps específico para LLMs en 2026: el panorama&lt;/a> — apertura de la serie. Sitúa el fine-tuning continuo dentro del marco de tres modalidades (fine-tuning, RAG, agents) y siete diferencias estructurales con MLOps clásico.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">PostgreSQL + Qdrant en la etapa de ingestión&lt;/a> — la etapa Data que alimenta este ciclo: cómo entran los eventos en Postgres y los embeddings en Qdrant que después este post curaría como dataset.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals: la capa después del tracing&lt;/a> — la etapa Eval del pipeline. Las eval gates que aquí se describen como predicados SQL son la materialización del framework genérico de aquel post.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/">El cluster GPU como plataforma multi-tenant&lt;/a> — la etapa Deploy donde el multi-LoRA hot-swap descrito aquí convive con quotas, gateway y aislamiento.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a> — los fundamentos del cache que entra en juego en cada eval intermedia del entrenamiento y en cada despliegue del adapter resultante.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention por dentro&lt;/a> — la mecánica del KV cache y el panorama de optimizaciones (vAttention, EvicPress, RadixAttention) que sostienen el throughput de las eval intermedias del pipeline de fine-tuning.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving: prefill y decode en pods especializados&lt;/a> — el patrón de serving al que se conecta el multi-LoRA hot-swap descrito aquí: cada pod especializado carga sus adapters por separado.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/data-versioning-dvc-lakefs/">Data versioning para LLMOps: DVC, lakeFS y golden dataset reproducible&lt;/a> — la posición opuesta. Este post defiende un stack minimalista (Postgres + pgvector + S3) sin DVC/lakeFS; el otro explica cuándo se cruza la línea y por qué.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Hu et al., &lt;em>LoRA: Low-Rank Adaptation of Large Language Models&lt;/em> (ICLR 2022).&lt;/li>
&lt;li>Dettmers et al., &lt;em>QLoRA: Efficient Finetuning of Quantized LLMs&lt;/em> (NeurIPS 2023).&lt;/li>
&lt;li>Rafailov et al., &lt;em>Direct Preference Optimization: Your Language Model is Secretly a Reward Model&lt;/em> (NeurIPS 2023).&lt;/li>
&lt;li>Meng, Xia, Chen, &lt;em>SimPO: Simple Preference Optimization with a Reference-Free Reward&lt;/em> (NeurIPS 2024).&lt;/li>
&lt;li>Hong et al., &lt;em>ORPO: Monolithic Preference Optimization without Reference Model&lt;/em> (2024).&lt;/li>
&lt;li>Ethayarajh et al., &lt;em>KTO: Model Alignment as Prospect Theoretic Optimization&lt;/em> (2024).&lt;/li>
&lt;li>Kwon et al., &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em> (SOSP 2023) — vLLM original.&lt;/li>
&lt;li>Documentación oficial de vLLM Multi-LoRA: &lt;a href="https://docs.vllm.ai/en/stable/features/lora/">https://docs.vllm.ai/en/stable/features/lora/&lt;/a>.&lt;/li>
&lt;li>Documentación oficial de pgvector 0.8: &lt;a href="https://github.com/pgvector/pgvector">https://github.com/pgvector/pgvector&lt;/a>.&lt;/li>
&lt;li>TRL (HuggingFace) docs: &lt;a href="https://huggingface.co/docs/trl">https://huggingface.co/docs/trl&lt;/a>.&lt;/li>
&lt;li>EU AI Act, texto consolidado y calendario de aplicación: &lt;a href="https://artificialintelligenceact.eu/">https://artificialintelligenceact.eu/&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>