Módulo 7: Shipping Ai Safely

Un modelo no es código estático: el mismo endpoint, un comportamiento que se mueve

Descripción

Cuando lanzaste recommendations en los módulos 1 a 6, el objeto que viajaba detrás del feature flag era, para todo propósito práctico, código: una función que, dado un userId, devuelve true o false de forma estable, y que solo cambia si alguien edita y deploya un archivo nuevo. Esta lección rompe esa intuición para el caso de un modelo: recs-v1, el modelo que decide qué productos recomendar, puede cambiar su comportamiento —qué recomienda, para quién, con qué frecuencia acierta— sin que nadie toque una sola línea del código que lo sirve. La causa no es un bug. Es una propiedad de cómo funcionan los modelos aprendidos de datos, y es la razón de fondo de todo lo que este módulo va a construir.

Conexión con el módulo. Esta lección no introduce ningún mecanismo de shadow mode ni de comparación todavía —eso empieza en la lección 3—. Lo que hace es dejar establecido el problema que esas herramientas existen para resolver: si un modelo puede cambiar de comportamiento sin deploy, entonces "el modelo de producción" no es un blanco fijo que puedas auditar una sola vez y olvidar. Cada lección que sigue en este módulo —shadow mode, tasa de acuerdo, postmortems, privacidad— es, en el fondo, una respuesta distinta a la misma pregunta que esta lección plantea: ¿cómo confías en algo que se mueve?

Una analogía: el empleado nuevo brillante, pero impredecible

Piensa en dos personas nuevas en un equipo. La primera es un pasante que sigue un manual de procedimientos: dado el mismo caso, siempre aplica la misma regla, en el mismo orden, y produce el mismo resultado — se puede auditar leyendo el manual una sola vez, porque el manual no cambia solo. La segunda es una persona con mucho criterio, contratada precisamente porque no sigue un guion fijo: evalúa cada caso con su propio juicio, aprende de la experiencia acumulada, y con el tiempo —a medida que ve más casos, o recibe más capacitación— empieza a decidir distinto de como decidía el primer mes, aunque nadie le haya dado nuevas instrucciones por escrito.

Un modelo de recomendaciones es la segunda persona, no la primera. recs-v1 no sigue un manual de reglas fijas que puedas leer una vez y confiar para siempre — aprendió patrones de un conjunto de datos, y si ese conjunto de datos se actualiza (un reentrenamiento con compras más recientes, por ejemplo), el modelo resultante puede recomendar distinto, aunque el código que lo llama —el mismo endpoint, la misma función getRecommendations()— no haya cambiado ni un carácter. Confiar en un empleado así no significa dejar de confiar — significa supervisarlo con una disciplina distinta a la que usarías con el pasante del manual: revisar su trabajo periódicamente, no solo el primer día.

Ejemplo trabajado: tres reentrenamientos de recs-v1, mismo código, comportamiento distinto

Vamos a construir el caso más simple posible: el historial de tres versiones de recs-v1, cada una entrenada en una fecha distinta con datos más recientes, sin que el código que las sirve haya cambiado entre versiones. Medimos su clickThroughRate (la fracción de veces que un comprador hace clic en una recomendación) en cada punto:

// summarizeModelHistory: compara cada version de un modelo contra la primera,
// para ver cuanto se movio el comportamiento SIN que el codigo que lo sirve
// haya cambiado -- solo el reentrenamiento con datos mas recientes.
function summarizeModelHistory(versions) {
  const baseline = versions[0].clickThroughRate;
  return versions.map((v) => {
    const deltaPct = ((v.clickThroughRate - baseline) / baseline) * 100;
    return {
      version: v.version,
      trainedOn: v.trainedOn,
      clickThroughRate: v.clickThroughRate,
      deltaVsFirstPct: deltaPct,
      flag: deltaPct <= -10 ? 'DEGRADED' : 'stable',
    };
  });
}

