Módulo 6: La cáscara determinista

La acción estructurada

Descripción

La lección 2 estableció que el modelo propone y el sistema dispone, y dibujó la caja —la cáscara determinista— que se interpone entre la propuesta y el efecto. Esta lección abre esa caja por su entrada y hace la pregunta que quedó pendiente: si el modelo va a proponer una acción, ¿en qué formato la propone? La respuesta define si todo lo demás es posible. Un modelo puede "proponer" un reembolso de tres maneras muy distintas: escribiendo un texto libre ("creo que deberíamos devolverle el dinero de este pedido"), generando código para que alguien lo corra (db.execute("UPDATE ...")), o devolviendo un comando estructurado —un objeto con nombre y campos tipados: {action: "refund", order_id: "A-1001", amount: 50.00}—. Solo la tercera forma permite que una capa determinista valide y despache la propuesta con garantías. Las otras dos son, cada una a su manera, una puerta abierta.

La acción estructurada es esa tercera forma: el modelo devuelve una propuesta validable —un comando de un menú conocido, con los campos que ese comando requiere—, y un dispatcher determinista la despacha. Lo que no es un comando conocido del menú, o le faltan campos, o es texto libre, no se puede despachar: se rechaza en el canal, antes siquiera de llegar a la validación de reglas de negocio. En esta lección vas a ver, ejecutado, cómo de seis propuestas del modelo, tres son comandos despachables y tres se rechazan en el canal —el texto libre, la acción inventada, el comando incompleto—.

Conexión con el módulo. Esta lección es el formato de la propuesta, la entrada de la cáscara. La lección 4 dirá qué comandos puede proponer el modelo (el menú de capabilities); la 5, contra qué reglas se validan (la política). Aquí tratamos algo más básico y previo: que la propuesta sea un comando despachable. La frontera con el módulo 4 es fina y hay que trazarla con cuidado: M4 validaba el contenido de la salida (¿el JSON está bien formado?, ¿tiene un claim prohibido?); aquí no validamos el contenido, sino que la salida sea una acción que el sistema sabe despachar —un comando del menú, con sus campos—. Un guardrail de M4 dice "esto es un JSON válido"; el canal de acción de M6 dice "esto es un comando que puedo ejecutar". Y la frontera con AI Engineering: cómo lograr que el modelo devuelva acciones estructuradas de forma confiable —tool use, function calling, structured outputs— es AI Eng; aquí tratamos por qué el formato estructurado es la única entrada segura de la cáscara.

Una analogía: la orden en la comanda contra el mesero que grita a la cocina

En una cocina profesional, las órdenes llegan en una comanda: un papel (o un ticket digital) con un formato fijo —mesa, platillo del menú, cantidad, notas—. La cocina solo prepara lo que viene en una comanda bien llenada: un platillo que existe en el menú, con la mesa anotada y la cantidad clara. Si llega una comanda con un platillo que no está en el menú, o sin número de mesa, o ilegible, la cocina la rechaza y pide que se corrija. La comanda es un formato estructurado: campos conocidos, valores validables, un menú cerrado de platillos. Por eso la cocina puede procesarla con confianza y sin adivinar.

Imagina la alternativa: un mesero que en vez de comanda grita a la cocina lo que cree que pidió el cliente —"¡algo con pollo para la mesa del fondo, creo que sin cebolla, y una bebida!"—. La cocina tiene que interpretar: ¿cuál platillo con pollo?, ¿cuál mesa del fondo?, ¿"creo" significa que confirme? Cada interpretación es una oportunidad de error, y peor: si el mesero grita "¡y cóbrenle lo que sea!", la cocina no tiene forma de saber qué es válido cobrar y qué no. El grito es texto libre: no tiene formato, no tiene campos, no se puede validar contra un menú. La cocina o adivina (y a veces se equivoca) o ejecuta ciegamente lo que oyó (y a veces es un desastre).

La comanda estructurada es la acción estructurada del modelo; el grito es el texto libre. Cuando el LLM propone {action: "refund", order_id: "A-1001", amount: 50.00}, es una comanda: la cáscara ve el "platillo" (refund) en su menú, ve los campos requeridos (order_id, amount), y puede procesarla o rechazarla con criterio. Cuando el LLM propone "creo que deberíamos devolverle el dinero", es un grito: no hay comando que despachar, no hay campos que validar, y la única opción sensata es rechazarlo en la puerta de la cocina. La cocina bien diseñada solo acepta comandas.

