Módulo 7: Shipping Ai Safely
Shadow mode: correr el modelo nuevo sin que nadie lo note
Descripción
La lección 2 dejó una pregunta abierta: cuando Mercado decida reemplazar recs-v1 por recs-v2, ¿cómo sabes si el modelo nuevo decide distinto del viejo, antes de que esa diferencia le llegue a un solo comprador? Esta lección construye la respuesta: shadow mode — correr recs-v2 en paralelo a recs-v1, sobre el mismo tráfico real, sin que ninguna de sus decisiones llegue todavía al usuario. El comprador sigue viendo, en todo momento, exactamente lo que recs-v1 decide. recs-v2 corre al lado, callado, y sus respuestas se registran para comparar después.
Conexión con el módulo. Esta lección construye la pieza de infraestructura que la lección 4 va a poner a trabajar de verdad: hoy defines recsV1() y recsV2() —las dos funciones que van a correr en paralelo durante el resto del módulo— y serveWithShadow(), que las llama a las dos pero solo deja pasar la respuesta de la primera. La lección 4 reutiliza estas mismas dos funciones, sin cambiarles una línea, para construir shadowCompare() sobre un volumen real de tráfico.
Una analogía: el piloto en prácticas, en el asiento de al lado
Antes de que un piloto en entrenamiento tome los controles de un vuelo con pasajeros, pasa muchas horas en el asiento del copiloto de vuelos reales, con el capitán al mando: observa los instrumentos, toma sus propias decisiones mentales sobre qué haría en cada maniobra —cuándo ajustar la altitud, cuándo corregir el rumbo—, y las anota. El capitán nunca cede el control durante esas horas. Los pasajeros nunca saben que, en el asiento de al lado, alguien está "volando" el mismo vuelo en su cabeza, tomando sus propias decisiones sin que ninguna de ellas mueva un solo control real.
Eso es shadow mode. recs-v1 es el capitán: sus decisiones son las que de verdad llegan al comprador. recs-v2 es el piloto en prácticas: recibe exactamente la misma información que recs-v1 —el mismo request, el mismo comprador, el mismo momento—, decide qué recomendaría, y esa decisión se registra para revisión, pero nunca toca los controles reales. Después de suficientes "vuelos" en esta modalidad, con suficientes decisiones registradas y comparadas, recién entonces —lección 4 en adelante— tiene sentido preguntarse si el piloto en prácticas está listo para un primer tramo real, corto y supervisado: el canary.
Ejemplo trabajado: serveWithShadow() sobre cuatro requests de Mercado
Vamos a construir recsV1() y recsV2(), los dos modelos que van a correr en paralelo durante el resto de este módulo, y serveWithShadow(), la función que los llama a los dos pero solo deja pasar la respuesta del primero al comprador:
// recsV1 (el modelo en produccion, el capitan): si el comprador tiene historial
// de compras, recomienda su ultima categoria comprada. Si es un comprador nuevo
// (cold start, sin historial), recomienda la categoria mas vendida de TODO el sitio.
function recsV1(request) {
if (request.hasHistory) return request.lastPurchaseCategory;
return 'electronics'; // trending general del sitio completo
}
// recsV2 (el modelo nuevo, en SHADOW -- el piloto en practicas): igual criterio
// para compradores con historial, pero cambia la estrategia de cold start: en vez
// de la tendencia general del sitio, usa la tendencia de la REGION del comprador.
const REGION_TRENDING = { north: 'electronics', south: 'home', east: 'fashion', west: 'sports' };
function recsV2(request) {
if (request.hasHistory) return request.lastPurchaseCategory;
return REGION_TRENDING[request.region];
}
// serveWithShadow: el comprador SOLO ve userSees (recs-v1). shadowOnly (recs-v2)
// se calcula en paralelo y se registra, pero nunca llega al comprador.
function serveWithShadow(request) {
const userSees = recsV1(request);
const shadowOnly = recsV2(request);
return { requestId: request.requestId, userSees, shadowOnly };
}
const demoRequests = [
{ requestId: 'req-demo-01', hasHistory: true, lastPurchaseCategory: 'sports', region: 'north' },
{ requestId: 'req-demo-02', hasHistory: true, lastPurchaseCategory: 'beauty', region: 'south' },
{ requestId: 'req-demo-03', hasHistory: false, lastPurchaseCategory: null, region: 'north' },
{ requestId: 'req-demo-04', hasHistory: false, lastPurchaseCategory: null, region: 'south' },
];
console.log('=== serveWithShadow sobre 4 requests de ejemplo ===\n');
demoRequests.forEach((r) => {
const result = serveWithShadow(r);
console.log(result.requestId + ' userSees=' + result.userSees + ' shadowOnly(recs-v2)=' + result.shadowOnly +
(result.userSees === result.shadowOnly ? ' [coinciden]' : ' [difieren]'));
});
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== serveWithShadow sobre 4 requests de ejemplo ===
req-demo-01 userSees=sports shadowOnly(recs-v2)=sports [coinciden]
req-demo-02 userSees=beauty shadowOnly(recs-v2)=beauty [coinciden]
req-demo-03 userSees=electronics shadowOnly(recs-v2)=electronics [coinciden]
req-demo-04 userSees=electronics shadowOnly(recs-v2)=home [difieren]
Fíjate en el patrón: para req-demo-01 y req-demo-02 —compradores con historial de compras—, los dos modelos coinciden siempre, porque comparten exactamente el mismo criterio para ese caso. Para req-demo-03 —un comprador nuevo en la región north— también coinciden, porque la tendencia general del sitio (electronics) resulta ser la misma que la tendencia de esa región. Pero req-demo-04 —un comprador nuevo en la región south— revela la primera diferencia real: recs-v1 recomienda electronics (la tendencia general), mientras recs-v2 recomienda home (la tendencia de esa región específica). Ese comprador, en este momento, nunca ve home — userSees sigue siendo electronics, exactamente como decidió recs-v1. La diferencia solo queda registrada en shadowOnly, disponible para revisión, sin ningún efecto en la experiencia real de compra.
Las dos propiedades que hacen que esto sea, de verdad, "sin riesgo"
Shadow mode solo cumple su promesa si se sostienen dos propiedades, y vale la pena nombrarlas con precisión.
La primera es que el shadow nunca toca la respuesta que recibe el usuario. En el código de esta lección, eso es literal: userSees viene únicamente de recsV1(), y ningún resultado de recsV2() puede, por accidente, filtrarse a esa variable. En un sistema real, esta propiedad se implementa típicamente a nivel de infraestructura —el modelo en shadow corre en un endpoint separado, o recibe una copia asíncrona del tráfico, de forma que un error o una lentitud en recs-v2 ni siquiera pueda demorar la respuesta que el comprador está esperando de recs-v1—. Si el modelo en shadow puede, de alguna forma, afectar la latencia o el contenido de lo que el usuario ve, ya no es shadow mode — es, en la práctica, un experimento con usuarios reales, aunque nadie lo haya llamado así.
La segunda es que el shadow recibe el mismo tráfico real que el modelo en producción, no un conjunto de prueba aparte. Esto es lo que distingue shadow mode de una simple evaluación offline: recs-v2, en este ejemplo, ve exactamente los mismos cuatro requests —mismo comprador, mismo momento, mismos datos de contexto— que vio recs-v1. Esa es la única forma de confirmar cómo se comportaría el modelo nuevo con la variedad real y a veces desordenada del tráfico de producción, en vez de solo con los casos limpios de un conjunto de prueba construido de antemano.
Errores comunes
Correr el modelo nuevo sobre datos de prueba, y llamarlo "shadow mode". Qué pasa: el equipo evalúa recs-v2 contra un conjunto de datos históricos guardado para pruebas, obtiene buenos resultados, y reporta que "ya está validado en shadow" — sin que el modelo haya visto un solo request de tráfico real y en vivo de Mercado. Por qué pasa: correr un modelo contra datos de prueba es más simple de organizar que correrlo en paralelo sobre tráfico de producción, y los dos ejercicios se sienten parecidos porque ambos "no afectan al usuario". Cómo detectarlo: si nadie puede señalar un request real y reciente de un comprador de Mercado que recs-v2 haya procesado, la validación fue offline, no shadow. Cómo corregirlo: como en el ejemplo de esta lección, shadow mode significa que el modelo nuevo recibe una copia del tráfico real que también recibe el modelo en producción — no una muestra guardada de antemano.
Dejar que el modelo en shadow pueda demorar o afectar la respuesta real, "solo un poco". Qué pasa: alguien implementa el shadow de forma que la aplicación espera la respuesta de recs-v2 antes de continuar, aunque después descarte ese resultado — y si recs-v2 es más lento que recs-v1, el comprador nota una demora, incluso sin ver ninguna diferencia en el contenido. Por qué pasa: la forma más simple de programar "llama a los dos modelos" es de forma secuencial y sincrónica, sin pensar en que la latencia del segundo modelo pueda filtrarse a la experiencia del usuario, aunque su contenido no lo haga. Cómo detectarlo: si la latencia percibida por el usuario sube cuando el shadow está activo, comparada con cuando está apagado, el shadow ya está afectando la experiencia real, aunque el contenido de la respuesta sea idéntico. Cómo corregirlo: el shadow debería correr de forma asíncrona o en paralelo genuino —nunca bloqueando la respuesta al usuario—, exactamente la garantía que describe la documentación de shadow testing de AWS SageMaker en los recursos de esta lección.
Dejar el shadow corriendo indefinidamente, sin un plan de qué hacer con lo que se registra. Qué pasa: el equipo activa el shadow de recs-v2, lo deja corriendo durante meses, acumulando registros, pero nunca define cuándo hay suficiente evidencia para tomar una decisión ni quién es responsable de revisarla. Por qué pasa: activar el shadow se siente como el paso importante y difícil; definir el criterio de "cuándo ya sabemos lo suficiente" se siente como un detalle administrativo que se puede posponer. Cómo detectarlo: si nadie puede decir, hoy, cuántos requests lleva acumulados el shadow de recs-v2 ni cuándo se va a revisar el resultado, el shadow es una fuente de datos sin destino. Cómo corregirlo: shadow mode existe para alimentar una decisión concreta —la que construye shadowCompare() en la lección 4—, no como un estado permanente. Define de antemano un volumen o un tiempo razonable de shadow, y qué se hace con el resultado.
Ejercicios
Ejercicio 1 — Un quinto request. Un comprador nuevo (sin historial) navega desde la región east. Usando recsV1() y recsV2() de esta lección, ¿qué ve el comprador (userSees), y qué queda solo registrado en shadow (shadowOnly)? ¿Coinciden o difieren?
Ver solución
recsV1(), para un comprador sin historial, siempre devuelve 'electronics' (la tendencia general), sin importar la región — así que userSees = 'electronics'. recsV2(), para la región 'east', consulta REGION_TRENDING.east, que es 'fashion' — así que shadowOnly = 'fashion'. Los dos difieren: el comprador ve electronics, mientras que en shadow, recs-v2 habría recomendado fashion. Esta diferencia queda registrada, sin ningún efecto en lo que el comprador realmente vio.
Ejercicio 2 — Explica sin usar la palabra "shadow". En dos o tres frases, explica a alguien nuevo en el equipo qué hace serveWithShadow(), sin usar la palabra "shadow" ni "sombra". Puedes usar la analogía del piloto en prácticas.
Ver solución
Un ejemplo de respuesta: "Es como tener a dos personas resolviendo el mismo caso al mismo tiempo, pero solo una de ellas tiene permiso de comunicar su respuesta al cliente. La segunda persona también resuelve el caso, con la misma información, y su respuesta se guarda para comparar después — pero el cliente nunca la ve ni se ve afectado por ella." La idea central, sin el vocabulario técnico: dos decisiones calculadas en paralelo sobre el mismo caso real, pero solo una con permiso de llegar a la persona real.
Ejercicio 3 — Encuentra el riesgo. Un compañero propone una variante de serveWithShadow() que primero llama a recsV2() y, si su resultado "se ve razonable" según una regla simple, lo usa como userSees en vez de recsV1(). ¿Por qué esto ya no sería shadow mode, aunque el compañero insista en llamarlo así?
Ver solución
En cuanto el resultado de recsV2() tiene la posibilidad de convertirse en lo que el comprador realmente ve —aunque sea bajo una condición—, recs-v2 dejó de estar en shadow: está tomando decisiones reales para usuarios reales, sin que nadie haya medido todavía qué tan distinto decide de recs-v1 ni con qué frecuencia. Esto es, en la práctica, un experimento con tráfico real disfrazado de shadow mode. La propiedad que define shadow mode —y que esta variante rompe— es que el modelo en shadow nunca, bajo ninguna condición, determina lo que el usuario recibe; esa decisión completa le pertenece siempre al modelo en producción, hasta que un canary explícito —lección 4 en adelante— decida lo contrario, con evidencia y de forma controlada.
Resumen y siguiente paso
En esta lección construiste recsV1(), recsV2() y serveWithShadow(): el modelo en producción sigue decidiendo todo lo que el comprador ve, mientras el modelo nuevo corre en paralelo, sobre el mismo tráfico real, sin ningún efecto en esa decisión. Confirmaste, con cuatro requests de ejemplo, que los dos modelos coinciden en la mayoría de los casos —los compradores con historial, y los cold-start de una región donde la tendencia local coincide con la general— pero ya empiezan a diferir en al menos uno: un comprador nuevo de una región cuya tendencia es distinta de la general.
Antes de avanzar deberías poder: explicar las dos propiedades que hacen que un shadow sea de verdad "sin riesgo" (nunca afecta la respuesta real, y recibe tráfico real); distinguir shadow mode de una evaluación offline contra datos de prueba; y anticipar por qué necesitas mucho más que cuatro requests para confiar en una conclusión sobre qué tan distinto decide recs-v2.
La lección 4 toma exactamente estas mismas dos funciones, sin cambiarles una línea, y las corre sobre un volumen real de tráfico de shadow — construyendo shadowCompare(), la función que convierte estas comparaciones individuales en una tasa de acuerdo medible, y en un mapa claro de dónde, específicamente, los dos modelos difieren.
Recursos
- Amazon SageMaker AI, "Shadow tests" — docs.aws.amazon.com/sagemaker/latest/dg/shadow-tests.html. Describe la garantía central de esta lección en una plataforma real: "solo las respuestas de la variante en producción se devuelven a la aplicación que llama" — shadow mode implementado a nivel de infraestructura, sin riesgo de que el modelo nuevo afecte al usuario. En inglés.
- 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 patrón de desplegar una versión nueva de un modelo junto a la de producción antes de dirigirle tráfico real, la misma idea que sostiene
serveWithShadow()en esta lección. En inglés.