Validar las tool calls: declarar un schema no basta

Validar las tool calls: declarar un schema no basta

La forma anunciada y los datos recibidos

Una tool es una función que el modelo puede pedir. Su input schema describe la forma de los argumentos: qué campos necesita y de qué tipo son. Si una función guarda una medición corporal, anunciar que el peso es un número no hace que la función compruebe automáticamente cada dato que recibe. Necesitas ejecutar esa comprobación en tu propio programa.

Las restricciones que ofrezca el proveedor pueden ayudar a generar datos correctos. Aun así, el harness debe hacer cumplir su contrato antes de actuar, especialmente si admite distintos proveedores. Una declaración describe lo aceptado; el validador compara esa declaración con una llamada concreta.

Tres preguntas antes de ejecutar

Hay tres comprobaciones distintas. Primero, ¿terminó la respuesta del proveedor? Después, ¿se pueden interpretar los argumentos como un objeto JSON? Por último, ¿ese objeto cumple el schema? Una respuesta completa puede contener argumentos incorrectos; un JSON legible también.

  1. Los eventos del proveedor llegan al parser, que reconstruye la llamada pendiente.
  2. Tras el resultado del parser, el harness comprueba que la respuesta está completa e interpreta los argumentos.
  3. El validador compara campos y tipos con el schema de esa tool.
  4. Si cumple el schema, entra en el handler; si falla, devuelve el error sin ejecutarlo.
  5. El resultado vuelve al modelo, que puede corregir la llamada o responder.

Un solo punto de comprobación

El despachador, la función que elige qué código ejecutar según el nombre de la tool, es un buen lugar para esta comprobación. Las lecturas y escrituras atraviesan el mismo punto. Si cada handler inventa sus propias reglas sobre tipos y campos requeridos, añadir una tool nueva puede abrir un hueco sin que nadie lo note.

Define la tool una sola vez y usa esa definición para anunciarla al proveedor y para comprobar los argumentos recibidos. El patrón no necesita un servidor: en Gymnasia la comprobación ocurre localmente. Fíjate en que el handler solo se ejecuta después de que el validador haya aceptado la llamada:

when a complete tool call arrives:
    find the tool definition
    parse the arguments as a JSON object
    errors = validate(arguments, definition.input_schema)
    if errors exist:
        return a tool result with the failing fields and reasons
    return run_handler(arguments)

append the tool result to the conversation
ask the model for the next turn, within the round limit

OpenAI, Anthropic y Google Interactions usan formatos distintos para pedir tools y recibir resultados. Una vez reconstruida la llamada, las tools de Coach llegan al mismo ejecutor. El proveedor compatible con OpenAI también usa ese ejecutor. No hacen falta cuatro versiones de las reglas de entrada.

Un error que permite continuar

Para guardar un peso, Gymnasia usa una tool llamada write_measurement. Este ejemplo no pasa: el campo data.weight_kg contiene texto, aunque tenga aspecto de número. La fecha y el resto del objeto pueden estar bien:

{"date":"2024-04-11","data":{"weight_kg":"75,5"}}

El resultado dice que la tool no se ejecutó y señala data.weight_kg como campo que debe ser un número. El harness devuelve ese resultado al modelo con la identidad de la llamada correspondiente. En la siguiente ronda, el modelo puede enviar:

{"date":"2024-04-11","data":{"weight_kg":75.5}}

El harness no convierte el texto ni vuelve a ejecutar por su cuenta. El modelo propone la corrección y la nueva llamada vuelve a comprobarse. Si es válida, se ejecuta; si sigue fallando, recibe otro error. Todo ocurre dentro del límite de rondas existente. Un error de argumentos conocido permite continuar; una escritura cuyo resultado sea incierto exige otro tratamiento y no debe repetirse como si nada hubiera pasado.

Casos reproducibles en Gymnasia

Estos casos proceden de pruebas del ejecutor y de fixtures reproducibles, respuestas de proveedor preparadas para tests. No son transcripciones de conversaciones privadas ni una medida de la frecuencia con que un modelo se equivoca.

Entrada problemáticaComportamiento anteriorCon la validación central
Medición enviada como JSON dentro de un string, con un número escrito como texto.El contrato de mediciones aceptaba ese formato heredado y normalizaba el valor.Se pide un objeto con valores numéricos, como anuncia el schema. La medición existente se conserva mientras se corrige.
Una comida escrita como “ desayuno ”.El handler normalizaba espacios y mayúsculas.Coach recibe los valores permitidos del enum y puede reenviar “Desayuno”.
JSON ilegible en OpenAI para una tool sin campos requeridos.El parser lo sustituía por un objeto vacío, que podía llegar al handler.Se devuelve un fallo de argumentos sin ejecutar la tool. Un objeto vacío legítimo sigue siendo válido si el schema lo permite.
Un campo numérico opcional enviado como null.El validador omitía ese valor.Se rechaza si el tipo no admite null, incluso en objetos o arrays anidados.

La forma no basta para decidir

El schema comprueba la forma, no toda la intención ni las reglas del producto. Una fecha puede ser un string y representar un día imposible. Un identificador puede tener el tipo correcto y referirse a un ejercicio inexistente. La validación de fechas, referencias y series sigue perteneciendo al dominio. Pasar el schema tampoco concede permiso para una acción que necesita confirmación.