Ejemplo trabajado: qué es despachable y qué se rechaza en el canal

Vamos a ejecutar la puerta de la cocina. Definimos un menú de comandos conocidosrefund, escalate_to_human, send_message— cada uno con sus campos requeridos. La función is_structured_action hace el chequeo de canal: ¿es un comando (un objeto), no texto libre? ¿nombra una acción del menú? ¿trae todos los campos requeridos? Ese chequeo es previo a la validación de reglas de negocio (que es la lección 5): aquí solo decidimos si la propuesta es despachable en absoluto. Corremos un lote de seis propuestas del modelo que mezcla comandos bien formados con texto libre, una acción inventada y un comando incompleto.

# Modulo 6, Leccion 3: la accion estructurada.
# El LLM no ejecuta codigo libre ni "hace" cosas: devuelve una PROPUESTA en un
# formato validable -un comando con nombre y campos tipados- que un DISPATCHER
# determinista despacha. Lo que no es un comando conocido, no se puede despachar.
# Sin red, sin API, sin claves. Datos fijos.

# Menu de comandos conocidos y sus campos REQUERIDOS. El dispatcher solo sabe
# despachar estos; nada mas es "ejecutable".
ACTION_MENU = {
    "refund":            {"order_id", "amount"},
    "escalate_to_human": {"reason"},
    "send_message":      {"text"},
}


def is_structured_action(proposal):
    # Chequeo de CANAL (no de politica de negocio, que va en la leccion 5):
    # 1) es un dict (un comando), no texto libre;
    # 2) nombra una accion del menu conocido;
    # 3) trae todos los campos requeridos de esa accion.
    if not isinstance(proposal, dict):
        return (False, "no es un comando (texto libre)")
    action = proposal.get("action")
    if action not in ACTION_MENU:
        return (False, f"accion desconocida: {action!r}")
    required = ACTION_MENU[action]
    missing = required - proposal.keys()
    if missing:
        return (False, f"faltan campos {sorted(missing)}")
    return (True, "comando despachable")


# Lo que el nucleo probabilistico (SIMULADO) propuso en un lote. Mezcla comandos
# bien formados con texto libre, una accion inventada y un comando incompleto.
PROPOSALS = [
    {"action": "refund", "order_id": "A-1001", "amount": 50.00},
    "Voy a reembolsarte, dame un momento",              # texto libre
    {"action": "delete_database"},                      # accion inventada
    {"action": "refund", "order_id": "A-1005"},         # falta 'amount'
    {"action": "escalate_to_human", "reason": "cliente pide supervisor"},
    {"action": "send_message", "text": "Tu pedido va en camino"},
]

dispatchable = rejected = 0
print(f"{'#':<3}{'propuesta':<48}{'verdicto':<14}razon")
print("-" * 92)
for i, p in enumerate(PROPOSALS):
    ok, reason = is_structured_action(p)
    verdict = "DESPACHABLE" if ok else "RECHAZADA"
    if ok:
        dispatchable += 1
    else:
        rejected += 1
    shown = (p["action"] if isinstance(p, dict) and "action" in p
             else (repr(p)[:44] if isinstance(p, str) else str(p)))
    print(f"{i:<3}{shown:<48}{verdict:<14}{reason}")

print()
print(f"Despachables (comando valido en el menu) : {dispatchable}/{len(PROPOSALS)}")
print(f"Rechazadas en el canal                   : {rejected}/{len(PROPOSALS)}")

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

#  propuesta                                       verdicto      razon
--------------------------------------------------------------------------------------------
0  refund                                          DESPACHABLE   comando despachable
1  'Voy a reembolsarte, dame un momento'           RECHAZADA     no es un comando (texto libre)
2  delete_database                                 RECHAZADA     accion desconocida: 'delete_database'
3  refund                                          RECHAZADA     faltan campos ['amount']
4  escalate_to_human                               DESPACHABLE   comando despachable
5  send_message                                    DESPACHABLE   comando despachable

Despachables (comando valido en el menu) : 3/6
Rechazadas en el canal                   : 3/6

Lee los seis casos, porque cada uno enseña una parte del canal.