const recsV1History = [
  { version: 'recs-v1.0', trainedOn: '2026-01-15', clickThroughRate: 0.041 },
  { version: 'recs-v1.1', trainedOn: '2026-03-02', clickThroughRate: 0.038 },
  { version: 'recs-v1.2', trainedOn: '2026-05-20', clickThroughRate: 0.033 },
];

console.log('=== summarizeModelHistory sobre recs-v1 (mismo codigo, tres reentrenamientos) ===\n');
summarizeModelHistory(recsV1History).forEach((r) => {
  console.log(r.version + '  entrenado ' + r.trainedOn + '  CTR=' + r.clickThroughRate + '  delta=' + r.deltaVsFirstPct.toFixed(1) + '%  ' + r.flag);
});

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== summarizeModelHistory sobre recs-v1 (mismo codigo, tres reentrenamientos) ===

recs-v1.0  entrenado 2026-01-15  CTR=0.041  delta=0.0%  stable
recs-v1.1  entrenado 2026-03-02  CTR=0.038  delta=-7.3%  stable
recs-v1.2  entrenado 2026-05-20  CTR=0.033  delta=-19.5%  DEGRADED

Fíjate en lo que no cambió entre estas tres filas: el endpoint que Mercado llama para pedir recomendaciones es el mismo, el código de la aplicación que lo consume es el mismo, y no hubo ningún pull request ni ningún deploy entre el 15 de enero y el 20 de mayo que tocara la lógica de recommendations. Lo único que cambió fue el conjunto de datos con el que recs-v1 se reentrenó cada vez — y aun así, el clickThroughRate cayó de 4.1% a 3.3%, una caída de 19.5% respecto al primer punto, suficiente para que summarizeModelHistory() la marque como DEGRADED. Ningún ingeniero que solo revise el historial de git log del repositorio de recommendations encontraría ninguna explicación para esta caída — porque la causa no está en el código, está en los datos que el modelo aprendió.

Este fenómeno tiene nombre en la práctica de la industria: data drift (o, más específico aquí, concept drift) — el patrón que un modelo aprendió deja de reflejar con la misma fidelidad el mundo real, a medida que ese mundo cambia. Un modelo entrenado con compras de enero puede no capturar tan bien los gustos de mayo, si las tendencias de compra de Mercado se movieron en esos meses. La lección 2 de la guía de métricas de Mercado ya asumía que el modelo entregaba resultados estables cuando se midió el lift de recommendations — este módulo es donde esa suposición deja de ser gratuita, y se vuelve algo que hay que vigilar activamente.

Por qué esto cambia cómo piensas en "lanzar" algo que usa un modelo

Con una feature normal, "está en producción" es una pregunta que se contesta mirando el código: ¿está deployado?, ¿el flag está encendido? Con un modelo, esa misma pregunta tiene una segunda capa que el código no responde: incluso si recs-v1 sigue siendo, técnicamente, la misma versión desplegada desde enero, su comportamiento del 20 de mayo ya no es el mismo que el del 15 de enero. Esto tiene dos consecuencias prácticas que el resto del módulo desarrolla:

La primera es que "validar una vez" no es suficiente para un modelo, de una forma que sí lo era, en gran medida, para una feature de código estático. Un modelo necesita revisión periódica de su comportamiento, no solo una revisión al momento del lanzamiento inicial — exactamente el rol que cumple el monitoreo de guardrails del módulo 4, ahora aplicado también a métricas propias del modelo, no solo a las de negocio.

La segunda, la que este módulo desarrolla con más detalle, es que cuando decides reemplazar deliberadamente un modelo por otro —no un drift lento por reentrenamiento, sino un cambio consciente de recs-v1 a recs-v2— esa decisión merece la misma pregunta que te harías si contrataras, de un día para otro, a un empleado distinto para el mismo puesto: ¿decide igual que la persona anterior, o distinto? ¿En qué casos? Contestar esa pregunta antes de que el cambio afecte a un solo comprador real es, exactamente, lo que la lección 3 empieza a construir con el shadow mode.

