Módulo 5: Modos de fallo y resiliencia para IA

Proyecto: haz resiliente una feature de IA de Mercado

Descripción

Este es el capstone del módulo. En las siete lecciones anteriores construiste, pieza por pieza, la defensa contra cada modo de fallo de un componente de IA: viste la taxonomía (L2), contuviste la alucinación con verificación (L3), pusiste el timeout contra el modelo caído/lento/rate-limited (L4), degradaste con una cascada de fallback (L5), evitaste el desperdicio con el circuit breaker (L6), y monitoreaste el drift (L7). Ahora las integras todas en una sola feature real de Mercado y la ejecutas de punta a punta. Tomas la búsqueda semántica y le construyes la cáscara de resiliencia completa —timeout + circuit breaker + fallback en cascada + degradación—, mides su disponibilidad con y sin la cáscara, y documentas la decisión en un ADR. Al terminar, tendrás el artefacto que demuestra la capacidad del módulo: una feature de IA cuyo fallo del modelo no tumba el sistema, con el número que lo prueba.

Conexión con el módulo. Esta lección no introduce ideas nuevas; sintetiza las seis anteriores en un sistema ejecutado. Es la contraparte del proyecto del módulo 4 (donde construiste la frontera de confianza) y anticipa el capstone del módulo 8 (donde arquitectarás una feature de IA completa, con la resiliencia de este módulo como una de sus caras). La frontera con la guía de resiliencia se mantiene: la cáscara que construyes aplica timeout, breaker y degradación al modelo; su mecánica a fondo está en resilience-and-reliability-patterns-guide. Aquí demuestras que sabes ubicar y componer esas piezas alrededor de un componente de IA, medir su efecto, y justificar la decisión.

El encargo

Eres el arquitecto de la búsqueda de Mercado. La búsqueda semántica —el LLM que entiende la intención de una query como "audífonos para correr bajo la lluvia"— sale a producción la semana que viene. El equipo de infraestructura te advierte: la API del modelo tiene una disponibilidad histórica del ~92%, con caídas ocasionales de varios minutos y picos de rate limit en las horas de mayor tráfico (que coinciden con las de mayor venta). Tu jefa te da el encargo en una frase: "la búsqueda no se puede caer cuando el modelo se caiga".

Tu entrega tiene cuatro piezas:

  1. El diagrama de la cáscara de resiliencia de la feature.
  2. El código ejecutado de la cáscara (timeout + circuit breaker + fallback en cascada + degradación), con la salida literal.
  3. Un ADR (Architecture Decision Record) que documenta la decisión.
  4. La justificación de por qué, con esta cáscara, el fallo del modelo no tumba el sistema.

El diagrama de la cáscara

Antes del código, el mapa. La cáscara de resiliencia rodea al componente de IA (el LLM tras su frontera) con las tres piezas de disponibilidad, y detrás tiene la cascada de fallback:

flowchart TD
    Q["query del usuario"] --> CB{"circuit breaker<br/>abierto?"}
    CB -- "OPEN: no llamar" --> FB
    CB -- "CLOSED/HALF_OPEN" --> TO["llamar al modelo<br/>con TIMEOUT"]
    TO -- "responde a tiempo" --> OK["resultado semantico<br/>(optimo)"]
    TO -- "falla / timeout / 429" --> REG["registra fallo<br/>en el breaker"]
    REG --> FB["cascada de FALLBACK"]
    FB --> C1{"en cache?"}
    C1 -- "si" --> CACHE["resultado cacheado<br/>(degradado)"]
    C1 -- "no" --> KW["busqueda por keywords<br/>(degradado, red final)"]
    OK --> RESP["respuesta al usuario"]
    CACHE --> RESP
    KW --> RESP
    MON["monitor de drift<br/>(eval continuo)"] -.observa.-> RESP

Léelo de arriba abajo: cada query pasa primero por el circuit breaker (¿está abierto porque el modelo viene fallando?). Si está abierto, va directo al fallback sin tocar el modelo. Si está cerrado (o probando), llama al modelo con timeout. Si el modelo responde a tiempo, resultado óptimo. Si falla, se cuela por timeout, o devuelve 429, se registra el fallo en el breaker y se baja a la cascada de fallback: primero cache, y si no hay, keywords (la red final que nunca falla). El monitor de drift observa la calidad de las respuestas a lo largo del tiempo, fuera del camino del request. Cada pieza es una lección del módulo; el diagrama es el módulo entero compuesto.