Los tres despachables (0, 4, 5) son comandos completos del menú. El refund con order_id y amount, el escalate_to_human con reason, el send_message con text: cada uno nombra una acción que el sistema conoce y trae los campos que esa acción necesita. Son comandas bien llenadas. Fíjate en algo importante: que sean despachables no significa que se vayan a ejecutar. El refund despachable del caso 0 todavía tiene que pasar por la validación de reglas de negocio (¿el pedido existe?, ¿dentro de la ventana?, ¿monto en límite?) que es la lección 5. El chequeo de canal solo dice "esto es un comando que puedo procesar"; la ejecución depende de las compuertas que siguen. Una comanda bien llenada entra a la cocina; que el platillo se prepare depende de si hay ingredientes.

El caso 1 es texto libre, y se rechaza porque no hay nada que despachar. "Voy a reembolsarte, dame un momento" no es un comando: es una frase. No tiene action, no tiene campos, no hay forma de convertirla en una operación sin interpretarla —y interpretar texto libre para ejecutar acciones es exactamente el grito a la cocina—. La cáscara no intenta adivinar qué reembolso quería hacer; lo rechaza en la puerta. Este es el caso que más importa entender: un modelo que "propone" en prosa no está proponiendo una acción, está pidiendo que alguien la deduzca, y esa deducción es una superficie de error (y de ataque) que no queremos.

El caso 2 es una acción inventada, y se rechaza porque no está en el menú. delete_database no es un comando que el sistema conozca —no está en ACTION_MENU—, así que no hay dónde despacharlo. Nota lo poderoso de esto: aunque el modelo alucine una acción destructiva, el canal la rechaza por definición, porque solo despacha lo que está en el menú. Esto es un anticipo de las capabilities acotadas de la lección 4: un menú cerrado significa que las acciones que no están en él no existen para la cáscara, por más que el modelo las proponga.

El caso 3 es un comando conocido pero incompleto, y se rechaza porque le falta un campo. {action: "refund", order_id: "A-1005"} nombra una acción del menú (refund), pero le falta amount —¿reembolsar cuánto?—. Un comando sin sus campos requeridos no es despachable: la cocina no puede preparar un platillo del menú si la comanda no dice la cantidad. Rechazarlo en el canal es mejor que despacharlo con un amount por defecto o adivinado, porque cualquier valor inventado sería un monto que el modelo no propuso.

El conteo, tres y tres, es el filtro del canal. De seis propuestas, tres pasaron el canal (son comandos despachables) y tres se rechazaron antes de llegar a la validación de reglas. Ese es el trabajo del formato estructurado: convertir "lo que el modelo dijo" en "un comando que el sistema puede procesar o rechazar con criterio", y filtrar en la puerta todo lo que no es siquiera un comando. Sin este filtro, el texto libre y las acciones inventadas llegarían a capas más profundas donde alguien tendría que interpretarlas —y ahí es donde nacen los desastres—.

Profundización: por qué el formato estructurado es la única entrada segura

El ejemplo mostró qué pasa; vale la pena entender por qué el formato estructurado es la condición que hace posible todo el módulo.

Un comando estructurado es validable; el texto libre y el código no lo son (de forma segura). Cuando la propuesta es {action: "refund", order_id: "A-1001", amount: 50.00}, la cáscara puede hacer preguntas concretas y deterministas: ¿action está en el menú?, ¿amount es un número?, ¿amount <= MAX_REFUND? Cada pregunta tiene una respuesta dura. Cuando la propuesta es texto libre —"devuélvele el dinero"—, no hay campos que interrogar: para validarla, primero hay que interpretarla, y la interpretación misma es probabilística (¿de qué pedido?, ¿cuánto?). El formato estructurado mueve la incertidumbre a un solo lugar —el modelo llenando los campos— y deja el resto del pipeline en terreno determinista. Por eso la acción estructurada es la frontera entre el núcleo probabilístico y la cáscara determinista: es donde la propuesta incierta se convierte en datos que el código certero puede manejar.

