Tool calling en producción: protege tus APIs del LLM
Tu agente funciona en el entorno de desarrollo. Los tests pasan. Haces el deploy. Y tres días después recibes un error de producción que no deberías ver nunca: una API lanzó una excepción porque llegó un campo numérico en formato string. O faltaba un campo obligatorio. O el valor estaba fuera de rango.
El modelo generó un tool call incorrecto. Y no había nada que lo parara antes de llegar a tu sistema.
El error que nadie testea en desarrollo
Cuando construyes un agente con tool calling — el mecanismo por el que un LLM decide llamar a una función externa — tus pruebas casi siempre cubren el caso feliz: el modelo genera el JSON correcto, la herramienta se ejecuta, el resultado vuelve al agente.
Lo que rara vez se testea:
- El modelo genera
"amount": "150"en lugar de"amount": 150— número como string - Omite
customerIdporque en el prompt anterior ese campo no era relevante - Inventa una fecha en el pasado:
"dueDate": "2024-03-01" - Pasa un UUID sintácticamente válido que no existe en tu base de datos
Estos no son bugs del modelo. Son características estadísticas de los LLMs: en muestras suficientemente grandes, ocurrirán. En producción, con miles de llamadas reales, las muestras son siempre suficientemente grandes.
El error arquitectónico más habitual es no tener ninguna capa entre el output del modelo y la ejecución de la herramienta.
Tres capas de validación para agentes en producción
La defensa se construye en tres niveles. Cada uno atrapa un tipo distinto de error.
Capa 1 — Validación de esquema con Zod
La primera línea de defensa verifica que el JSON generado por el modelo cumple el contrato de la herramienta. Nunca ejecutes el tool call con los argumentos crudos del modelo:
import { z } from 'zod';
const SendInvoiceSchema = z.object({
customerId: z.string().uuid(),
amount: z.number().positive(),
currency: z.enum(['EUR', 'USD', 'GBP']),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
type SendInvoiceArgs = z.infer<typeof SendInvoiceSchema>;
function validateToolCall(name: string, args: unknown): SendInvoiceArgs {
const result = SendInvoiceSchema.safeParse(args);
if (!result.success) {
throw new ToolValidationError(name, result.error.format());
}
return result.data;
}
Si la validación falla, no llegas a la API. El error se gestiona internamente, no como una excepción de producción.
Capa 2 — Retry con feedback de error al modelo
La validación sola no es suficiente si simplemente terminas el proceso cuando falla. Lo que distingue a un agente robusto es que cuando falla la validación, devuelve el error al modelo y le da la oportunidad de corregirlo:
async function executeWithRetry<T>(
toolName: string,
rawArgs: unknown,
validator: (args: unknown) => T,
executor: (args: T) => Promise<unknown>,
llm: LLMClient,
maxAttempts = 3
): Promise<unknown> {
let currentArgs = rawArgs;
let lastError: string | null = null;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const validArgs = validator(currentArgs);
return await executor(validArgs);
} catch (err) {
lastError = err instanceof Error ? err.message : String(err);
if (attempt + 1 >= maxAttempts) break;
// Devolver el error al modelo para que corrija el tool call
currentArgs = await llm.correctToolCall(toolName, currentArgs, lastError);
}
}
throw new Error(
`Tool ${toolName} failed after ${maxAttempts} attempts. Last error: ${lastError}`
);
}
El método correctToolCall envía al modelo un mensaje del tipo: "El tool call que generaste para send_invoice falló con este error: amount debe ser un número positivo, recibiste una cadena. Corrige los argumentos." El modelo genera la versión corregida. Este ciclo ocurre en milliseconds y es invisible para el usuario final.
Capa 3 — Validación semántica de negocio
Hay errores que un schema nunca puede atrapar porque son válidos sintácticamente pero incorrectos desde el punto de vista del negocio:
amount: 0.001— número positivo válido, pero una factura de 0,1 céntimos es un errordueDate: "2020-01-01"— formato correcto, pero en el pasadocustomerId: "uuid-válido-pero-de-cliente-dado-de-baja"— UUID correcto, cliente inactivo
Estas validaciones van dentro de la herramienta misma, antes de ejecutar la operación real:
async function sendInvoice(args: SendInvoiceArgs): Promise<void> {
if (args.amount < 1) {
throw new BusinessValidationError('El importe mínimo de factura es 1€');
}
const dueDate = new Date(args.dueDate);
if (dueDate < new Date()) {
throw new BusinessValidationError('La fecha de vencimiento no puede ser pasada');
}
const customer = await db.customer.findUnique({ where: { id: args.customerId } });
if (!customer?.active) {
throw new BusinessValidationError(
`Cliente ${args.customerId} no encontrado o dado de baja`
);
}
await invoiceService.create(args);
}
Estos errores de negocio se retroalimentan al modelo con el mismo patrón de retry de la capa 2.
Lo que separa un prototipo de un agente en producción real
Un agente de desarrollo funciona cuando el modelo está en modo cooperativo y el input es el esperado. Un agente de producción funciona bajo condiciones adversas: inputs inesperados, modelos con alucinaciones puntuales, reinicios de proceso.
Las tres capas — esquema, retry con feedback, validación de negocio — no son optimizaciones opcionales. Son la diferencia entre un prototipo y un sistema que puede tocar dinero real, datos de clientes o flujos operativos sin supervisión constante.
El artículo anterior sobre agentes IA stateful cubre el problema del estado persistente. Este es el problema hermano: qué pasa cuando el LLM genera una llamada incorrecta. Son dos capas distintas de la misma arquitectura robusta.
En los proyectos de integración IA y agentes de automatización que construimos en DAILYMP, estas tres capas van siempre en el diseño inicial. Añadirlas como parche después del primer fallo visible en producción es más caro, más arriesgado y más difícil de razonar.
Si tienes un agente que ya está en producción y no sabe qué hace cuando el modelo genera un tool call incorrecto, merece revisarlo antes de que ocurra el primer fallo real: