Módulo 6: La cáscara determinista

El modelo propone, el sistema dispone

Descripción

La lección 1 instaló la tesis del módulo en una frase: el modelo propone, el sistema dispone. Esta lección la convierte en el principio raíz del que salen todos los demás, y lo hace de la única forma que convence de verdad: poniendo lado a lado dos diseños del agente de soporte de Mercado que reciben exactamente las mismas propuestas del modelo, y midiendo cuánto dinero mueve cada uno. En el diseño ingenuo, la salida del LLM ejecuta la acción directamente: el modelo dice "reembolsa" y el dinero se mueve, sin nadie que dispute. En el diseño de cáscara, la salida del LLM es una propuesta que una capa determinista valida antes de ejecutar. Mismo modelo, mismas propuestas, mismos errores; lo único que cambia es quién tiene la última palabra sobre el dinero.

La diferencia que vas a medir es brutal y es el punto entero: el diseño ingenuo pagó $5315 y ejecutó seis acciones —cuatro de ellas peligrosas—; el de cáscara pagó $125 y ejecutó dos. La cáscara no hizo al modelo mejor: hizo que el modelo dejara de tener el poder de ejecutar. Ese traspaso de poder —de "el LLM hace" a "el LLM sugiere y el código decide"— es el antipatrón raíz que este módulo corrige, y verlo en dinero lo vuelve imposible de ignorar.

Conexión con el módulo. Esta es la lección-principio, la que desarrolla la tesis de la 1 hasta el fondo. Todas las lecciones que siguen son cómo se dispone: la acción estructurada (L3) es el formato en que el modelo propone; las capabilities acotadas (L4) son el menú de lo que puede proponer; la validación de reglas (L5) es la política contra la que se dispone; el núcleo pequeño (L6) es cuánto se deja en manos del modelo; el pipeline (L7) las ensambla. Aquí establecemos por qué proponer y no ejecutar, con dinero. La frontera con AI Engineering se mantiene: no hablamos de cómo hacer que el modelo proponga mejor (mejor prompt, tool use, function calling) —eso es AI Eng—; hablamos de por qué, proponga lo que proponga, no debe ejecutar.

Una analogía: el mesero que trae la cuenta y el mesero que cobra de tu tarjeta

Imagina dos restaurantes con dos formas distintas de cobrar. En el primero, el mesero trae la cuenta a tu mesa: un papel con lo que pediste y el total. Tú la revisas —"esto no lo ordené", "aquí cobraron de más"— y solo entonces pagas lo que apruebas. El mesero propone un cobro; tú dispones si se ejecuta. Si el mesero se equivocó, apuntó un platillo de otra mesa, o infló el total, tú lo atrapas antes de que el dinero se mueva, porque hay un paso de aprobación entre la propuesta (la cuenta) y la ejecución (el pago).

En el segundo restaurante, el mesero tiene tu tarjeta desde que llegaste y cobra directamente lo que él cree que pediste, sin traerte la cuenta. Si acierta, no pasa nada. Pero si se equivoca —te cobra un platillo que no ordenaste, apunta mal el total, o pone en tu cuenta lo de la mesa de al lado—, el dinero ya salió: no hubo un paso de aprobación donde atraparlo. Tu única opción es reclamar después, pelear un reembolso, revisar tu estado de cuenta semanas después y descubrir el error cuando ya es un problema.

Los dos meseros pueden ser igual de listos y de bien intencionados. La diferencia no está en el mesero: está en si hay un paso de aprobación entre la propuesta y la ejecución. El primer restaurante lo tiene —la cuenta que revisas—; el segundo no. Y por eso, aunque los dos meseros se equivoquen con la misma frecuencia, en el primero los errores se atrapan antes de tocar tu dinero y en el segundo se convierten en cargos que ya pasaron. Tu agente de soporte es el mesero: el diseño ingenuo le da tu tarjeta (ejecuta directo); el diseño de cáscara le hace traer la cuenta (propone, y el código dispone).

Ejemplo trabajado: mismo modelo, dos diseños, la diferencia en dinero