El dispatcher es una tabla, no un intérprete. La forma correcta de ejecutar una acción estructurada es un dispatcher: una tabla que mapea el nombre del comando a una función que lo maneja —{"refund": handle_refund, "escalate_to_human": handle_escalate, ...}—. El modelo elige de la tabla; el código ejecuta la función correspondiente. Nota la diferencia radical con la alternativa peligrosa: si el modelo generara código (db.execute(...)) y el sistema lo corriera, el modelo tendría poder arbitrario —podría hacer cualquier cosa que el código pueda expresar—. Con un dispatcher, el modelo solo puede seleccionar de un conjunto de operaciones que escribiste y controlas. El modelo no escribe la acción; la nombra. Esa es la diferencia entre darle al mesero la libreta de comandas (elige del menú) y darle acceso a la caja registradora (hace lo que quiera).

   El LLM devuelve una ACCION ESTRUCTURADA (una comanda), no codigo ni prosa:

        {action: "refund", order_id: "A-1001", amount: 50.00}
              │
              ▼
     ┌──────────────────┐   ¿es un dict?                 no → RECHAZA (texto libre)
     │  chequeo de canal │   ¿action en el menu?          no → RECHAZA (accion desconocida)
     │  (esta leccion)   │   ¿trae campos requeridos?     no → RECHAZA (campos faltantes)
     └──────────────────┘
              │ si (comando despachable)
              ▼
     ┌──────────────────┐   DISPATCHER (una tabla, no un interprete):
     │   dispatch        │   {"refund": handle_refund, "escalate": handle_escalate}
     │                   │   el modelo NOMBRA la accion; tu codigo la ejecuta
     └──────────────────┘
              │
              ▼
      capabilities (L4) -> reglas de negocio (L5) -> ejecutar

El chequeo de canal es previo a todo lo demás. Fíjate en el orden del diagrama: el chequeo de canal va antes de las capabilities y de las reglas de negocio. Tiene sentido: no puedes preguntar "¿este reembolso está dentro de política?" si ni siquiera sabes que la propuesta es un reembolso con un monto. Primero conviertes la propuesta en un comando bien formado (o la rechazas), y solo entonces las capas siguientes pueden razonar sobre ella. Es el mismo orden que la cocina: primero la comanda tiene que estar bien llenada; después se ve si hay ingredientes.

La frontera con el módulo 4, con precisión. Es fácil confundir el chequeo de canal con el guardrail de esquema del módulo 4, porque ambos "validan la estructura de la salida". La distinción: el guardrail de M4 valida que la salida cumpla un esquema (que sea un JSON con ciertos campos y tipos) como un fin en sí mismo —para que sea parseable y no tenga contenido prohibido—. El chequeo de canal de M6 valida que la salida sea un comando ejecutable —que nombre una acción que el sistema sabe despachar—. En la práctica se solapan (un comando bien formado suele pasar un guardrail de esquema), pero la pregunta es distinta: M4 pregunta "¿es una salida válida?"; M6 pregunta "¿es una acción que puedo ejecutar?". Y hay salidas que pasan M4 y fallan el canal de M6: {action: "delete_database"} es un JSON impecable (pasa M4) que el canal rechaza porque delete_database no está en el menú (falla M6). El esquema mira la forma; el canal mira si la acción existe.

Errores comunes

Dejar que el modelo devuelva texto libre y luego interpretarlo para actuar. Qué pasa: el agente responde en prosa —"claro, procedo a reembolsarte el pedido"— y un segundo paso (a veces otro LLM, a veces una heurística) intenta deducir de esa prosa qué acción ejecutar. Cada interpretación es una probabilidad de error, y el texto libre puede esconder instrucciones que no querías ejecutar. Por qué pasa: es más "natural" que el modelo hable en prosa, y parece amigable. Cómo detectarlo: si entre la salida del modelo y la ejecución hay un paso que interpreta lenguaje para decidir qué hacer, tienes un grito a la cocina. Cómo corregirlo: haz que el modelo devuelva una acción estructurada —un comando con campos— y despáchala con una tabla, no con un intérprete. La prosa puede ir en un campo message para mostrar al usuario, pero la acción va en campos estructurados.

Dejar que el modelo genere código para ejecutarlo. Qué pasa: para darle "flexibilidad", se le pide al modelo que genere una consulta SQL, o un snippet, y el sistema lo corre. Ahora el modelo tiene poder arbitrario: puede expresar cualquier operación, incluidas las que nunca quisiste permitir (un DELETE sin WHERE, un UPDATE masivo). Por qué pasa: generar código se siente potente y general. Cómo detectarlo: si en algún punto el sistema ejecuta una cadena que el modelo produjo (eval, exec, SQL crudo, shell), tienes una puerta abierta del tamaño del lenguaje que ejecutas. Cómo corregirlo: nunca ejecutes código del modelo; hazlo elegir de un menú de comandos que tú escribiste. El modelo nombra la operación (refund); tu código la implementa. La expresividad que pierdes es exactamente la que no querías darle.