El código ejecutado

Construimos la cáscara sobre el modelo simulado. El stream tiene 30 requests con una caída del modelo del request 12 al 20 (nueve requests) y dos cuelgues (requests 5 y 25, que exceden el timeout). Medimos la disponibilidad con la cáscara (timeout + breaker + fallback) contra sin ella (solo semántica). El LLM está simulado por el stub, sin red ni API real.

# Leccion 8 (proyecto): la busqueda semantica de Mercado hecha RESILIENTE.
# Junta timeout + circuit breaker + fallback en cascada + degradacion, todo
# sobre el componente de IA SIMULADO (sin red, sin API). Medimos la
# disponibilidad del sistema CON vs SIN la cascara de resiliencia.

LATENCY_BUDGET_MS = 800


class ModelError(Exception): pass
class ModelTimeout(Exception): pass


CATALOG = [
    (1, "audifonos inalambricos con cancelacion de ruido"),
    (2, "cafetera de goteo programable"),
    (3, "mochila impermeable para laptop"),
    (4, "teclado mecanico retroiluminado"),
    (5, "audifonos deportivos resistentes al agua"),
]
CACHE = {"audifonos": [1, 5], "cafetera": [2]}

# Stream de 30 requests. Cada uno: (query, kind, duration_ms). El modelo cae
# en una ventana (12..20) y hay dos llamadas lentas que exceden el timeout.
import itertools
_QUERIES = ["audifonos", "cafetera", "mochila", "teclado", "audifonos agua"]
_q = itertools.cycle(_QUERIES)
STREAM = []
for i in range(30):
    q = next(_q)
    if 12 <= i <= 20:
        STREAM.append((q, "down", 0))          # caida del modelo
    elif i in (5, 25):
        STREAM.append((q, "slow", 9000))       # cuelgue: excede el presupuesto
    else:
        STREAM.append((q, "ok", 150))


def semantic_search(query, kind, dur):
    if kind == "down":
        raise ModelError("modelo caido")
    if dur > LATENCY_BUDGET_MS:
        raise ModelTimeout("excede el presupuesto de latencia")
    words = query.split()
    return [pid for pid, name in CATALOG if any(w in name for w in words)]


def keyword_search(query):
    words = query.split()
    return [pid for pid, name in CATALOG if any(w in name for w in words)]


class CircuitBreaker:
    def __init__(self, fail_threshold=3, cooldown=4):
        self.fail_threshold = fail_threshold
        self.cooldown = cooldown
        self.fails = 0
        self.state = "CLOSED"
        self.opened_at = None

    def allow(self, now):
        if self.state == "OPEN":
            if now - self.opened_at >= self.cooldown:
                self.state = "HALF_OPEN"
                return True
            return False
        return True

    def on_success(self):
        self.fails = 0
        self.state = "CLOSED"

    def on_failure(self, now):
        self.fails += 1
        if self.fails >= self.fail_threshold:
            self.state = "OPEN"
            self.opened_at = now


def resilient(i, query, kind, dur, cb):
    # Cascara: breaker -> (timeout+modelo) -> cascada de fallback (cache/keyword).
    if cb.allow(i):
        try:
            hits = semantic_search(query, kind, dur)
            cb.on_success()
            return ("semantic", hits, False, "modelo")
        except (ModelError, ModelTimeout):
            cb.on_failure(i)   # cae a la cascada de fallback
    else:
        pass                   # breaker OPEN: ni tocamos el modelo
    if query in CACHE:
        return ("cache", CACHE[query], True, "fallback")
    return ("keyword", keyword_search(query), True, "fallback")


# --- SIN cascara: solo semantica; si el modelo falla, el request se cae ---
responded_A = 0
for i, (q, kind, dur) in enumerate(STREAM):
    try:
        semantic_search(q, kind, dur)
        responded_A += 1
    except (ModelError, ModelTimeout):
        pass
avail_A = responded_A / len(STREAM) * 100

# --- CON cascara ---
cb = CircuitBreaker(fail_threshold=3, cooldown=4)
tiers = {"semantic": 0, "cache": 0, "keyword": 0}
opened_at_req = None
for i, (q, kind, dur) in enumerate(STREAM):
    tier, hits, degraded, _by = resilient(i, q, kind, dur, cb)
    tiers[tier] += 1
    if opened_at_req is None and cb.state == "OPEN":
        opened_at_req = i