Vamos a medir el traspaso de poder. Tomamos las mismas seis propuestas que el núcleo probabilístico generó en la lección 1 —las mismas buenas y las mismas malas— y las corremos por dos diseños. En el diseño A (ingenuo), cada propuesta del modelo se ejecuta directo: el dinero se mueve. Para ilustrar el daño, corremos una validación que solo etiqueta qué salió mal, pero que no frena nada —porque en el diseño ingenuo no hay nada que frene—. En el diseño B (cáscara), la misma validación decide: solo se ejecuta lo que pasa.

# Modulo 6, Leccion 2: el modelo PROPONE, el sistema DISPONE.
# Comparamos DOS disenos con las MISMAS propuestas del LLM (simulado):
#   A) ingenuo : el LLM EJECUTA la accion directamente (toca el dinero).
#   B) cascara : el LLM PROPONE y una capa determinista DISPONE (valida antes).
# Sin red, sin API, sin claves. Datos fijos.

ORDERS = {
    "A-1001": {"total": 50.00,  "days_since_delivery": 3,  "refunded": False},
    "A-1002": {"total": 120.00, "days_since_delivery": 45, "refunded": False},
    "A-1003": {"total": 30.00,  "days_since_delivery": 5,  "refunded": True},
    "A-1005": {"total": 75.00,  "days_since_delivery": 8,  "refunded": False},
}
MAX_REFUND = 100.00
RETURN_WINDOW_DAYS = 30

# Las MISMAS propuestas que el nucleo probabilistico genero en la leccion 1.
PROPOSALS = [
    {"action": "refund", "order_id": "A-1001", "amount": 50.00},
    {"action": "refund", "order_id": "A-1002", "amount": 120.00},
    {"action": "refund", "order_id": "A-1003", "amount": 30.00},
    {"action": "refund", "order_id": "A-9999", "amount": 40.00},
    {"action": "refund", "order_id": "A-1005", "amount": 5000.00},
    {"action": "refund", "order_id": "A-1005", "amount": 75.00},
]


def validate(proposal):
    oid, amount = proposal["order_id"], proposal["amount"]
    if oid not in ORDERS:
        return (False, "pedido no existe")
    order = ORDERS[oid]
    if order["refunded"]:
        return (False, "ya reembolsado")
    if order["days_since_delivery"] > RETURN_WINDOW_DAYS:
        return (False, "fuera de ventana")
    if amount > order["total"]:
        return (False, "monto > total")
    if amount > MAX_REFUND:
        return (False, "monto > limite")
    return (True, "aprobado")


# --- Diseno A: ingenuo. El LLM ejecuta; el dinero se mueve sin preguntar. ---
paid_A = 0.0
executed_A = 0
damage = []
for p in PROPOSALS:
    # No hay capa que dispute: lo que el LLM propuso, se ejecuta.
    paid_A += p["amount"]
    executed_A += 1
    ok, reason = validate(p)   # solo para ETIQUETAR el dano, NO frena nada
    if not ok:
        damage.append((p["order_id"], p["amount"], reason))

# --- Diseno B: cascara. El LLM propone; la capa determinista dispone. ---
paid_B = 0.0
executed_B = 0
for p in PROPOSALS:
    ok, _ = validate(p)
    if ok:
        paid_B += p["amount"]
        executed_B += 1

print("=== Diseno A: el LLM EJECUTA directamente (ingenuo) ===")
print(f"  acciones ejecutadas : {executed_A}/{len(PROPOSALS)}")
print(f"  dinero pagado       : {paid_A:.2f}")
print("  danos que salieron (dinero real perdido o mal movido):")
for oid, amt, reason in damage:
    print(f"    - {oid:<7} {amt:>8.2f}  ({reason})")

print()
print("=== Diseno B: el LLM PROPONE, la cascara DISPONE ===")
print(f"  acciones ejecutadas : {executed_B}/{len(PROPOSALS)}")
print(f"  dinero pagado       : {paid_B:.2f}")

print()
print("=== La diferencia que hace la cascara ===")
print(f"  ejecuciones peligrosas evitadas : {executed_A - executed_B}")
print(f"  dinero protegido                : {paid_A - paid_B:.2f}")

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