Aceptar cualquier action que el modelo ponga, sin un menú cerrado. Qué pasa: el dispatcher intenta manejar cualquier nombre de acción que venga —"si action es X, busca un handler llamado handle_X"—, en vez de tener un menú fijo. El día que el modelo alucina action: "delete_account", si por casualidad existe un handle_delete_account, se ejecuta. Por qué pasa: parece más extensible resolver handlers dinámicamente. Cómo detectarlo: si tu dispatcher no rechaza explícitamente las acciones que no están en un conjunto cerrado, no tienes un menú. Cómo corregirlo: define un ACTION_MENU explícito y rechaza todo lo que no esté en él, como en el ejemplo. Un menú cerrado es lo que hace que delete_database se rechace por definición, no por casualidad. La lección 4 desarrolla esto como capabilities acotadas.

Ejercicios

Ejercicio 1 — La comanda y el grito. Para cada una de estas propuestas del modelo, di si es una acción estructurada despachable o un grito (texto libre / no despachable), y por qué: (a) {action: "send_message", text: "Tu paquete llega mañana"}; (b) "Escálalo a un humano por favor"; (c) {action: "refund", amount: 50.00} (sin order_id); (d) {action: "ban_seller", seller_id: "S-9"} con un menú que solo tiene refund, escalate_to_human, send_message.

Ver solución
  • (a) → despachable. Es un comando del menú (send_message) con su campo requerido (text). Comanda bien llenada; la cocina la procesa.
  • (b) → grito (texto libre). "Escálalo a un humano por favor" es prosa, no un comando: no tiene action ni campos. Aunque su intención sea un escalamiento, no es despachable —habría que interpretarla—, y la cáscara la rechaza en el canal. Lo correcto sería que el modelo devolviera {action: "escalate_to_human", reason: "..."}.
  • (c) → no despachable (campo faltante). refund está en el menú, pero le falta order_id: ¿reembolsar cuál pedido? Un comando conocido pero incompleto se rechaza en el canal, igual que una comanda sin número de mesa.
  • (d) → no despachable (acción fuera del menú). ban_seller no está en el menú de este agente, así que la cáscara no sabe despacharlo y lo rechaza por definición. No importa que el JSON esté bien formado; la acción no existe para esta cáscara. (Es el caso que la lección 4 tratará como una capability no otorgada.)

El patrón: despachable = comando del menú + todos sus campos. Todo lo demás —prosa, comando incompleto, acción fuera del menú— se rechaza en el canal, antes de la validación de reglas.

Ejercicio 2 — Despachable no es ejecutable. En el ejemplo, el refund del caso 0 salió "DESPACHABLE", pero eso no significa que el reembolso se ejecute. Explica la diferencia entre "despachable" (chequeo de canal, esta lección) y "ejecutable" (validación de reglas, lección 5), y da un ejemplo de un comando que sea despachable pero que la lección 5 deba bloquear.

Ver solución

"Despachable" significa que la propuesta es un comando bien formado del menú: tiene una action que el sistema conoce y todos sus campos requeridos. Es un chequeo de forma: ¿puedo siquiera procesar esto como una acción? "Ejecutable" significa que la acción, además de estar bien formada, cumple las reglas de negocio: es un chequeo de política —¿el pedido existe?, ¿está dentro de la ventana?, ¿el monto está en el límite?—. Son dos compuertas en serie: primero canal (¿es un comando?), luego reglas (¿la acción es válida?).

Un ejemplo de despachable pero no ejecutable: {action: "refund", order_id: "A-1005", amount: 5000.00}. Pasa el canal —es un refund con order_id y amount, una comanda perfectamente llenada— pero la lección 5 debe bloquearlo porque el monto ($5000) excede el total del pedido y el límite del agente. La comanda está bien escrita; el platillo no se puede preparar. Que algo sea despachable solo abre la puerta a la siguiente compuerta; no garantiza la ejecución. Por eso el módulo tiene varias capas: el canal filtra lo que ni es un comando; las reglas filtran los comandos que violan la política.

Ejercicio 3 — Del grito a la comanda. Un equipo tiene un agente que responde en prosa y un segundo paso que "lee" esa prosa con otro LLM para decidir la acción. Explica los dos problemas de este diseño según la lección, y reescribe la interfaz para que el agente devuelva una acción estructurada. Muestra el menú de comandos que propondrías para un agente de soporte.

Ver solución