responded_B = len(STREAM)
avail_B = responded_B / len(STREAM) * 100
degraded_total = tiers["cache"] + tiers["keyword"]

print("Feature: busqueda semantica de Mercado con cascara de resiliencia")
print(f"  stream de {len(STREAM)} requests; modelo caido en 12..20, "
      f"lento en 5 y 25")
print()
print("Disponibilidad del sistema:")
print(f"  SIN cascara (solo semantica) : {responded_A}/{len(STREAM)} = {avail_A:.1f}%")
print(f"  CON cascara (breaker+timeout+fallback) : "
      f"{responded_B}/{len(STREAM)} = {avail_B:.1f}%")
print()
print("Como se sirvieron los 30 requests CON la cascara:")
print(f"  optimo   (semantic) : {tiers['semantic']}")
print(f"  degradado (cache)   : {tiers['cache']}")
print(f"  degradado (keyword) : {tiers['keyword']}")
print(f"  total degradados (peores, pero validos): {degraded_total}")
print(f"  el breaker se abrio en el request {opened_at_req} y dejo de golpear "
      f"al modelo caido.")

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

Feature: busqueda semantica de Mercado con cascara de resiliencia
  stream de 30 requests; modelo caido en 12..20, lento en 5 y 25

Disponibilidad del sistema:
  SIN cascara (solo semantica) : 19/30 = 63.3%
  CON cascara (breaker+timeout+fallback) : 30/30 = 100.0%

Como se sirvieron los 30 requests CON la cascara:
  optimo   (semantic) : 18
  degradado (cache)   : 6
  degradado (keyword) : 6
  total degradados (peores, pero validos): 12
  el breaker se abrio en el request 14 y dejo de golpear al modelo caido.

Lee los números, porque son la demostración del encargo cumplido.

La disponibilidad: de 63.3% a 100%. Con la caída del modelo (9 requests) y los dos cuelgues, la búsqueda sin cáscara respondió solo 19 de 30 = 63.3% —más de un tercio de los usuarios habrían visto la búsqueda rota durante el incidente—. Con la cáscara, respondió 30 de 30 = 100%: nadie se quedó sin resultados. El encargo de tu jefa —"la búsqueda no se puede caer cuando el modelo se caiga"— está cumplido y medido. Y como en todo el módulo, la clave es que el modelo tiene la misma (mala) disponibilidad en los dos casos; la cáscara hizo que el sistema dejara de depender de él para responder.

El desglose por tier muestra la degradación honesta. De los 30 requests, 18 se sirvieron óptimos (semántica, cuando el modelo estaba sano) y 12 degradados —6 por cache, 6 por keyword—. Esos 12 son los requests durante la caída y los cuelgues: se sirvieron peor (sin comprensión semántica) pero válidos (con productos relevantes), en vez de fallar. Sin la cáscara, esos 12 habrían sido 11 errores (19 respondieron, no 18, porque sin breaker cada request de la caída intenta y falla individualmente). La feature degrada elegantemente: peor, no rota.

El breaker se abrió en el request 14. La caída empieza en el 12; el breaker cuenta 3 fallos (12, 13, 14) y se abre en el 14, dejando de golpear al modelo caído durante el resto de la ventana —yendo directo al fallback y ahorrando timeouts y llamadas—, con intentos de prueba periódicos hasta que el modelo se recupera pasado el request 20. Es todo el ciclo de vida del breaker de la lección 6, ahora dentro de la feature completa.

La demostración: cada pieza del módulo hizo su parte, y juntas convirtieron una feature 63% disponible en una 100% disponible. El timeout atrapó los cuelgues (5 y 25); el breaker dejó de martillear el modelo caído (desde el 14); la cascada de fallback sirvió cache y keywords cuando el modelo no estaba; la degradación marcó esas 12 respuestas como lo que son. La no-determinación y la fragilidad del componente de IA quedaron contenidas por una cáscara determinista.

El ADR de la decisión

Un ADR (Architecture Decision Record) documenta una decisión arquitectónica: su contexto, la decisión tomada, las alternativas, y las consecuencias. Este es el ADR de tu entrega:

# ADR-005: Cascara de resiliencia para la busqueda semantica

## Estado
Aceptado

