Function Calling
Function Calling (o Tool Use) es la capacidad de los LLMs modernos de decidir cuándo llamar a una función de tu código, especificar los argumentos con los que llamarla, y utilizar el resultado de esa función para continuar generando la respuesta.
El LLM no ejecuta código directamente: te dice qué función llamar y con qué argumentos. Tu backend ejecuta la función y le devuelve el resultado.
Casos de uso:
- Consultar bases de datos o APIs externas.
- Ejecutar operaciones en tu sistema (crear tareas, enviar emails…).
- Obtener datos en tiempo real (clima, precios, noticias).
- Realizar cálculos complejos.
- Enriquecer respuestas con datos reales.
Function Calling con OpenAI
OpenAI fue el pionero en esta técnica. Para usarla, debemos pasar un array de tools (herramientas) en la petición al modelo.
Definición de herramientas
Cada herramienta se define con un nombre, una descripción clara (fundamental para que el LLM sepa cuándo usarla) y un esquema de parámetros en formato JSON Schema.
const herramientas = [
{
type: 'function',
function: {
name: 'buscarProducto',
description: 'Busca información de un producto por su nombre o ID',
parameters: {
type: 'object',
properties: {
nombre: {
type: 'string',
description: 'Nombre del producto a buscar'
},
incluirStock: {
type: 'boolean',
description: 'Si se debe incluir información de stock'
}
},
required: ['nombre']
}
}
},
{
type: 'function',
function: {
name: 'crearPedido',
description: 'Crea un nuevo pedido para un cliente',
parameters: {
type: 'object',
properties: {
productoId: { type: 'string' },
cantidad: { type: 'number' },
clienteId: { type: 'string' }
},
required: ['productoId', 'cantidad', 'clienteId']
}
}
}
];
Implementación completa
A diferencia de una petición normal, el function calling suele requerir un bucle. El modelo puede decidir llamar a una herramienta, esperar el resultado, y luego decidir si necesita llamar a otra o si ya tiene información suficiente para responder al usuario.
Es vital añadir el resultado de la función al historial con el rol tool y el tool_call_id correspondiente para que el modelo sepa a qué petición de función corresponde cada dato.
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Implementaciones reales de las herramientas
const implementaciones = {
async buscarProducto({ nombre, incluirStock = false }) {
// Aquí iría la consulta real a tu DB
const producto = await db.productos.findOne({ nombre });
if (!producto) return { error: 'Producto no encontrado' };
return {
id: producto.id,
nombre: producto.nombre,
precio: producto.precio,
...(incluirStock && { stock: producto.stock })
};
},
async crearPedido({ productoId, cantidad, clienteId }) {
const pedido = await db.pedidos.create({ productoId, cantidad, clienteId });
return { pedidoId: pedido.id, estado: 'confirmado' };
}
};
async function chatConHerramientas(mensajes) {
const historial = [...mensajes];
// Bucle: el LLM puede llamar múltiples herramientas en secuencia
while (true) {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: historial,
tools: herramientas,
tool_choice: 'auto' // el LLM decide si usar o no herramientas
});
const mensaje = response.choices[0].message;
historial.push(mensaje);
// Si no hay llamadas a herramientas, devolver respuesta final
if (!mensaje.tool_calls?.length) {
return mensaje.content;
}
// Ejecutar cada herramienta solicitada
for (const toolCall of mensaje.tool_calls) {
const nombre = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
console.log(`Ejecutando herramienta: ${nombre}`, args);
let resultado;
try {
resultado = await implementaciones[nombre](args);
} catch (err) {
resultado = { error: err.message };
}
// Añadir resultado al historial
historial.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(resultado)
});
}
// El bucle continúa para que el LLM procese los resultados
}
}
// Uso
const respuesta = await chatConHerramientas([
{ role: 'user', content: '¿Cuánto cuesta el producto "Teclado Mecánico" y hay stock?' }
]);
console.log(respuesta);
// "El Teclado Mecánico cuesta 89,99€ y actualmente hay 23 unidades en stock."
Tool Use con Anthropic (Claude)
Anthropic utiliza una lógica similar pero con ligeras diferencias en la estructura. Claude utiliza el campo stop_reason para indicar que se ha detenido porque necesita que ejecutes una herramienta (tool_use). La respuesta de la herramienta se envía de vuelta con el tipo tool_result.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const herramientas = [
{
name: 'obtener_clima',
description: 'Obtiene el clima actual de una ciudad',
input_schema: {
type: 'object',
properties: {
ciudad: {
type: 'string',
description: 'Nombre de la ciudad'
},
unidades: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Unidades de temperatura'
}
},
required: ['ciudad']
}
}
];
async function chatClaude(pregunta) {
const messages = [{ role: 'user', content: pregunta }];
while (true) {
const response = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
tools: herramientas,
messages
});
messages.push({ role: 'assistant', content: response.content });
// Respuesta final (sin uso de herramientas)
if (response.stop_reason === 'end_turn') {
return response.content.find(b => b.type === 'text')?.text;
}
// Procesar llamadas a herramientas
if (response.stop_reason === 'tool_use') {
const toolResults = [];
for (const block of response.content) {
if (block.type !== 'tool_use') continue;
const resultado = await ejecutarHerramienta(block.name, block.input);
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: JSON.stringify(resultado)
});
}
messages.push({ role: 'user', content: toolResults });
}
}
}
async function ejecutarHerramienta(nombre, args) {
if (nombre === 'obtener_clima') {
// Llamada real a API del clima
return { temperatura: 22, descripcion: 'Soleado', ciudad: args.ciudad };
}
}
Function Calling con Vercel AI SDK
El Vercel AI SDK es, con diferencia, la forma más sencilla de implementar function calling. Elimina la necesidad de escribir el bucle while manualmente gracias al parámetro maxSteps, que permite al SDK gestionar automáticamente los turnos de "llamada-ejecución-respuesta".
Además, permite definir los parámetros usando Zod y ejecutar la lógica directamente en la propiedad execute.
import { generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const { text } = await generateText({
model: openai('gpt-4o-mini'),
messages: [{ role: 'user', content: '¿Cuántos usuarios se registraron hoy?' }],
tools: {
contarUsuarios: tool({
description: 'Cuenta los usuarios registrados en un rango de fechas',
parameters: z.object({
fechaInicio: z.string().describe('Fecha inicio en formato ISO'),
fechaFin: z.string().describe('Fecha fin en formato ISO')
}),
execute: async ({ fechaInicio, fechaFin }) => {
// Consulta real a la DB
const count = await db.usuarios.count({
where: { creadoEn: { gte: fechaInicio, lte: fechaFin } }
});
return { total: count };
}
}),
listarUsuariosRecientes: tool({
description: 'Lista los últimos N usuarios registrados',
parameters: z.object({
limite: z.number().default(10)
}),
execute: async ({ limite }) => {
return await db.usuarios.findMany({
orderBy: { creadoEn: 'desc' },
take: limite,
select: { id: true, email: true, creadoEn: true }
});
}
})
},
maxSteps: 5 // máximo de pasos de razonamiento + herramientas
});
console.log(text);
Forzar el uso de una herramienta específica
Por defecto (tool_choice: 'auto'), el modelo decide si usa o no las herramientas. Sin embargo, a veces queremos obligar al modelo a usar una función concreta (por ejemplo, para asegurarnos de que la salida siempre sea un JSON con un formato específico).
// OpenAI: forzar una herramienta específica
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages,
tools: herramientas,
tool_choice: {
type: 'function',
function: { name: 'extraerDatosFactura' }
}
});
// AI SDK: con toolChoice
const result = await generateText({
model: openai('gpt-4o-mini'),
messages,
tools: { extraerDatosFactura: tool({ ... }) },
toolChoice: { type: 'tool', toolName: 'extraerDatosFactura' }
});
Ejemplo práctico: extracción de datos estructurados
Una de las aplicaciones más potentes del function calling es la extracción de información. Podemos pasarle un texto desordenado (un email, una factura escaneada, una nota de voz transcrita) y pedirle que use una "herramienta" de guardado para convertir ese caos en datos limpios en nuestra base de datos.
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const facturaSchema = z.object({
proveedor: z.string(),
fecha: z.string(),
total: z.number(),
iva: z.number(),
conceptos: z.array(z.object({
descripcion: z.string(),
importe: z.number()
}))
});
async function extraerFactura(textoFactura) {
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: facturaSchema,
prompt: `Extrae los datos de esta factura:\n\n${textoFactura}`
});
return object;
}
const datos = await extraerFactura(`
ACME S.L. - Factura 2024-001
Fecha: 15/03/2024
- Desarrollo web: 1.200€
- Hosting anual: 150€
Base imponible: 1.350€
IVA (21%): 283,50€
TOTAL: 1.633,50€
`);
console.log(datos);
// { proveedor: "ACME S.L.", fecha: "15/03/2024", total: 1633.50, iva: 283.50, ... }