Módulo 7: Shipping Ai Safely

Migrar un modelo de principio a fin: de shadow a producción, sin atajos

Descripción

Las cinco lecciones anteriores construyeron piezas: serveWithShadow() (L3), shadowCompare() con su tasa de acuerdo y su desglose por segmento (L4), checkReproducibility() para investigar incidentes probabilísticos (L5), redactPII() para no registrar más de lo necesario (L6). Esta lección las junta en un proceso completo, de principio a fin, y agrega la pieza que faltaba: modelMigrationDecision(), la función que convierte el resultado de shadowCompare() en una decisión explícita de si conviene, o no, promover un modelo a canary — la misma disciplina de decisión formal que launchVerdict() (M1) y rollbackDecision() (M5) ya aplicaron a otras decisiones de esta guía.

Conexión con el módulo. Esta lección no reemplaza nada de lo construido antes — lo ordena. Reutiliza shadowCompare() de la lección 4 exactamente como quedó, sobre exactamente los mismos 50 requests de shadow traffic, y agrega la función de decisión que faltaba para que ese resultado se traduzca en una acción concreta. El proyecto de la lección 8 reutiliza esta misma función, sin cambios, sobre el caso completo de recommendations.

Una analogía: el día del primer tramo real

El piloto en prácticas de las lecciones 3 y 4 lleva ya suficientes horas de shadow como para tener un número: coincide con el capitán en el 78% de las maniobras observadas, con un detalle importante — en las maniobras de despegue y aterrizaje con pasajeros nuevos a bordo (el "cold-start" de un vuelo, en cierto sentido), esa coincidencia cae a menos del 27%. Un capitán que solo mirara el 78% podría sentirse tentado a cederle un tramo completo. Un capitán que revisó el desglose sabe que no es el momento todavía — no porque el piloto en prácticas sea malo, sino porque hay un tipo específico de maniobra que necesita más práctica antes de intentarse con pasajeros reales.

Esta lección es el protocolo completo que decide cuándo sí llega ese día, y qué pasa una vez que llega. No es solo "el número supera el umbral, adelante" — es una secuencia completa: confirmar el número, confirmar que ningún segmento crítico se queda atrás, ceder solo un tramo corto al principio (el canary de los módulos 2 y 3), mantener al capitán con la mano lista para retomar el control (el kill switch del módulo 2), vigilar instrumentos durante todo el tramo (los guardrails del módulo 4), y tener claro, de antemano, qué hacer si algo sale mal (el rollback del módulo 5).

Ejemplo trabajado: modelMigrationDecision() sobre el resultado de shadowCompare()

Vamos a construir la función de decisión y aplicarla al resultado exacto que shadowCompare() produjo en la lección 4 —78.0% de acuerdo global, 26.7% en el segmento cold-start—:

// modelMigrationDecision: cruza el resultado de shadowCompare() contra DOS
// umbrales explicitos -- el acuerdo global, Y el acuerdo del segmento mas
// critico -- antes de autorizar cualquier canary. Un solo umbral global no
// alcanza (leccion 4): un segmento puede estar muy por debajo sin que el
// numero global lo revele.
function modelMigrationDecision({ agreementRate, minAgreement, criticalSegmentAgreementRate, minCriticalSegmentAgreement }) {
  if (agreementRate < minAgreement) {
    return 'NO promuevas a canary: la tasa de acuerdo global esta por debajo del minimo -- revisa las diferencias primero';
  }
  if (criticalSegmentAgreementRate < minCriticalSegmentAgreement) {
    return 'NO promuevas a canary todavia: un segmento critico difiere demasiado -- diagnostica esa estrategia antes de exponer trafico real';
  }
  return 'promueve recs-v2 a canary (1%), vigilando los guardrails de M4 en cada etapa';
}

// Resultado real de shadowCompare() sobre el caso de Mercado, de la leccion 4
// (reproducido aqui como dato de entrada; la funcion completa vive en L4).
const shadowResult = {
  agreementRate: 78.0,
  segmentBreakdown: [
    { segment: 'with-history', agreementRate: 100.0 },
    { segment: 'cold-start', agreementRate: 26.7 },
  ],
};