## Contexto
La busqueda semantica de Mercado depende de una API de modelo (LLM) con
~92% de disponibilidad historica, con caidas de varios minutos y picos de
rate limit en horas de alto trafico (que coinciden con las de alta venta).
Sin proteccion, la disponibilidad de la busqueda seria, en el mejor caso,
la del modelo (~92%), y una caida del modelo dejaria la busqueda sin
funcionar por completo, con perdida directa de trafico y ventas. La
busqueda es un camino de negocio critico.

## Decision
Envolver la llamada al modelo en una CASCARA DE RESILIENCIA con cuatro
piezas, aplicadas al componente de IA:
  - TIMEOUT (800 ms, derivado del presupuesto de latencia del modulo 2):
    ninguna query espera al modelo mas que el presupuesto.
  - CIRCUIT BREAKER sobre el modelo (umbral 3 fallos, enfriamiento): deja
    de llamar al modelo cuando viene fallando, para no desperdiciar
    latencia/costo ni empeorar un rate limit.
  - FALLBACK EN CASCADA: semantica -> cache -> keywords. El nivel final
    (keywords) es determinista y local: no comparte la dependencia fragil
    (la API del modelo), asi que nunca falla.
  - DEGRADACION marcada: las respuestas de fallback se etiquetan como
    degradadas, para el monitoreo y (opcionalmente) para el usuario.
Ademas, un MONITOR DE DRIFT corre el eval en produccion de forma continua
para detectar degradacion silenciosa de la calidad (modulo 3 + 7).
La MECANICA de timeout/breaker/degradacion se toma de la guia de
resiliencia; aqui se aplica al componente de IA.

## Alternativas consideradas
  - Llamada directa al modelo sin proteccion: RECHAZADA. Disponibilidad de
    la busqueda = disponibilidad del modelo (~63% medido en el incidente
    simulado); la busqueda se cae con el modelo. Incumple el requisito.
  - Solo fallback, sin timeout ni breaker: RECHAZADA. El fallback sin
    timeout no protege contra cuelgues (se congela); sin breaker,
    desperdicia timeout/costo/cuota en cada request durante una caida larga.
  - Fallback a un modelo mas pequeno del mismo proveedor como nivel final:
    RECHAZADA como NIVEL FINAL. Comparte la dependencia fragil (la API del
    proveedor); si cae el proveedor entero, cae con el. Aceptable como nivel
    INTERMEDIO, pero el nivel final debe ser determinista y local.

## Consecuencias
  + La disponibilidad de la busqueda se DESPEGA de la del modelo:
    100% medido vs 63.3% sin cascara, ante una caida + dos cuelgues.
  + Durante una caida, la busqueda degrada (keywords/cache) en vez de caer.
  + El breaker ahorra latencia, costo y cuota durante las caidas.
  - Durante la degradacion, la calidad de la busqueda baja (sin comprension
    semantica); se mitiga marcando y monitoreando la proporcion degradada.
  - Complejidad extra: hay que mantener el fallback por keywords, el cache,
    y calibrar timeout/umbral/enfriamiento (ver guia de resiliencia).
  - El monitor de drift requiere correr el eval en produccion de forma
    continua (costo operativo, justificado por atrapar el drift temprano).

La justificación

La pregunta que cierra el módulo: ¿por qué, con esta cáscara, el fallo del modelo ya no tumba el sistema? La respuesta tiene tres capas, y conviene decirlas explícitas porque son la tesis del módulo hecha argumento.

Primero: la disponibilidad del sistema se desacopló de la del modelo. El componente más frágil del sistema —el modelo, ~92% disponible, con caídas— dejó de ser el que determina si la búsqueda funciona. Lo logra el fallback con un nivel final determinista y local (keywords en memoria), que no comparte la dependencia frágil y por eso responde aunque el proveedor entero caiga. El número lo prueba: 100% de sistema sobre un modelo que en el incidente estuvo al 63%. La resiliencia no eliminó el fallo del modelo (el modelo cayó igual); desacopló al sistema de ese fallo.

Segundo: los fallos se contienen en el momento y en el tiempo. En el momento del request, el timeout evita que un cuelgue congele el sistema, el breaker evita que una caída larga desperdicie recursos, y la cascada da la ruta alterna. A lo largo del tiempo, el monitor de drift atrapa la degradación silenciosa que ninguna defensa por-request puede ver. Las dos dimensiones del fallo —instantánea (caído/lento/rate-limited/alucinación) y temporal (drift)— quedan cubiertas.