En Gymnasia el validador propio cubre los tipos y restricciones que declara su catálogo, no toda la especificación JSON Schema. No añade una dependencia nueva al bundle móvil ni transforma los argumentos. Los campos extra se permiten cuando el schema no los prohíbe y se rechazan cuando additionalProperties es false. Un campo opcional puede omitirse; eso no convierte null en un número. Estas reglas están documentadas en las referencias de JSON Schema sobre objetos y null.

Si tus schemas empiezan a necesitar uniones, referencias o nuevas restricciones, amplía el validador y su contrato o adopta una librería que las soporte. No anuncies una regla que el programa nunca comprueba. Las tools que declaran un campo como string con JSON dentro necesitan además interpretar y validar ese contenido.

El contrato también puede cambiar durante el transporte. En una prueba real con OpenAI Responses, pedir solo el peso hizo que el proveedor exigiera todas las medidas y el modelo rellenara las demás con 0,01. En un ensayo temporal con strict:false, el schema conservó los campos opcionales y el modelo envió solo el peso. La validación local siguió activa. La documentación de OpenAI explica el valor por defecto de strict.

Pruebas del contrato y de sus efectos

Las pruebas unitarias deben cubrir argumentos válidos sin modificación, campos requeridos ausentes, tipos incorrectos, enums, límites y objetos anidados. Gymnasia compara además su validador con Ajv en tests para el subconjunto de schemas usado. La generación de argumentos arbitrarios, o fuzzing, busca entradas que causen excepciones o discrepancias.

La propiedad que importa al ejecutar es observable: una llamada inválida no carga datos, resuelve catálogos, crea IDs ni produce una escritura del handler. Los tests de integración reproducen una medición inválida, el resultado de error, una llamada corregida y una sola escritura. El E2E hace ese recorrido desde el chat con OpenAI, Anthropic y Google simulados, comprueba el almacenamiento y vuelve a cargar la app. Esto demuestra el comportamiento local con esas respuestas, no que un modelo real siempre se corrija.

Ver la implementación y sus pruebas en la PR de Gymnasia. La validación central ya se ha fusionado y publicado en web.

La QA con OpenAI real detectó además que el transporte cambiaba el contrato de los campos opcionales. La corrección adicional ya está publicada en web. La QA final con el código publicado, sin intervención temporal de red, registró solo 75,9 kg y conservó las demás medidas al recargar.

Una serie sobre cómo construir agentes

Esta entrega forma parte de la serie para construir un agente y su harness usando una aplicación de gimnasio como caso práctico. Cada artículo desarrolla una decisión que puedes trasladar a tu propio agente.

Ver el índice de la serie

Seguir leyendo

Últimos posts -->

¿Has visto estos proyectos?

Gymnasia

Gymnasia Gymnasia
Expo
React Native
TypeScript
OpenAI
Anthropic

App de fitness con dos agentes que se ejecutan íntegramente en el dispositivo, sin backend, de forma que los datos del usuario nunca salen del móvil. Un coach conversacional BYOK con tools locales y system prompt remoto con fallback offline, y un estimador de comidas que saca los macronutrientes de una foto del plato, con lectura de códigos de barras contra OpenFoodFacts.

LangGraph Deep Researcher

LangGraph Deep Researcher LangGraph Deep Researcher
Python
LangGraph
FastAPI
React
TypeScript
Docker

Sistema multiagente de investigación construido con LangGraph. Un supervisor descompone tu pregunta en temas y lanza subagentes de búsqueda en paralelo; cada uno comprime sus hallazgos antes de pasarlos a un agente redactor que escribe el informe final en markdown con sus fuentes. Streaming en vivo por WebSockets, modelo configurable por rol y claves de API propias que nunca se guardan en el servidor.

Tau

Tau Tau
Python
LangChain

Sistema multiagente de tutoría para estudiantes de secundaria, con un agente por asignatura y material de curso elaborado y validado por un equipo de profesores. Llegó a usarse con alumnos reales en un colegio privado en España y en un instituto en Colombia.

Ver todos los proyectos -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres ver alguna charla?

Últimas charlas -->

¿Quieres mejorar con estos tips?

Últimos tips -->

Usa esto en local

Los espacios de Hugging Face nos permite ejecutar modelos con demos muy sencillas, pero ¿qué pasa si la demo se rompe? O si el usuario la elimina? Por ello he creado contenedores docker con algunos espacios interesantes, para poder usarlos de manera local, pase lo que pase. De hecho, es posible que si pinchas en alún botón de ver proyecto te lleve a un espacio que no funciona.

Flow edit

Flow edit Flow edit

Edita imágenes con este modelo de Flow. Basándose en SD3 o FLUX puedes editar cualquier imagen y generar nuevas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos los contenedores -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres entrenar tu modelo con estos datasets?

short-jokes-dataset

HuggingFace

Dataset de chistes en inglés

Uso: Fine-tuning de modelos de generación de texto humorístico

231K filas 2 columnas 45 MB
Ver en HuggingFace →

opus100

HuggingFace

Dataset con traducciones de inglés a español

Uso: Entrenamiento de modelos de traducción inglés-español

1M filas 2 columnas 210 MB
Ver en HuggingFace →

netflix_titles

HuggingFace

Dataset con películas y series de Netflix

Uso: Análisis de catálogo de Netflix y sistemas de recomendación

8.8K filas 12 columnas 3.5 MB
Ver en HuggingFace →
Ver más datasets -->