Saltar al contenido
18/30Capítulo 18 de 30

Tool Calling y salidas estructuradas: el contrato que aguanta

Veinticuatro llamadas, cero JSON roto y dos fechas útiles. Luego el mismo endpoint con una mejor descripción, y lo que un esquema no arregla.

En esta página

Dale a un modelo una herramienta de búsqueda de vuelos y pídele que encuentre un vuelo de Madrid a Berlín. Esto es lo que devuelve:

TEXT
<tool_call>
{"name": "search_flights",
 "arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>

El JSON es válido. El nombre de la herramienta es correcto. Todos los campos obligatorios están presentes. Y la llamada no sirve: ninguna API de vuelos acepta "Madrid" cuando espera un código de aeropuerto, ni "3rd October 2026" cuando espera una fecha.

Esa brecha —sintácticamente perfecta, semánticamente inútil— es de lo que trata este capítulo, y lo primero que hay que dejar claro es que no es un problema de JSON. En veinticuatro solicitudes con esta herramienta, el modelo produjo 24 llamadas a herramientas válidas y cero JSON roto. No falló ni una sola vez en la parte que todo el mundo depura.

Antes de la mecánica, la frase que evita la mayoría de las confusiones: una llamada a herramienta es una solicitud, no una acción.

El modelo emite un mensaje estructurado que dice me gustaría que se llamara a search_flights con estos argumentos. Luego se detiene. Tu código recibe ese mensaje, decide si lo atiende, llama a lo que tenga que llamar y devuelve el resultado como otro mensaje. El modelo nunca ha tocado tu base de datos, nunca ha hecho una solicitud HTTP, nunca ha tenido credenciales.

Todo lo relativo a la seguridad de agent en el capítulo 30 se deriva de esa división, y también todo lo relativo al diseño de agent en el capítulo 23: el modelo propone y tu código dispone, y el código es donde vive cada garantía.

Así que una herramienta, despojada de vocabulario, son dos cosas:

Un esquema. Un JSON Schema que describe una función: su nombre, qué hace y qué argumentos acepta, con sus tipos y restricciones. Esto es lo que entra en el prompt, y es lo único que el modelo ve jamás.

Un endpoint. Una función en tu código que toma esos argumentos y devuelve algo. El modelo nunca la ve, nunca sabe en qué lenguaje está escrita y no puede distinguir una consulta a base de datos de una cadena hardcodeada.

Las definiciones de herramientas van en el prompt, serializadas en el formato para el que se entrenó el modelo. Cuestan tokens en cada llamada, un hecho que vuelve con un número más adelante en este capítulo.

El modelo responde con una llamada en vez de texto

Enlace a la sección: El modelo responde con una llamada en vez de texto

En lugar de prosa, la respuesta contiene una solicitud estructurada, y la API informa de un motivo de finalización que lo indica. Ese motivo importa: es como tu código sabe que debe ejecutar una herramienta en lugar de mostrarle una respuesta al usuario.

Este es el paso en el que no hay modelo. Valida los argumentos contra el esquema, decide si a quien llama se le permite hacer esto y ejecuta.

Envías el resultado de vuelta como un mensaje

Enlace a la sección: Envías el resultado de vuelta como un mensaje

El resultado se convierte en otro turno de la conversación, en un rol reservado para ello. El modelo lo lee como cualquier otro context.

Ese es el bucle del capítulo 23, y la razón por la que una sola solicitud puede convertirse en una docena de idas y vueltas.

Nada de esto es emergente. Como estableció el capítulo 11, tool calling es un comportamiento entrenado:1 durante el post-training, el modelo vio miles de conversaciones con exactamente esta forma. Por eso el formato es específico de cada modelo, por eso la fiabilidad varía tanto entre modelos de tamaño similar, y por eso un modelo puede llamar a una herramienta que nunca ha visto: la forma se entrenó; la herramienta concreta viene de tu prompt.

Esta es la herramienta tal como la escribe casi todo el mundo al principio. Fíjate en que no hay nada incorrecto en ella; simplemente es escueta:

tools/badFlights.tsTS
{
  name: "search_flights",
  description: "Search for flights.",
  parameters: {
    type: "object",
    properties: {
      from: { type: "string", description: "Airport." },   
      to:   { type: "string", description: "Airport." },   
      date: { type: "string", description: "The date." },  
    },
    required: ["from", "to", "date"],
  },
}

Veinticuatro solicitudes, seis pares de ciudades cruzados con cuatro formas de expresar una fecha ("el día 3 del mes que viene", "el próximo viernes", "15 de diciembre", "mañana"), greedy decoding para que los resultados se reproduzcan:

herramienta llamadaJSON rotofecha en ISOaeropuertos como IATAtodo correcto
el esquema anterior24/2402/244/241/24

Lee las dos primeras columnas antes de las tres últimas. El modelo llama siempre a la herramienta correcta y produce siempre JSON bien formado. El fallo está por completo en los valores, y los valores no sirven: "Madrid" en vez de MAD, "3rd October 2026" en vez de 2026-10-03.

Conviene insistir en esto porque determina dónde miras cuando algo se rompe. El instinto es añadir un parser de JSON con reintento, o pedirle al modelo con más firmeza que genere JSON válido. Ninguna de las dos cosas aborda nada de lo que ha pasado aquí.

Mismo endpoint. Mismo código detrás. Mismo modelo, mismos prompts, mismo decoding. Lo único que cambia es el texto del esquema:

tools/goodFlights.tsTS
{
  name: "search_flights",
  description: "Search scheduled flights between two airports on a given day.",
  parameters: {
    type: "object",
    properties: {
      from: {
        type: "string",
        description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",   
        pattern: "^[A-Z]{3}$",
      },
      to: { /* same */ },
      date: {
        type: "string",
        description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",   
        format: "date",
        pattern: "^\\d{4}-\\d{2}-\\d{2}$",
      },
    },
    required: ["from", "to", "date"],
  },
}
FORMATO de fechaVALOR de fechaFORMATO de aeropuertoVALOR de aeropuerto
esquema escueto2/241/244/244/24
esquema descrito24/2412/2416/248/24

El formato de fecha pasa de 2 de 24 a 24 de 24. Perfecto, a partir de un cambio de texto, sin tocar código y sin lógica de reintento. Si te llevas un hábito operativo de este capítulo, que sea este: cuando una herramienta se llama mal, la solución casi siempre está en la descripción, y es la solución más barata del sistema.

Ahora lee la segunda columna, que es la mitad más importante.

Un esquema restringe la forma. No puede aportar conocimiento.

Enlace a la sección: Un esquema restringe la forma. No puede aportar conocimiento.

La fecha está en formato ISO 24 veces de 24. Es el día correcto 12 veces de 24.

Así que la mitad de las llamadas llevan ahora una fecha perfectamente formateada que es la fecha equivocada. La descripción le dijo al modelo qué forma producir, y el modelo la produjo impecablemente, pero convertir "el próximo viernes" en 2026-09-11 exige saber la fecha de hoy y hacer aritmética de calendario, y ninguna cantidad de descripción aporta eso. Lo mismo ocurre con los aeropuertos: el formato pasó de 4 a 16, pero el valor solo de 4 a 8, porque escribir MAD exige saber que el aeropuerto de Madrid es MAD.

Esa distinción es la idea que sostiene el capítulo:

Un esquema es un contrato sobre la forma. Puede hacer que la salida del modelo sea parseable, tipada y coherente. No puede hacer que sea verdadera, y todo modo de fallo que sobreviva a un buen esquema es un fallo de conocimiento, no un fallo de formato.

Las dos cosas necesitan soluciones distintas, y confundirlas hace perder semanas. Los fallos de formato se arreglan en la descripción o con constrained decoding, más abajo. Los fallos de conocimiento se arreglan metiendo el conocimiento en el prompt: la fecha actual en el mensaje de sistema, una búsqueda de aeropuerto como una segunda herramienta que el modelo llama primero, un enum en el esquema cuando el conjunto es lo bastante pequeño como para enumerarlo. Fíjate en lo que tienen en común las tres opciones: sacan el problema de la memoria del modelo y lo ponen en su entrada, que es todo el capítulo 24.

Salidas estructuradas, y qué es realmente "constrained decoding"

Enlace a la sección: Salidas estructuradas, y qué es realmente "constrained decoding"

Todo lo anterior sigue dependiendo de que el modelo elija producir la forma correcta. Hay una garantía más fuerte disponible, y es la mejor recompensa del capítulo 17.

Recuerda cómo funciona la generación: en cada paso el modelo produce un logit para cada token del vocabulario, y el muestreador elige uno. Constrained decoding inserta un paso entre medias. Dada una gramática —derivada de tu JSON Schema— calcula qué tokens podrían venir legalmente a continuación, pone los logits de todos los demás a infinito negativo y deja que el muestreador elija entre lo que queda.

Si el esquema dice que lo siguiente debe ser un {, entonces todo token que no sea { tiene probabilidad cero. No "improbable": cero. El modelo no puede emitir JSON inválido porque los tokens inválidos se eliminaron de la distribución antes del muestreo.

Eso es lo que hay debajo de "salidas estructuradas", "modo JSON" y "generación guiada", y explica sus dos propiedades. La garantía es total para cualquier cosa que la gramática pueda expresar —tipos, campos obligatorios, enums, anidamiento— porque se aplica mecánicamente en lugar de pedirse con educación. Y no dice nada sobre el contenido: una gramática puede obligar a que "date" sea una cadena que coincide con un patrón de fecha, y no puede obligar a que sea el día correcto. Es el mismo muro que en la sección anterior, alcanzado desde el otro lado.

Dos notas prácticas. No es gratis: la máscara tiene que calcularse en cada paso, y las gramáticas complejas cuestan una latencia medible. Y cambia lo que está haciendo el modelo: un modelo apartado de su token preferido puede producir peor contenido mientras produce una estructura perfecta, por eso "pedirlo bien y validar" sigue siendo un valor por defecto razonable para formas simples, y constrained decoding se gana su coste cuando la forma es compleja o el consumidor es estricto.

Efectos secundarios, y la única propiedad que importa

Enlace a la sección: Efectos secundarios, y la única propiedad que importa

El capítulo 14 midió un timeout seguido de un reintento que facturaba dos generaciones por una respuesta. Con herramientas, el mismo fallo empeora, porque una herramienta puede hacer algo.

Si tu código llama a charge_card, hace timeout y reintenta, tienes dos cargos. El modelo no tiene ni idea de que nada de esto ha pasado; ve un resultado de herramienta. La solución es la misma que en cualquier sistema distribuido y no es problema del modelo: hacer que la operación sea idempotente dándole una clave a la llamada, para que la segunda ejecución reconozca la primera y devuelva su resultado en lugar de volver a hacer el trabajo.

La regla de diseño que se deriva de esto merece decirse claramente. Separa las lecturas de las escrituras en tu catálogo de herramientas. Una lectura puede reintentarse libremente, ejecutarse en paralelo y cachearse. Una escritura no, y debería llevar una clave, una comprobación de permisos y —para cualquier cosa de la que un usuario querría enterarse antes de que ocurra— un paso de aprobación que ponga a una persona entre la solicitud y la acción. Ese paso de aprobación no es una cortesía: es una de las pocas cosas que se interponen entre un prompt injection y una consecuencia real y, como mide el capítulo 30, la más débil de ellas.

El folclore dice que cargar muchas herramientas hace que el modelo elija mal. Merece la pena medirlo en lugar de repetirlo, así que: las mismas veinticuatro solicitudes, con la herramienta de vuelos más un conjunto creciente de otras, incluidas tres deliberadamente confundibles (horarios de tren, travesías en ferry, rutas de autobús).

herramientas cargadasprompt tokenseligió search_flightsfecha en ISO
135324/2424/24
573024/2424/24
101,19321/2421/24
202,11924/2424/24

La selección no degradó. Con veinte herramientas, tres de ellas plausiblemente confundibles, un modelo de quinientos millones de parámetros eligió la correcta veinticuatro veces de veinticuatro. La bajada en diez son tres llamadas que nombraron otra herramienta, y no sobrevive al pasar a veinte.

Es un resultado negativo y debería contarse como tal: en esta tarea, con estas herramientas, "demasiadas herramientas" no fue el problema. Lo que sí creció, de forma monótona y por un factor de seis, es el prompt: de 353 tokens a 2,119, pagados en cada solicitud de la conversación, para siempre, se use o no alguna herramienta.

Así que la versión honesta del folclore trata de coste y context, no de precisión. Veinte herramientas son un impuesto permanente sobre cada mensaje, y el capítulo 16 ya mostró lo que hace un prefijo permanente a una factura a lo largo de cuarenta turnos. Cuando la gente informa de que muchas herramientas perjudican la calidad, el mecanismo suele ser que las definiciones desplazaron el context que importaba, que es un problema del capítulo 24 disfrazado de capítulo 18. Las herramientas que son casi duplicados reales entre sí también son un problema, y la solución para ellas no es menos herramientas, sino mejores descripciones y namespaces: prefíjalas por sistema (crm.search_customer, billing.search_customer) para que dos catálogos fusionados de dos equipos no colisionen y para que el modelo tenga algo con lo que discriminar.

Tres tipos de herramienta, y la que abre la siguiente parte

Enlace a la sección: Tres tipos de herramienta, y la que abre la siguiente parte

Ayuda ordenar las herramientas por lo que hacen al mundo, porque la ingeniería difiere en cada caso.

Herramientas de datos leen: buscan, recuperan, consultan. Reintentables, paralelizables, cacheables. Fallan devolviendo nada útil, y su principal riesgo es que traen texto no fiable al context, que es toda la superficie de ataque del capítulo 30.

Herramientas de acción escriben: envían, crean, cobran, eliminan. No son reintentables sin una clave, no son paralelizables de forma segura, y son la razón de que existan los flujos de aprobación.

Herramientas de orquestación llaman a otros modelos. Una herramienta cuya implementación es otro agent, con su propio prompt, sus propias herramientas y su propio bucle; y para el modelo que la llama tiene exactamente el mismo aspecto que las otras dos, porque un esquema y un endpoint es todo lo que llega a ver.

Ese tercer tipo no es una curiosidad. Es el mecanismo detrás de la mitad de agent-como-herramienta del capítulo 25: la otra topología, el handoff, entrega la conversación y nunca la recupera. Y funciona precisamente porque la interfaz de este capítulo es lo bastante estrecha como para que quepa un agent entero detrás.

Ahora tienes un modelo que puede pedir cosas, y un contrato que hace que la petición sea parseable. Lo que no tienes es nada sobre lo que pueda preguntar más allá de lo que cabe en su prompt.

La herramienta más común en producción, con mucha diferencia, es una búsqueda sobre un cuerpo de texto que el modelo nunca vio durante el entrenamiento: tu documentación, tus tickets, tus contratos. Suena a problema resuelto —embed it, encuentra los vecinos más cercanos, pégalos dentro— y las partes que no están resueltas son las que deciden si la respuesta es fiable: cómo se corta el texto antes de hacer embedding, qué umbral de similitud es lo bastante bajo como para significar no lo sé, y cómo se adjunta una cita a una afirmación para que una persona pueda comprobarla.

El capítulo 19 es retrieval, y es el capítulo en el que una respuesta incorrecta deja de ser una curiosidad y empieza a ser una responsabilidad.


Las mediciones de este capítulo proceden de Qwen/Qwen2.5-0.5B-Instruct con greedy decoding, sobre 24 solicitudes generadas que cruzan seis pares de ciudades con cuatro formulaciones de fecha, usando la propia plantilla de chat del modelo para las definiciones de herramientas. Se reproducen exactamente, y son un modelo pequeño: lee la división formato/valor como una demostración del mecanismo, no como un benchmark de lo que hacen los modelos actuales. Un modelo frontier resuelve "el próximo viernes" correctamente con mucha más frecuencia, y aun así un esquema no puede obligarle a hacerlo, que es la parte que generaliza.

El vocabulario de JSON Schema usado arriba (type, properties, required, pattern, format, enum) está especificado en el borrador de JSON Schema que nombre la documentación de tu proveedor; el subconjunto útil es pequeño y el mismo entre proveedores, y las diferencias que sí existen —qué keywords se hacen cumplir mediante constrained decoding en lugar de simplemente pasarse al modelo— merece la pena leerlas en la guía de salidas estructuradas del proveedor en vez de darlas por supuestas.

Para constrained decoding como técnica, las bibliotecas de estilo guidance y el proyecto outlines documentan la construcción de gramática a máscara de logit de una manera que encaja directamente con el muestreador del capítulo 17. Y para la ida y vuelta en sí, la especificación más clara no es un tutorial, sino un protocolo: el capítulo 26 lo lee línea por línea.

  1. Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). El paper que convirtió en estándar la receta de post-training; la forma de una llamada a herramienta se aprende ahí, a partir de demostraciones, exactamente igual que la forma de una respuesta.

¿Listo para dejar que elija LIA?

Crea con todos los modelos de IA en un mismo sitio. Empieza gratis hoy.