Tercero: la degradación es honesta y elegida. El sistema no finge que todo va bien durante una caída: sirve respuestas degradadas marcadas como tales, monitorea la proporción degradada, y trata la degradación como una decisión de diseño deliberada —"prefiero servir peor a no servir"— no como un accidente. Eso es lo que separa un sistema resiliente de uno que simplemente esconde sus fallos: el resiliente sabe cuándo está degradado y lo dice.

La síntesis, que enlaza con el módulo 6: la no-determinación y la fragilidad del componente de IA quedaron contenidas por una cáscara determinista. El LLM propone la mejor respuesta cuando puede; cuando no puede —porque cayó, se colgó, o se está rate-limitando—, una capa determinista dispone la ruta alterna. El núcleo probabilístico y frágil quedó pequeño, envuelto, y contenido. Esa es la arquitectura de un sistema AI-native que sobrevive a sus propias piezas de IA.

Errores comunes

Componer las piezas en el orden equivocado. Qué pasa: el equipo pone el fallback pero llama al modelo sin timeout, o pone el breaker después de un timeout infinito; el sistema tiene todas las piezas pero mal ordenadas, y un cuelgue lo congela igual porque el timeout —que debe ir primero, convirtiendo el cuelgue en excepción— no está o está mal puesto. Por qué pasa: se juntan las piezas sin respetar su interdependencia (lección 6, ejercicio 3). Cómo detectarlo: revisa que el timeout envuelva cada llamada, que el breaker cuente los fallos que el timeout genera, y que el fallback responda cuando cualquiera de los dos corta. Cómo corregirlo: el orden es breaker (¿llamo?) → timeout (llamo acotado) → registro del fallo → cascada de fallback. El ejemplo lo compone así.

Un fallback que comparte la dependencia frágil. Qué pasa: el nivel final de la cascada llama a otra API externa (o al mismo proveedor), y el día de una caída del proveedor entero, todo cae junto. Por qué pasa: se pensó el fallback como "otra forma de resolver" sin verificar que sea independiente del fallo que cubre. Cómo detectarlo: tu nivel final de fallback tiene una dependencia de red o de proveedor. Cómo corregirlo: el nivel final debe ser determinista y local (keywords en memoria, plantilla, cache local) —la red de seguridad que sobrevive a la caída que cubre—. El ADR lo marca como alternativa rechazada.

Entregar la cáscara sin medir su efecto. Qué pasa: el equipo construye timeout + breaker + fallback, se ve bien, y lo despliega sin nunca medir la disponibilidad con y sin la cáscara —así que no sabe si de verdad funciona ni tiene el número para justificar la complejidad extra—. Por qué pasa: "se ve resiliente" se confunde con "es resiliente". Cómo detectarlo: no puedes citar tu disponibilidad con vs sin la cáscara ante un incidente simulado. Cómo corregirlo: simula el fallo y mide —como el ejemplo: 63.3% sin cáscara, 100% con— porque la resiliencia que no mediste es una hipótesis, no una propiedad. El número es lo que convierte la cáscara en una decisión defendible en el ADR.

Ejercicios

Ejercicio 1 — Adapta la cáscara al agente de soporte. El proyecto construyó la cáscara para la búsqueda semántica. Adáptala al agente de soporte de Mercado (que responde preguntas de clientes y a veces toca datos de pedidos). Di qué cambia en cada pieza: (a) el nivel final del fallback; (b) qué defensa adicional necesita el agente que la búsqueda no; (c) por qué el timeout y el breaker aplican igual.