=== Diseno A: el LLM EJECUTA directamente (ingenuo) ===
  acciones ejecutadas : 6/6
  dinero pagado       : 5315.00
  danos que salieron (dinero real perdido o mal movido):
    - A-1002    120.00  (fuera de ventana)
    - A-1003     30.00  (ya reembolsado)
    - A-9999     40.00  (pedido no existe)
    - A-1005   5000.00  (monto > total)

=== Diseno B: el LLM PROPONE, la cascara DISPONE ===
  acciones ejecutadas : 2/6
  dinero pagado       : 125.00

=== La diferencia que hace la cascara ===
  ejecuciones peligrosas evitadas : 4
  dinero protegido                : 5190.00

Compara las dos corridas, porque la diferencia entre ellas es la lección entera.

El diseño ingenuo ejecutó las seis propuestas. No porque el modelo fuera peor —es el mismo modelo, las mismas propuestas—, sino porque no hay ninguna capa entre la propuesta y la ejecución. Lo que el modelo propuso, se hizo. Pagó $5315, y la lista de daños muestra qué se movió que no debía: un reembolso fuera de ventana ($120), un doble reembolso ($30), un pago a un pedido inexistente ($40) y —el que duele— un reembolso de $5000 de un pedido de $75. Fíjate en un detalle importante: la validación sí existía en el diseño A, pero solo etiquetaba el daño después de que ya pasó. Validar sin poder de veto no es una cáscara; es una autopsia. Saber después que reembolsaste $5000 de más no te devuelve los $5000.

El diseño de cáscara ejecutó dos. Mismas seis propuestas, misma validación —pero ahora la validación decide, no solo etiqueta—. Las dos legítimas (A-1001 por $50, A-1005 por $75) pasaron; las cuatro peligrosas se bloquearon antes de mover un centavo. Pagó $125. La cáscara no cambió lo que el modelo propuso; cambió quién tiene la última palabra: en el diseño A la tiene el modelo, en el B la tiene el código determinista.

La diferencia, en el número que importa: $5190 protegidos y 4 ejecuciones peligrosas evitadas. Ese es el valor exacto de mover el poder de ejecutar del modelo a la cáscara. Y observa que no hicimos nada para mejorar el modelo —no cambiamos su prompt, no lo hicimos más listo, no redujimos su tasa de error—. Solo le quitamos el poder de ejecutar directamente y se lo dimos a una capa determinista. Esa es la esencia del principio: la seguridad no viene de un modelo mejor, viene de que el modelo no ejecute.

Profundización: por qué "ejecutar lo que el modelo dijo" es el antipatrón raíz

El diseño ingenuo tiene un nombre honesto: acoplar la ejecución a la salida del modelo. Y es el antipatrón del que se derivan casi todos los desastres de las apps AI-native que tocan estado. Vale la pena entender por qué es tan tentador y por qué es tan peligroso.

Es tentador porque es el camino más corto. El modelo ya "sabe" qué hay que hacer —lo dice en su respuesta—, así que conectar esa respuesta directo a la acción parece eliminar un paso inútil. "¿Para qué validar de nuevo lo que el modelo ya decidió?" La respuesta es que el modelo no decidió nada con garantía: propuso algo con una probabilidad de estar bien. Conectar la propuesta directo a la ejecución trata una sugerencia probabilística como si fuera una orden verificada, y esa confusión es la raíz del problema.

Es peligroso porque las acciones son irreversibles. Un texto malo se puede filtrar, corregir, o simplemente no mostrar. Una acción mala —dinero que salió, un pedido que se canceló, una cuenta que se dio de baja— ya pasó. El módulo 4 protegía contra salidas malas que muestras; este módulo protege contra acciones malas que ejecutas, y la diferencia es que en el segundo caso no hay "deshacer" gratis. Por eso la validación tiene que ir antes de la ejecución, no después: después es una autopsia, antes es una cáscara.

   ANTIPATRON (ingenuo):                    PATRON (cascara):

   mensaje del cliente                      mensaje del cliente
          │                                        │
          ▼                                        ▼
   ┌─────────────┐                          ┌─────────────┐
   │   el LLM    │                          │   el LLM    │
   │  (propone)  │                          │  (propone)  │
   └─────────────┘                          └─────────────┘
          │                                        │
          │  la salida ejecuta                     │  la salida es una PROPUESTA
          ▼                                        ▼
   ┌─────────────┐                          ┌─────────────┐
   │   dinero    │                          │   cascara   │  valida contra reglas
   │  / estado   │  ◄── ya paso             │determinista │  ┌──────────────┐
   └─────────────┘                          └─────────────┘  │ pasa?  no ── bloquea
                                                   │         └──────────────┘
                                                   ▼ si
                                            ┌─────────────┐
                                            │   dinero    │
                                            │  / estado   │  ◄── solo lo aprobado
                                            └─────────────┘