Errores comunes

Asumir que "no hubo deploy" significa "el comportamiento no cambió". Qué pasa: un ingeniero, investigando por qué cayó una métrica relacionada con recommendations, revisa el historial de deploys del repositorio, no encuentra ningún cambio reciente, y concluye que el problema debe estar en otro lado —tráfico, infraestructura, cualquier cosa menos el modelo—. Por qué pasa: la intuición de "sin deploy no hay cambio" es completamente correcta para código tradicional, y es fácil aplicarla sin ajuste a un sistema que incluye un modelo. Cómo detectarlo: si la investigación de una caída de métrica nunca revisa cuándo fue el último reentrenamiento del modelo, esa fuente de cambio quedó completamente fuera del análisis. Cómo corregirlo: como muestra el ejemplo de esta lección, el historial de versiones de un modelo —con su fecha de entrenamiento y sus métricas— es una fuente de cambio tan real como el historial de git log, y debería revisarse con la misma disciplina.

Confundir un reentrenamiento rutinario con una migración de modelo. Qué pasa: el equipo trata cada reentrenamiento periódico de recs-v1 (una práctica normal, casi automática) con el mismo nivel de ceremonia que este módulo reserva para cambiar de recs-v1 a recs-v2 —shadow mode completo, canary, todo el proceso de la lección 7— y termina bloqueando un flujo que debería ser ligero. Por qué pasa: una vez que aprendes que "un modelo puede cambiar de comportamiento", es tentador tratar cualquier cambio de modelo con la máxima cautela posible, sin distinguir grados de riesgo. Cómo detectarlo: si cada reentrenamiento rutinario de recs-v1 requiere el mismo proceso de aprobación que un cambio de arquitectura completo, el proceso probablemente está mal calibrado para el riesgo real. Cómo corregirlo: la severidad del cambio importa — un reentrenamiento con la misma arquitectura y el mismo tipo de datos justifica, como mínimo, el monitoreo continuo de sus métricas (lo que summarizeModelHistory() ilustra); un cambio de arquitectura completo, como recs-v1 a recs-v2, es lo que justifica el proceso completo de shadow y canary que este módulo construye.

Notar el drift solo cuando ya causó un daño visible. Qué pasa: nadie revisa el clickThroughRate de recs-v1 entre reentrenamientos, y la caída de 4.1% a 3.3% de este ejemplo solo se descubre meses después, cuando alguien nota que la conversión general de Mercado bajó y empieza a investigar causas. Por qué pasa: sin un chequeo activo como summarizeModelHistory(), el drift es silencioso por definición — no hay ninguna alarma que se dispare sola, porque técnicamente nada se "rompió" en el sentido de un error o una excepción. Cómo detectarlo: si la única forma en que el equipo se entera de un drift es por una queja de negocio ya materializada, no hay ningún chequeo activo corriendo. Cómo corregirlo: correr una comparación como la de esta lección —cada versión contra la línea base, con un umbral explícito de degradación— después de cada reentrenamiento, no solo cuando algo ya se ve mal. Es la misma disciplina que el módulo 4 aplicó a los guardrails de negocio, ahora aplicada a la salud del modelo mismo.

Ejercicios

Ejercicio 1 — Calcula el delta a mano. Si recs-v1.3 se entrenara con datos hasta el 2026-08-01 y su clickThroughRate fuera 0.036, ¿cuál sería su deltaVsFirstPct respecto a recs-v1.0 (CTR 0.041), y qué flag le asignaría summarizeModelHistory()?

Ver solución

deltaVsFirstPct = ((0.036 - 0.041) / 0.041) * 100 = -12.195...%, que redondeado a una cifra decimal es -12.2%. Como -12.2 <= -10, la función le asignaría flag: 'DEGRADED' — igual que a recs-v1.2, aunque con un poco menos de caída. Vale la pena notar que este resultado también muestra que el modelo se recuperó parcialmente respecto a recs-v1.2 (que tenía -19.5%), sin llegar todavía a stable.