Ver solución
  • (a) El nivel final del fallback: en la búsqueda era keywords (determinista, local). En el agente de soporte, el nivel final es escalar a un humano —el agente humano es la red de seguridad universal que resuelve cualquier caso—. La cascada sería: agente LLM → respuestas de plantilla para preguntas frecuentes → humano. El último recurso es humano, no determinista, porque el soporte toca casos (dinero, reclamos) donde un humano es la única garantía (lección 5, ejercicio 1).
  • (b) La defensa adicional que necesita el agente: la verificación contra la fuente de verdad (lección 3), porque el agente afirma datos factuales (estado del pedido, guía) que puede alucinar. La búsqueda devuelve productos (verificables por "¿existe/está activo?", una verificación ligera), pero el agente afirma hechos sobre pedidos que hay que confrontar contra el registro real. Sin esa verificación, el agente serviría alucinaciones aunque el modelo estuviera 100% disponible —es un fallo de contenido, no de disponibilidad—. La cáscara de disponibilidad (timeout/breaker/fallback) no cubre la alucinación; el agente necesita las dos defensas.
  • (c) Timeout y breaker aplican igual: porque el agente también depende de la misma API de modelo lenta y con cuotas. Un modelo caído/lento/rate-limited afecta al agente igual que a la búsqueda, así que el timeout (no esperar para siempre) y el breaker (dejar de golpear un modelo caído) son idénticos. Lo que cambia es el destino del fallback (humano en vez de keywords) y la defensa extra (verificación de contenido). Las piezas de disponibilidad son las mismas; el contenido del fallback y las defensas de contenido cambian según la feature.

Ejercicio 2 — El número que justifica la complejidad. Tu jefa pregunta: "esta cáscara agrega complejidad —hay que mantener el fallback, el cache, calibrar el breaker—; ¿vale la pena?". Usa los números del proyecto y una estimación de negocio para justificar (o matizar) la decisión. Supón que la búsqueda genera 100,000 requests al día y que el modelo tiene incidentes que suman ~8 horas al mes.

Ver solución

Con el número del proyecto: sin cáscara, la disponibilidad de la búsqueda cae a ~63% durante los incidentes (y en operación normal, al ~92% del modelo). Con cáscara, 100% en ambos casos.

Estimación de negocio: 100,000 requests/día. Los incidentes del modelo suman ~8 h/mes ≈ ~1.1% del tiempo, pero concentrados y, según el enunciado del proyecto, en horas de alto tráfico y alta venta. Durante esas 8 horas, sin cáscara, más de un tercio de las búsquedas fallan (o, en una caída total, todas). Si en esas horas caen ~30,000 búsquedas al mes y una fracción de las búsquedas se convierte en venta, cada búsqueda fallida es una venta potencialmente perdida, además del daño a la confianza (un usuario que ve la búsqueda rota puede no volver). Contra eso, el costo de la cáscara es: mantener un fallback por keywords (código simple, ya lo tenías para búsqueda básica), un cache (que además sirve para latencia/costo, módulo 2), y calibrar tres parámetros (una vez, con guía).

La justificación: la complejidad de la cáscara es acotada y reusable (el fallback y el cache tienen otros usos), mientras el costo de no tenerla es variable y golpea en el peor momento (los incidentes coinciden con las horas de venta). La decisión es claramente favorable para un camino de negocio crítico como la búsqueda. Matiz honesto: para una feature no crítica y de bajo tráfico (por ejemplo, un generador de descripciones que un vendedor usa una vez), la cáscara completa podría ser sobre-ingeniería —bastaría un fallback simple sin breaker ni cache—. La regla: la inversión en resiliencia se dimensiona según la criticidad y el tráfico de la feature. Para la búsqueda, la cáscara completa vale; el número (63% → 100% en el peor momento) lo respalda.

Ejercicio 3 — El ADR que faltó una consecuencia. Todo ADR honesto lista sus consecuencias negativas, no solo las positivas. El ADR del proyecto lista varias. Identifica una consecuencia negativa adicional de esta cáscara que el ADR no menciona explícitamente, explica por qué es un costo real, y propón cómo mitigarla.

Ver solución

Una consecuencia negativa adicional no listada explícitamente: el riesgo de que la degradación silenciosa esconda un problema del modelo. Como la cáscara hace que el sistema responda el 100% incluso cuando el modelo está caído, un equipo que solo mire "¿la búsqueda responde?" podría no darse cuenta de que el modelo lleva días degradado o caído, porque la métrica de disponibilidad se ve perfecta (100%) mientras casi todo se sirve por fallback de baja calidad. La cáscara, al hacer bien su trabajo, oculta el fallo subyacente. Es el reverso de su virtud.

Por qué es un costo real: podrías estar sirviendo búsqueda por keywords (mala calidad) durante una semana sin enterarte, perdiendo conversión, porque el dashboard de disponibilidad dice "100%, todo bien". El fallback resolvió el síntoma (disponibilidad) y tapó la enfermedad (el modelo caído).