console.log('=== modelMigrationDecision sobre el resultado de shadowCompare ===\n');
const coldStartSegment = shadowResult.segmentBreakdown.find((s) => s.segment === 'cold-start');
const decision = modelMigrationDecision({
  agreementRate: shadowResult.agreementRate,
  minAgreement: 70,
  criticalSegmentAgreementRate: coldStartSegment.agreementRate,
  minCriticalSegmentAgreement: 60,
});
console.log('agreementRate global=' + shadowResult.agreementRate.toFixed(1) + '%  (minimo=70%)');
console.log('agreementRate cold-start=' + coldStartSegment.agreementRate.toFixed(1) + '%  (minimo=60%)');
console.log('Decision: ' + decision);

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

=== modelMigrationDecision sobre el resultado de shadowCompare ===

agreementRate global=78.0%  (minimo=70%)
agreementRate cold-start=26.7%  (minimo=60%)
Decision: NO promuevas a canary todavia: un segmento critico difiere demasiado -- diagnostica esa estrategia antes de exponer trafico real

El 78.0% global sí pasa el primer umbral (70%) — si modelMigrationDecision() solo mirara ese número, la decisión sería "adelante, canary". Pero el segundo chequeo, sobre el segmento cold-start, encuentra un 26.7% muy por debajo del umbral crítico de 60% — y esa sola señal es suficiente para frenar la decisión completa, sin importar qué tan bien se vea el número global. Fíjate en el orden de los dos if: primero el global, después el segmento crítico — si el global ya hubiera fallado, ni siquiera haría falta revisar el segmento, porque ya no habría ninguna base para migrar. Este es exactamente el mismo patrón de decisión escalonada que launchVerdict() (M1) y rollbackDecision() (M5) ya usaron: varias condiciones explícitas, evaluadas en un orden que tiene sentido, en vez de una sola pregunta que colapsa toda la evidencia en un sí o un no.

El proceso completo, de principio a fin

Con modelMigrationDecision() como la bisagra, así se ve el proceso completo de migrar recs-v1 a recs-v2, uniendo las piezas de este módulo con las de los módulos anteriores:

1. SHADOW MODE (L3)
   recs-v2 corre en paralelo a recs-v1, sobre trafico real.
   Cero impacto en lo que el comprador ve.
        │
        ▼
2. MEDIR (L4)
   shadowCompare() sobre suficiente volumen acumulado.
   Tasa de acuerdo global + desglose por segmento.
        │
        ▼
3. DECIDIR (L7 -- esta lección)
   modelMigrationDecision(): ¿el global Y cada segmento crítico
   pasan su umbral?
        │
   ┌────┴────┐
   NO         SÍ
   │           │
   ▼           ▼
Diagnostica   4. CANARY (M2 + M3)
el segmento   recs-v2 detrás de un feature flag, rollout
que falla,    gradual: 1% → 10% → 50% → 100%, con
vuelve a      isEnabled() decidiendo quién ve cuál modelo.
shadow              │
                    ▼
              5. VIGILAR (M4)
              guardrailWatch() en cada etapa: latencia,
              quejas, churn, márgenes -- los mismos de siempre.
                    │
              ┌─────┴─────┐
              OK           Guardrail roto
              │             │
              ▼             ▼
        Sigue subiendo   6. KILL SWITCH + ROLLBACK (M2 + M5)
        la rampa         Apaga recs-v2 al instante, vuelve
                          a recs-v1. rollbackDecision() decide
                          revertir o fix-forward.
                                │
                                ▼
                          7. POSTMORTEM (M6 + L5)
                          Si la causa es probabilística,
                          checkReproducibility() primero.
                                │
                                ▼
                          8. TODO EL PROCESO REGISTRA
                          SOLO LO NECESARIO (L6)
                          redactPII() en cada log generado.

Vale la pena notar lo que este diagrama confirma: ningún paso de este módulo reemplaza a los módulos 2 a 6 — los extiende. El canary de un modelo usa el mismo isEnabled() y el mismo rolloutPlan() que cualquier otra feature; el kill switch es el mismo mecanismo binario de la lección 5 del módulo 2; el rollback es el mismo rollbackDecision() del módulo 5. Lo único genuinamente nuevo es lo que pasa antes de que el flag exista siquiera —shadow mode y la decisión basada en acuerdo— y lo que cambia cuando algo sale mal —la pregunta de reproducibilidad antes del postmortem, y el cuidado adicional sobre qué se registra en cada paso.