Ejercicio 2 — Encuentra el bug de razonamiento. Un compañero de equipo argumenta: "recommendations no puede tener ningún problema de comportamiento — revisé el repositorio y el último commit que tocó ese código fue hace cuatro meses". Usando lo que aprendiste en esta lección, ¿qué le falta considerar a ese argumento?

Ver solución

El argumento confunde "el código no cambió" con "el comportamiento no cambió" — una equivalencia válida para software tradicional, pero no para un sistema que incluye un modelo entrenado de datos. Aunque el código que sirve recommendations no haya tenido ningún commit en cuatro meses, recs-v1 puede haberse reentrenado varias veces en ese período (como en el ejemplo de esta lección, sin ningún cambio de código de por medio), y cada reentrenamiento pudo haber movido su comportamiento. El compañero necesita revisar el historial de versiones del modelo, no solo el historial de commits del repositorio, para descartar esta fuente de cambio.

Ejercicio 3 — Diseña tu propio umbral. summarizeModelHistory() usa -10% como el umbral que separa stable de DEGRADED. ¿En qué circunstancias de negocio tendría sentido un umbral más estricto (por ejemplo, -5%), y en cuáles uno más permisivo (por ejemplo, -15%)?

Ver solución

Un umbral más estricto (-5%) tiene sentido cuando la métrica que se vigila está directamente ligada a un resultado de negocio de alto impacto y bajo margen de error —por ejemplo, si clickThroughRate predice de forma muy directa la conversión de checkout, y Mercado no puede permitirse una caída sostenida sin reaccionar rápido—. Un umbral más permisivo (-15%) tiene más sentido cuando la métrica tiene ruido natural considerable entre reentrenamientos —por ejemplo, si el tamaño de la muestra usada para calcular clickThroughRate es chico, y algo de variación entre versiones es esperable sin que signifique una degradación real—. No hay un umbral universalmente correcto: la elección depende de cuánta variación natural tiene la métrica y cuánto le cuesta al negocio reaccionar tarde ante una degradación real.

Resumen y siguiente paso

En esta lección construiste summarizeModelHistory() y la corriste sobre tres reentrenamientos reales de recs-v1: el clickThroughRate cayó de 4.1% a 3.3% —un 19.5% de caída respecto a la primera versión— sin que ningún código de la aplicación cambiara entre esas fechas. Confirmaste, con este dato, la idea central de la lección: un modelo puede degradarse sin ningún deploy de por medio, porque su comportamiento vive en los datos con los que fue entrenado, no en el código que lo sirve.

Antes de avanzar deberías poder: explicar la diferencia entre "el código cambió" y "el comportamiento del modelo cambió"; describir qué es el data drift en tus propias palabras; y justificar por qué un modelo necesita revisión continua de su comportamiento, no solo una validación en el momento del lanzamiento.

La lección 3 toma esta misma preocupación y la aplica a un escenario más deliberado: no un reentrenamiento gradual de recs-v1, sino el reemplazo consciente por recs-v2. Ahí construyes el mecanismo que responde a la pregunta de fondo de esta lección —¿cómo confías en algo que puede cambiar de comportamiento?— sin exponer a ningún comprador todavía: el shadow mode.

Recursos

  • Chip Huyen, Designing Machine Learning Systems (O'Reilly, 2022) — oreilly.com/library/view/designing-machine-learning/9781098107956. El capítulo sobre monitoreo y mantenimiento de modelos en producción desarrolla en profundidad el data drift y el concept drift que esta lección ilustra con el caso de recs-v1. En inglés.
  • Google SRE Book, Capítulo 6, "Monitoring Distributed Systems" — sre.google/sre-book/monitoring-distributed-systems. Aunque escrito para sistemas tradicionales, el principio de vigilancia continua —en vez de validación única— es exactamente el que esta lección extiende a un modelo cuyo comportamiento se mueve solo. En inglés.