Asegura el tipo de datos en function calling usando TypeScript
Function calling tipado con TypeScript: deja de adivinar lo que devuelve el modelo
Tiempo estimado de lectura: 4 min
- Ideas clave:
- Los LLMs fallan en formato y semántica: validar la salida evita errores en producción.
- Define esquemas con Zod, deriva tipos con z.infer<>, y valida antes de ejecutar herramientas.
- Usa .parse() para fallar rápido en endpoints y .safeParse() para autocorrección en agentes.
- Mide y registra: trazabilidad completa (prompt, response, error de Zod, tool invocada).
Introducción
Cuando un agente llama a una herramienta, el modelo genera un JSON con argumentos. Asumir que ese JSON tendrá la forma correcta es la fuente de la mayoría de fallos en producción. Implementar Function calling tipado con TypeScript: deja de adivinar lo que devuelve el modelo no es opcional: es ingeniería defensiva. Con Zod validas en runtime, con z.infer<> obtienes tipos sincronizados y con un framework que integre ambos cierras el círculo.
Fuentes útiles: Vercel AI SDK, Zod, OpenAI Structured Outputs.
Resumen rápido (lectores con prisa)
Qué es: Validación tipada de la salida de modelos mediante Zod y TypeScript.
Cuándo usarlo: Siempre que un LLM invoque herramientas, modifique estado o llame APIs críticas.
Por qué importa: Previene errores por campos faltantes, tipos incorrectos o JSON mal formado en producción.
Cómo funciona: Define esquemas Zod, deriva tipos con z.infer<>, valida con .parse() o .safeParse() antes de ejecutar.
Por qué tipar el function calling importa ahora
Los LLMs fallan de formas predecibles: omiten campos, envían strings en vez de números, rodean JSON con Markdown o inventan claves. Si procesas ese output con JSON.parse() y as Tipo, renuncias a la seguridad de TypeScript en runtime. El resultado: escrituras corruptas en bases de datos, llamadas a APIs con parámetros inválidos y bugs que sólo aparecen semanas después.
La alternativa técnica es clara:
- declarar el esquema con Zod,
- derivar el tipo TypeScript con
z.infer<>, - validar antes de ejecutar la herramienta.
Eso convierte la entrada del agente en un contrato matemático que protege tu lógica de negocio.
Arquitectura práctica: esquema → validación → ejecución
Patrón recomendado:
- Define el esquema Zod y añádele descripciones que el LLM pueda leer.
- Expón ese esquema en el prompt (o úsalo con Structured Outputs).
- Valida la respuesta del LLM con
.safeParse()o.parse()antes de llamar a la función. - Si falla, captura el
ZodError, loguéalo y opcionalmente reintenta con autocorrección.
Código mínimo (ejemplo de consulta de divisas)
import { tool } from 'ai'; // p. ej. Vercel AI SDK
import { z } from 'zod';
const ExchangeSchema = z.object({
base: z.string().length(3).toUpperCase().describe('Moneda base ISO 4217, ej. USD'),
target: z.string().length(3).toUpperCase().describe('Moneda destino ISO 4217, ej. EUR'),
});
type ExchangeParams = z.infer;
export const getExchangeRate = tool({
description: 'Devuelve el tipo de cambio entre dos monedas',
parameters: ExchangeSchema,
execute: async ({ base, target }: ExchangeParams) => {
const res = await fetch(`https://api.exchangerate-api.com/v4/latest/${base}`);
if (!res.ok) throw new Error('API externa falló');
const data = await res.json();
return { rate: data.rates[target] };
}
});
Si la validación falla, execute nunca se ejecuta: el SDK/Zod detiene la cadena y devuelve un error estructurado.
.parse() vs .safeParse() y autocorrección
Usa .parse() cuando quieras fallar rápido (endpoints HTTP que deben devolver 4xx/5xx). Usa .safeParse() en agentes y workflows que puedan auto‑corregirse sin intervención humana.
Patrón de autocorrección:
- LLM genera JSON.
.safeParse()devuelvesuccess: falseyerror.- Serializas
error.flatten()y lo inyectas en un nuevo prompt: “Tu respuesta falló por X. Corrige el JSON.” - Reintentás N veces con backoff; si sigue fallando, encolas para revisión humana.
Ese ciclo convierte errores estructurales en una conversación de corrección con el modelo, robusta y trazable.
Operaciones y observabilidad
No basta con validar: mide y actúa.
Métricas recomendadas:
- tasa de validación fallida por prompt/modelo,
- latencia media de autocorrección,
- número de reintentos hasta éxito,
- porcentaje de degradaciones a intervención humana.
Registra siempre: prompt, raw response, resultado de Zod (.error.flatten()), y el tool invocado. Eso te da trazabilidad: prompt → response → validación → acción. Sin esos registros no hay postmortem útil.
Decisiones arquitectónicas y trade‑offs
– Structured Outputs (OpenAI) y generateObject reducen errores de formato pero no sustituyen la validación semántica: un amount: -5 puede pasar el schema si no validas signo y rango. Siempre valida con Zod (https://zod.dev/).
– Tipar desde el día 0 exige disciplina: los esquemas son contratos que obligan a diseñar prompts claros y a mantener tests de integración. La deuda que previene compensa la inversión inicial.
– En entornos orquestados (n8n, XState) preferir que el LLM decida la herramienta y que la ejecución quede en una máquina de estado puede ser más seguro para acciones críticas. Igual aplica: la entrada debe validarse antes de actuar.
Conclusión: deja de adivinar, empieza a garantizar
Function calling tipado con TypeScript: deja de adivinar lo que devuelve el modelo — es una fórmula sencilla y comprobada: define el esquema (Zod), extrae el tipo (z.infer<>), valida antes de ejecutar y automatiza la corrección cuando tenga sentido. Esa disciplina transforma un LLM impredecible en un componente confiable de tu arquitectura. Si tu agente escribe en bases de datos, llama APIs facturadas o toma decisiones que afectan a clientes, no hay excusas: valida antes de ejecutar y loguea todo. Así se construyen agentes que pueden correr solos, y no problemas que sólo aparecen en producción.
Para continuar explorando prácticas operativas y experimentos en automatización e IA aplicada, consulta Dominicode Labs. Esta referencia complementa las técnicas descritas y ofrece recursos prácticos para implementar pipelines seguros y trazables en producción.
FAQ
- ¿Por qué no basta con hacer JSON.parse() y castear a un tipo?
- ¿Cuándo debo usar .parse() en lugar de .safeParse()?
- ¿Qué hago si .safeParse() falla continuamente?
- ¿Debo exponer el esquema Zod en el prompt?
- ¿Qué debo registrar para tener trazabilidad adecuada?
- ¿Los Structured Outputs sustituyen la validación con Zod?
¿Por qué no basta con hacer JSON.parse() y castear a un tipo?
Porque JSON.parse() solo asegura formato JSON válido, no la semántica ni la presencia y tipo de campos esperados. Castear con as Tipo ignora la verificación en runtime, lo que permite entradas inválidas que pueden provocar errores en bases de datos o llamadas a APIs en producción.
¿Cuándo debo usar .parse() en lugar de .safeParse()?
Usa .parse() en contextos donde quieras fallar rápido y retornar un error (por ejemplo endpoints HTTP que deben devolver 4xx/5xx). Usa .safeParse() cuando el flujo puede intentar autocorrección o reintentos antes de degradar a intervención humana.
¿Qué hago si .safeParse() falla continuamente?
Serializa el error con error.flatten(), inyecta esa información en un nuevo prompt pidiendo corrección, y reintenta N veces con backoff. Si sigue fallando, encola la unidad para revisión humana y registra el incidente para análisis posterior.
¿Debo exponer el esquema Zod en el prompt?
Sí: exponer el esquema ayuda al modelo a generar la estructura correcta (especialmente con Structured Outputs). Aun así, la validación con Zod debe ejecutarse en runtime; el esquema en el prompt no sustituye la verificación.
¿Qué debo registrar para tener trazabilidad adecuada?
Registra el prompt, la respuesta cruda del modelo, el resultado de Zod (error.flatten()), y la herramienta (tool) invocada. Esos datos permiten reconstruir el flujo prompt → response → validación → acción para postmortems.
¿Los Structured Outputs sustituyen la validación con Zod?
No. Structured Outputs y utilidades como generateObject reducen errores de formato, pero no validan semántica ni rangos (por ejemplo, amount: -5 podría pasar). Sigue validando con Zod en runtime.