Cómo mitigarla: monitorear la proporción degradada como una métrica de primera clase, con su propia alerta. No basta con medir "¿respondió?"; hay que medir "¿respondió óptimo o degradado?", y alertar si la proporción degradada supera un umbral por un tiempo sostenido. En el ejemplo, si de repente el 40% de las respuestas son por fallback, hay una caída del modelo en curso aunque la disponibilidad total sea 100%. Esto conecta con la lección 5 (marcar lo degradado) y la 7 (monitorear). El ADR debería agregar esta consecuencia y su mitigación: "la cáscara puede ocultar un fallo prolongado del modelo; se mitiga alertando sobre la proporción degradada, no solo sobre la disponibilidad total". Un buen ADR nombra incluso los costos de sus propias virtudes.

Resumen y siguiente paso

En este capstone integraste las seis lecciones del módulo en una feature real de Mercado ejecutada: le construiste a la búsqueda semántica la cáscara de resiliencia completa —timeout + circuit breaker + fallback en cascada + degradación, con un monitor de drift observando— y demostraste el encargo con un número: la disponibilidad pasó de 63.3% sin cáscara a 100% con cáscara ante una caída del modelo y dos cuelgues, con 18 respuestas óptimas y 12 degradadas pero válidas, y el breaker abriéndose en el request 14. Entregaste las cuatro piezas: el diagrama de la cáscara, el código ejecutado, el ADR con sus alternativas y consecuencias, y la justificación de por qué el fallo del modelo ya no tumba el sistema —porque la disponibilidad se desacopló del modelo, los fallos se contienen en el momento y en el tiempo, y la degradación es honesta y elegida—.

Con esto cierras el módulo 5. Deberías poder, ahora, tomar cualquier feature de IA y diseñar su resiliencia: nombrar sus modos de fallo (disponibilidad, contenido, temporal), poner las defensas que corresponden (timeout, fallback, breaker, verificación, monitoreo), componerlas en el orden correcto, y medir su efecto con un incidente simulado. Y sabes dónde está la frontera: aquí aplicaste los patrones de resiliencia al componente de IA; su mecánica a fondo vive en resilience-and-reliability-patterns-guide.

Hacia dónde sigue la guía: el módulo 6, la cáscara determinista, generaliza lo que hiciste aquí —el modelo propone, el sistema dispone— a su forma más fuerte: una capa determinista que valida lo que el modelo sugiere antes de tocar dinero o estado (el LLM nunca ejecuta un reembolso directo; propone, y reglas deterministas aprueban o rechazan). La resiliencia de este módulo es una cara de esa cáscara. Y el módulo 7, el lazo de datos, toma el monitor de drift de la lección 7 y lo convierte en el flywheel completo de observabilidad y retroalimentación. Más allá de esta guía: para construir las piezas de IA (RAG, agentes, evals, fine-tune) el destino es el ecosistema de AI Engineering; para la mecánica de resiliencia a fondo, resilience-and-reliability-patterns-guide; para las decisiones y tradeoffs arquitectónicos, architecture-decisions-and-tradeoffs-guide.

Recursos

  • resilience-and-reliability-patterns-guide (este ecosistema) — la referencia para la mecánica de todo lo que compusiste: M2 timeouts, M5 circuit breakers, M7 degradación. Su propio capstone (M8, "hacer resiliente el checkout de Mercado") es el hermano de este proyecto, aplicado a un servicio clásico en vez de a un componente de IA. En español.
  • architecture-for-ai-native-systems-guide, Módulo 6 (la cáscara determinista) y Módulo 7 (el lazo de datos) — hacia donde sigue la guía; generalizan la contención y el monitoreo que este proyecto usó. Y Módulo 3 (el eval) y Módulo 2 (latencia/costo), que este proyecto reusa. En español.
  • Martin Fowler, "Architecture Decision Records" y (Bharani Subramaniam y Martin Fowler) "Emerging Patterns in Building GenAI Apps" — martinfowler.com. El formato del ADR que usaste y el mapa de patrones de resiliencia alrededor de un componente de IA. En inglés.
  • Chip Huyen, AI Engineering (O'Reilly, 2024). Los capítulos sobre confiabilidad, costo y monitoreo integran las piezas de este módulo en el diseño operativo de una aplicación con modelos de fundación. En inglés.
  • Para construir las piezas de IA (RAG, agentes, evals, fine-tuning) que aquí solo tratamos como componentes con propiedades de fallo: el ecosistema de AI Engineering, fuera del alcance de esta guía. En español/inglés.