El paso de aprobación es el corazón del patrón. Mira los dos diagramas: la única diferencia estructural es que el patrón inserta una caja —la cáscara determinista— entre la propuesta y el efecto. Esa caja es el mesero que trae la cuenta, el gerente que autoriza, la envolvente que recorta. No cambia lo que el modelo propone; cambia que su propuesta pase por una aprobación determinista antes de tocar nada. Todo el resto del módulo es qué hay dentro de esa caja: cómo recibe la propuesta (acción estructurada, L3), qué acciones acepta siquiera considerar (capabilities, L4), contra qué las valida (reglas de negocio, L5). Pero la idea es esta: existe una caja, y el modelo no puede saltársela.

Validar sin poder de veto no cuenta. Un error sutil que el ejemplo deja claro: el diseño A tenía la validación —la misma función validate—, pero solo la usaba para etiquetar el daño, no para frenarlo. Esto pasa en la vida real más de lo que parece: equipos que loguean "esta acción parece fuera de política" mientras la ejecutan de todos modos, y creen que están protegidos porque "lo estamos monitoreando". Monitorear un desfalco mientras ocurre no es contenerlo. La validación solo es cáscara si tiene poder de veto: si su "no" detiene la ejecución. Un "no" que solo se anota en un log es una autopsia con buena documentación.

El peso de la cáscara se calibra a la reversibilidad de la acción. No toda acción del modelo necesita la misma contención, y entender por qué afina el diseño. Lo que hace peligroso al diseño ingenuo es que las acciones de este ejemplo —mover dinero— son irreversibles: una vez pagado el reembolso, no hay un "deshacer" gratis. Para una acción irreversible, la validación tiene que ir antes de ejecutar, porque después ya no hay remedio. Pero hay acciones del modelo que son reversibles o de bajo riesgo —guardar un borrador, reenviar un correo, marcar algo para revisión— donde un error se corrige sin costo real. La regla de diseño que se desprende: la cantidad de cáscara que una acción necesita es proporcional a su radio de daño. Reembolsar exige el pipeline completo de compuertas que verás en las lecciones siguientes; reenviar una factura exige apenas un chequeo básico. Confundir los dos extremos cuesta de las dos formas: poner poca cáscara sobre una acción irreversible es el desastre del diseño A; poner cáscara pesada sobre una acción trivial es fricción inútil. La disciplina es reservar el peso de la contención para donde el daño de una alucinación de verdad importa —lo irreversible, lo que toca dinero o estado crítico— y aligerarla donde no. En este módulo trabajamos el caso difícil (el reembolso) porque es el que fija el patrón; el caso fácil es el mismo patrón con menos reglas.

Errores comunes

Conectar la salida del modelo directo a la API que toca dinero. Qué pasa: el agente detecta que hay que reembolsar y llama directo a la API de reembolsos con lo que el modelo dijo. Es el diseño A. Funciona en las pruebas —el modelo propuso bien casi siempre— hasta el día que propone $5000 o un pedido inexistente, y para entonces el dinero ya salió. Por qué pasa: es el camino más corto y parece innecesario validar "lo que el modelo ya decidió". Cómo detectarlo: si entre la salida del modelo y el efecto sobre el dinero no hay una capa con poder de veto, tienes el antipatrón. Cómo corregirlo: inserta la cáscara —la caja del diagrama— y haz que la salida del modelo sea una propuesta que la cáscara valida, no una orden que ejecutas.