Los dos problemas: (1) interpretar texto libre para actuar es probabilístico —el segundo LLM puede deducir mal qué acción quería el primero, agregando una segunda fuente de error sobre la primera—; y (2) el texto libre es una superficie de ataque y de ambigüedad —una prosa puede esconder instrucciones o intenciones que el intérprete ejecute sin querer, y no hay campos concretos que validar antes de actuar—. En términos de la lección, es un grito a la cocina resuelto con más adivinanza en vez de con una comanda.

La reescritura: el agente devuelve directamente una acción estructurada de un menú cerrado, y la cáscara la despacha con una tabla (no con un intérprete). Un menú razonable para un agente de soporte:

ACTION_MENU = {
    "refund":            {"order_id", "amount"},
    "escalate_to_human": {"reason"},
    "send_message":      {"text"},          # la prosa para el cliente va AQUI,
    "resend_receipt":    {"order_id"},      # como un campo, no como la accion
}

Nota que la prosa amable para el cliente no desaparece: vive en el campo text del comando send_message. Lo que cambia es que la acción (reembolsar, escalar, reenviar) es un comando estructurado que se valida y despacha, no una frase que alguien interpreta. Así se elimina el segundo LLM intérprete: no hay nada que interpretar, hay un comando que despachar. La incertidumbre queda contenida en un solo lugar (el modelo llenando los campos), y todo lo demás es determinista.

Resumen y siguiente paso

En esta lección abriste la caja de la cáscara por su entrada: la acción estructurada. Si el modelo propone y no ejecuta, propone en un formato validable —un comando con nombre y campos tipados, {action: "refund", ...}— que un dispatcher determinista despacha, no en texto libre ni en código. Lo mediste: de seis propuestas, tres eran comandos despachables y tres se rechazaron en el canal —el texto libre porque no hay nada que despachar, la acción inventada porque no está en el menú, el comando incompleto porque le falta un campo—. Viste que el chequeo de canal es previo a la validación de reglas (despachable no es ejecutable), que el dispatcher es una tabla y no un intérprete (el modelo nombra la acción, tu código la ejecuta), y que la frontera con M4 es de pregunta: M4 valida "¿es una salida válida?", el canal de M6 valida "¿es una acción que puedo ejecutar?".

Antes de avanzar deberías poder: explicar por qué una acción estructurada es validable y el texto libre no lo es de forma segura; distinguir un dispatcher (tabla de comandos) de un intérprete (ejecuta lo que el modelo exprese); argumentar por qué nunca se ejecuta código del modelo; y separar "despachable" (canal) de "ejecutable" (reglas).

La lección 4 toma el menú de comandos que aquí dimos por dado y lo convierte en un principio de diseño: las capabilities acotadas. El caso 2 del ejemplo —delete_database rechazado porque no está en el menú— fue un anticipo: un menú cerrado significa que las acciones fuera de él no existen para la cáscara. Vas a ver, medido, cómo el radio de daño de un agente crece con el tamaño de su menú: darle capabilities amplias "para que sea útil" pone operaciones irreversibles al alcance de cada alucinación, mientras que un menú mínimo las deja en cero. El least privilege de la seguridad clásica, aplicado a lo que un LLM puede proponer.

Recursos

  • Documentación de Claude, tool usedocs.anthropic.com. La forma canónica de la acción estructurada: el modelo devuelve una llamada a herramienta con un nombre y argumentos tipados, definidos por ti, y tu sistema decide qué hacer con ella. Es exactamente el "comando del menú" de esta lección. Léela sin fijarte en una versión de modelo específica. En inglés.
  • Anthropic, "Building Effective Agents" (2024) — anthropic.com/engineering/building-effective-agents. Su insistencia en dar al modelo un conjunto acotado de herramientas bien definidas, en vez de ejecución libre, es la base del formato estructurado y del dispatcher. En inglés.
  • Martin Fowler y Bharani Subramaniam, "Emerging Patterns in Building GenAI Apps" — martinfowler.com/articles/gen-ai-patterns. El patrón de estructurar la salida del modelo como datos validables, en vez de prosa a interpretar, aparece como una pieza recurrente en las apps GenAI robustas. En inglés.
  • Chip Huyen, AI Engineering (O'Reilly, 2024). Su tratamiento de function calling y de salidas estructuradas cubre cómo lograr que el modelo devuelva acciones bien formadas de manera confiable —la frontera con AI Eng—; aquí nos quedamos con por qué el formato estructurado es la entrada segura de la cáscara. En inglés.