Errores comunes

Saltarse el paso de decisión, y pasar directo de "corrimos shadow" a "prendamos el canary". Qué pasa: el equipo corre recs-v2 en shadow durante un tiempo razonable, mira el resultado de shadowCompare() de forma informal —"se ve bastante parecido"—, y activa el canary sin haber aplicado ningún umbral explícito ni haber revisado el desglose por segmento. Por qué pasa: después de invertir el esfuerzo de construir el shadow, avanzar al canary se siente como el paso natural, y una revisión informal del resultado parece suficiente cuando el número global se ve razonable. Cómo detectarlo: si nadie puede señalar un umbral explícito —ni para el acuerdo global ni para ningún segmento— que se haya comparado formalmente antes de activar el canary, la decisión fue informal, no basada en criterio. Cómo corregirlo: como en el ejemplo de esta lección, modelMigrationDecision() con umbrales explícitos, definidos de antemano, es el paso obligatorio entre "medimos" y "exponemos" — nunca un salto directo.

Migrar el 100% de golpe, porque "ya pasó el shadow test". Qué pasa: una vez que modelMigrationDecision() (o su equivalente informal) da luz verde, el equipo interpreta eso como autorización para poner recs-v2 al 100% inmediatamente, sin pasar por las etapas de canary y rampa gradual. Por qué pasa: pasar el shadow test se siente como la validación completa y definitiva, y es fácil olvidar que el shadow, por diseño, todavía no expuso a un solo comprador real — el rollout gradual sigue siendo necesario después, exactamente como para cualquier otra feature. Cómo detectarlo: si el plan de migración no incluye un canary al 1% ni etapas intermedias, saltó directo de "shadow aprobado" a "producción completa", sin el rollout que los módulos 2 y 3 ya construyeron para este propósito exacto. Cómo corregirlo: pasar el shadow test es la autorización para empezar el rollout gradual —el mismo canary 1% → 10% → 50% → 100% de siempre—, no para saltárselo. El diagrama de esta lección lo muestra en orden: shadow y decisión primero, canary y rampa después, nunca al revés ni combinados.

No tener un kill switch específico para el modelo, distinto del flag de la feature completa. Qué pasa: recommendations como feature tiene su kill switch (módulo 2), pero el equipo asume que ese mismo interruptor sirve para volver de recs-v2 a recs-v1 — sin haber construido, de hecho, una forma de cambiar de modelo sin apagar la feature completa. Por qué pasa: un solo flag booleano para toda la feature se siente suficiente, y es fácil no notar la diferencia entre "apagar recommendations por completo" y "volver al modelo anterior, manteniendo la feature encendida" hasta el momento en que de verdad hace falta esa segunda opción. Cómo detectarlo: si la única forma de dejar de usar recs-v2 es apagar recommendations completo para todos los compradores, falta un nivel de control más fino. Cómo corregirlo: el registro de flags de la lección 3 del módulo 2 ya soporta esto de forma natural —un campo adicional que indica qué versión de modelo sirve el flag activo, cambiable de forma independiente al enabled general—, así una regresión del modelo nuevo no obliga a apagar toda la feature para volver a la versión conocida.

Ejercicios

Ejercicio 1 — Cambia los umbrales. Si Mercado decidiera que, para este caso específico, el umbral del segmento crítico fuera 20 en vez de 60 (aceptando más diferencia en cold-start antes de bloquear la migración), ¿qué decisión devolvería modelMigrationDecision() con los mismos datos de esta lección (agreementRate=78.0, cold-start=26.7)?

Ver solución

Con minCriticalSegmentAgreement: 20, el chequeo 26.7 < 20 es false, así que ese if ya no bloquea la decisión. Como el primer chequeo (78.0 < 70) también es false, la función llegaría hasta el final y devolvería 'promueve recs-v2 a canary (1%), vigilando los guardrails de M4 en cada etapa'. Este ejercicio muestra algo importante: el umbral no es un dato objetivo que "shadowCompare()" calcula — es una decisión de negocio sobre cuánta diferencia está dispuesto a tolerar el equipo antes de investigar, y cambiar ese número cambia la decisión final sin que ningún dato real haya cambiado.

Ejercicio 2 — Ubica cada pieza en el diagrama. Sin mirar el diagrama de esta lección, intenta recordar: ¿en qué paso del proceso completo aparece checkReproducibility() de la lección 5, y por qué en ese punto y no antes?

Ver solución

checkReproducibility() aparece en el paso del postmortem, después de un incidente detectado durante el canary o el rollout —no antes, porque no hay ningún incidente que investigar todavía durante el shadow o la decisión de migración—. Tiene sentido que aparezca justo antes del postmortem (paso 7 del diagrama): antes de escribir la causa raíz de cualquier incidente que involucre al modelo, primero hay que confirmar si el componente responsable es determinista o probabilístico, exactamente la pregunta que la lección 5 enseña a hacer primero.

Ejercicio 3 — Explica la frontera con M2 y M3. Un compañero, después de leer este módulo, comenta: "entonces migrar un modelo es un proceso completamente distinto de lanzar una feature normal". ¿Estás de acuerdo? Corrige la afirmación usando lo que aprendiste en esta lección.

Ver solución

No es del todo correcto. Migrar un modelo reutiliza casi todo el proceso de lanzar una feature normal —el mismo feature flag, el mismo rollout gradual con canary, los mismos guardrails, el mismo rollback— sin ningún cambio. Lo que es distinto, y lo que este módulo agrega, es lo que pasa antes de que el flag siquiera se cree: el shadow mode y la decisión basada en tasa de acuerdo, que no tienen equivalente en una feature de código normal, porque una feature de código no tiene una "versión anterior" con la que comparar su comportamiento de la misma forma probabilística en que dos modelos se pueden comparar. La corrección justa sería: "migrar un modelo agrega una capa nueva antes del proceso de siempre, no reemplaza el proceso de siempre".

Resumen y siguiente paso

En esta lección construiste modelMigrationDecision() y confirmaste, con los datos reales de recommendations, que el 78.0% de acuerdo global no es suficiente por sí solo: el 26.7% del segmento cold-start bloquea la migración hasta que ese segmento se investigue. Viste el proceso completo de principio a fin —shadow, medir, decidir, canary, vigilar, kill switch y rollback si algo sale mal, postmortem con la pregunta de reproducibilidad primero, y minimización de datos en cada log generado— y confirmaste que este módulo no reemplaza nada de lo construido en los módulos 2 a 6: lo extiende con lo que es específico de un modelo.

Antes de avanzar deberías poder: explicar por qué una decisión de migración necesita dos tipos de umbral, no uno; dibujar de memoria el proceso completo, señalando qué es nuevo de este módulo y qué se reutiliza de los anteriores; y explicar la diferencia entre el kill switch de una feature completa y el de una versión específica de modelo.

La lección 8, el mini-proyecto y cierre de este módulo, te pone en el lugar del equipo de Mercado: correr el proceso completo sobre el caso real, reutilizando shadowCompare(), modelMigrationDecision() y redactPII() sin cambiarles una línea, para producir la decisión formal y el reporte de datos que el equipo necesita antes de tocar recs-v2 de nuevo.

Recursos

  • Microsoft Learn, "Safe rollout for online endpoints" (Azure Machine Learning) — learn.microsoft.com/en-us/azure/machine-learning/how-to-safely-rollout-online-endpoints. Documenta el proceso completo —desplegar en paralelo, dirigir una fracción de tráfico, vigilar, y revertir si hace falta— que esta lección arma a mano, cruzando shadow mode con el rollout gradual de los módulos 2 y 3. En inglés.
  • Chip Huyen, Designing Machine Learning Systems (O'Reilly, 2022) — oreilly.com/library/view/designing-machine-learning/9781098107956. El capítulo sobre despliegue de modelos describe estrategias de migración gradual —shadow, canary, blue-green— con el mismo criterio de decisión basado en comparación que modelMigrationDecision() implementa en código. En inglés.