Poner la validación después de ejecutar, no antes. Qué pasa: el equipo sí valida, pero lo hace después de mover el dinero —"reembolsamos y luego revisamos si estuvo bien"—. Como las acciones son irreversibles, la validación tardía solo descubre el daño; no lo evita. Por qué pasa: a veces por diseño (parece más simple ejecutar primero), a veces por un mal orden de las operaciones. Cómo detectarlo: en tu flujo, ¿el if que decide si la acción es válida corre antes o después de la línea que la ejecuta? Si es después, estás haciendo autopsias. Cómo corregirlo: la validación va antes de la ejecución, siempre. La acción solo se ejecuta en la rama donde la validación pasó.

Confundir monitorear con contener. Qué pasa: el sistema loguea cada acción sospechosa —"reembolso fuera de política detectado"— pero la ejecuta igual, y el equipo cree que está protegido porque "tiene observabilidad". Es exactamente el diseño A: validación que etiqueta pero no veta. Por qué pasa: el logging es fácil de agregar y da una falsa sensación de control. Cómo detectarlo: pregúntate si tus alertas de "acción peligrosa" se disparan antes de que la acción ocurra (y la detienen) o después (y solo la reportan). Cómo corregirlo: la validación tiene que tener poder de veto —su "no" detiene la ejecución—. Monitorear está bien y es útil (el log de auditoría de la lección 7 lo usa), pero no sustituye a la contención: son cosas distintas, y solo la segunda protege el dinero.

Ejercicios

Ejercicio 1 — Los dos meseros. Explica, con la analogía de los dos restaurantes, por qué el diseño ingenuo (diseño A) y el de cáscara (diseño B) pueden usar el mismo modelo y aun así uno pagar $5315 y el otro $125. ¿Qué elemento tiene el segundo restaurante que el primero no, y a qué corresponde en el código?

Ver solución

Los dos diseños usan el mismo modelo porque el modelo propone exactamente lo mismo en ambos —las mismas seis acciones, con los mismos cuatro errores—. La diferencia no está en el modelo; está en si existe un paso de aprobación entre la propuesta y la ejecución.

El segundo restaurante (el diseño B, la cáscara) tiene lo que el primero (el diseño A, el ingenuo) no: la cuenta que revisas antes de pagar. El mesero (el modelo) propone un cobro; tú (la cáscara determinista) lo revisas y solo apruebas lo correcto. En el primer restaurante, el mesero tiene tu tarjeta y cobra directo: sus errores se convierten en cargos que ya pasaron.

En el código, ese "paso de aprobación" es la diferencia entre las dos ramas: en el diseño A, paid_A += p["amount"] corre para toda propuesta (se cobra directo); en el diseño B, paid_B += p["amount"] corre solo dentro de if ok: (se cobra solo lo aprobado). Esa condición —la validación con poder de veto antes de mover el dinero— es la cuenta que revisas. Sin ella, el modelo tiene tu tarjeta; con ella, el modelo te trae la cuenta.

Ejercicio 2 — La validación que llega tarde. En el diseño A, la función validate sí se llama —para armar la lista de daños—, pero no evita ni un solo pago. Explica por qué "validar después de ejecutar" no protege el dinero, y da un ejemplo del código donde se ve que la validación en el diseño A no tiene poder de veto.

Ver solución

"Validar después de ejecutar" no protege el dinero porque las acciones son irreversibles: para cuando la validación descubre que el reembolso de $5000 estuvo mal, el dinero ya salió. La validación tardía produce conocimiento (sabes qué estuvo mal), pero no produce contención (no evitó que pasara). Es una autopsia: te dice de qué murió el paciente, no lo mantiene vivo.

En el código del diseño A se ve claro en el orden de las líneas: primero paid_A += p["amount"] (se ejecuta el pago), y luego ok, reason = validate(p) seguido de if not ok: damage.append(...). La validación corre después del pago, y su resultado solo se usa para agregar a una lista (damage), nunca para revertir ni evitar el paid_A += ... que ya ocurrió. No hay ninguna rama donde el False de validate impida el pago. Compara con el diseño B, donde validate corre primero y el pago está dentro de if ok: —ahí sí, el False veta el pago—. La lección: la validación solo es cáscara si corre antes y su "no" detiene la ejecución.

Ejercicio 3 — Dónde poner la caja. Un compañero propone este diseño para el generador "describe tu producto" de Mercado: el LLM redacta la descripción, la publica directo en el catálogo, y en paralelo un proceso revisa las descripciones publicadas y baja las que violan la política. Identifica el antipatrón, di qué error de esta lección comete, y propón el rediseño.

Ver solución

El antipatrón es que la publicación está acoplada a la salida del modelo (el LLM publica directo) y la validación corre después (el proceso revisa lo ya publicado). Comete dos errores de la lección a la vez: conectar la salida del modelo directo a la acción (publicar es la acción que toca estado) y poner la validación después de ejecutar, no antes. El resultado: toda descripción que viole la política —un claim prohibido, un texto ofensivo— está publicada y visible para clientes durante el tiempo que tarde el proceso de revisión en bajarla. El daño (una descripción falsa o prohibida vista por clientes) ya ocurrió, aunque después se corrija.

El rediseño mueve la caja: el LLM propone la descripción, una cáscara determinista la valida antes de publicar (¿longitud dentro del límite?, ¿sin claims prohibidos?, ¿sin lenguaje ofensivo?), y solo se publica si pasa. La descripción que viola la política nunca llega al catálogo, en vez de llegar y bajarse después. La validación pasa de ser una autopsia (revisar lo publicado) a ser una cáscara (aprobar antes de publicar). Nota: aquí el chequeo de contenido (claims, longitud, lenguaje) es parte guardrail de salida (M4) y parte cáscara de acción (publicar); lo que la lección aporta es el orden —validar antes de que la acción de publicar toque el estado, no después—.

Resumen y siguiente paso

En esta lección convertiste la tesis del módulo en un principio medido: el modelo propone, el sistema dispone. Pusiste lado a lado dos diseños con las mismas propuestas del modelo —el ingenuo, donde el LLM ejecuta directo, y el de cáscara, donde el LLM propone y el código dispone— y mediste la diferencia: $5315 pagados contra $125, cuatro ejecuciones peligrosas contra cero, $5190 protegidos. Viste que la seguridad no vino de mejorar el modelo (es el mismo modelo) sino de quitarle el poder de ejecutar; que la validación solo cuenta si va antes de la ejecución y tiene poder de veto (validar después es una autopsia, monitorear no es contener); y que el patrón entero se reduce a insertar una caja —la cáscara determinista— entre la propuesta y el efecto, una caja que el modelo no puede saltarse.

Antes de avanzar deberías poder: explicar por qué acoplar la ejecución a la salida del modelo es el antipatrón raíz; argumentar por qué la validación va antes y no después; distinguir monitorear (etiquetar) de contener (vetar); y dibujar la caja de la cáscara entre la propuesta y el efecto.

La lección 3 abre la caja y empieza por su entrada: la acción estructurada. Si el modelo va a proponer y no ejecutar, ¿en qué formato propone? La respuesta no es "texto libre que alguien interpreta" ni "código que se corre", sino un comando con nombre y campos tipados{action: "refund", order_id: ..., amount: ...}— que un dispatcher determinista puede validar y despachar. Vas a ver, ejecutado, por qué una propuesta estructurada es despachable y segura, mientras que el texto libre y las acciones inventadas se rechazan en el canal antes de llegar a la validación de reglas.

Recursos

  • Anthropic, "Building Effective Agents" (2024) — anthropic.com/engineering/building-effective-agents. La distinción entre un modelo que sugiere una acción y un sistema que decide ejecutarla es el eje del diseño de agentes seguros que describe el artículo. En inglés.
  • Martin Fowler y Bharani Subramaniam, "Emerging Patterns in Building GenAI Apps" — martinfowler.com/articles/gen-ai-patterns. El patrón de mantener al humano o al código en el lazo de aprobación antes de una acción irreversible es la versión general de "el modelo propone, el sistema dispone". En inglés.
  • Chip Huyen, AI Engineering (O'Reilly, 2024). Su discusión sobre el control de las acciones de un agente —y sobre por qué las acciones con efectos secundarios necesitan un paso de aprobación— es la base conceptual de esta lección. En inglés.
  • El principio de least privilege (mínimo privilegio) y el patrón de separación de responsabilidades de la seguridad clásica: quien propone no es quien autoriza. La lección 4 lo desarrolla como capabilities acotadas. Cualquier referencia introductoria de seguridad lo cubre.