Function Calling y Tool Use
Function calling (o tool use) permite que un LLM invoque funciones externas para obtener datos en tiempo real, ejecutar acciones o interactuar con APIs. Es la base para construir agentes y aplicaciones AI que van más allá de generar texto.
¿Cómo funciona?
1. Defines las herramientas disponibles (schema JSON)
2. Envías el mensaje del usuario + definición de tools
3. El LLM decide SI llamar una tool y con qué parámetros
4. Tú ejecutas la función y retornas el resultado
5. El LLM genera la respuesta final con el resultado
Usuario: "¿Qué clima hace en Madrid?"
│
▼
LLM analiza → Decide llamar: get_weather({ city: "Madrid" })
│
▼
Tu código ejecuta get_weather("Madrid") → { temp: 22, condition: "sunny" }
│
▼
LLM recibe resultado → "En Madrid hace 22°C y está soleado."
Function Calling con OpenAI
Definir herramientas
import OpenAI from 'openai';
const openai = new OpenAI();
const tools: OpenAI.ChatCompletionTool[] = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Obtiene el clima actual de una ciudad',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'Nombre de la ciudad (ej: "Madrid", "Buenos Aires")',
},
units: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Unidad de temperatura',
},
},
required: ['city'],
},
},
},
{
type: 'function',
function: {
name: 'search_database',
description: 'Busca información en la base de datos de la empresa',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Término de búsqueda' },
category: {
type: 'string',
enum: ['products', 'users', 'orders'],
},
limit: { type: 'number', description: 'Máximo de resultados' },
},
required: ['query'],
},
},
},
];
Loop de ejecución
// Las funciones reales que el LLM puede invocar
const toolFunctions: Record<string, Function> = {
get_weather: async ({ city, units = 'celsius' }: any) => {
const response = await fetch(
`https://api.weather.com/v1/current?city=${city}&units=${units}`
);
return response.json();
},
search_database: async ({ query, category, limit = 5 }: any) => {
return prisma[category || 'products'].findMany({
where: { name: { contains: query, mode: 'insensitive' } },
take: limit,
});
},
};
async function chatWithTools(userMessage: string): Promise<string> {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: 'system', content: 'Eres un asistente útil con acceso a herramientas.' },
{ role: 'user', content: userMessage },
];
// Loop: el LLM puede hacer múltiples llamadas a tools
while (true) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools,
tool_choice: 'auto', // 'auto' | 'none' | 'required' | { function: { name: '...' } }
});
const choice = response.choices[0];
// Si no hay tool calls, retornar la respuesta final
if (choice.finish_reason === 'stop') {
return choice.message.content!;
}
// Procesar tool calls
if (choice.message.tool_calls) {
messages.push(choice.message); // Agregar la decisión del LLM
for (const toolCall of choice.message.tool_calls) {
const functionName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
console.log(`🔧 Calling: ${functionName}(${JSON.stringify(args)})`);
try {
const result = await toolFunctions[functionName](args);
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result),
});
} catch (error) {
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({ error: (error as Error).message }),
});
}
}
}
}
}
Tool Use con Anthropic (Claude)
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: 'get_weather',
description: 'Obtiene el clima actual de una ciudad',
input_schema: {
type: 'object' as const,
properties: {
city: { type: 'string', description: 'Nombre de la ciudad' },
},
required: ['city'],
},
},
];
async function claudeWithTools(userMessage: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: userMessage },
];
while (true) {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools,
messages,
});
// Si termina sin tool use, retornar texto
if (response.stop_reason === 'end_turn') {
return response.content
.filter(b => b.type === 'text')
.map(b => b.text)
.join('');
}
// Procesar tool use
if (response.stop_reason === 'tool_use') {
messages.push({ role: 'assistant', content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === 'tool_use') {
const result = await toolFunctions[block.name](block.input);
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: JSON.stringify(result),
});
}
}
messages.push({ role: 'user', content: toolResults });
}
}
}
Herramientas prácticas comunes
const practicalTools: OpenAI.ChatCompletionTool[] = [
// Ejecutar SQL (con permisos restringidos)
{
type: 'function',
function: {
name: 'run_sql_query',
description: 'Ejecuta una consulta SQL SELECT de solo lectura contra la base de datos',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Consulta SQL SELECT' },
},
required: ['query'],
},
},
},
// Enviar email
{
type: 'function',
function: {
name: 'send_email',
description: 'Envía un email a un destinatario',
parameters: {
type: 'object',
properties: {
to: { type: 'string', description: 'Email del destinatario' },
subject: { type: 'string' },
body: { type: 'string', description: 'Contenido del email en HTML' },
},
required: ['to', 'subject', 'body'],
},
},
},
// Crear ticket
{
type: 'function',
function: {
name: 'create_ticket',
description: 'Crea un ticket de soporte en el sistema',
parameters: {
type: 'object',
properties: {
title: { type: 'string' },
description: { type: 'string' },
priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
assignee: { type: 'string', description: 'Email del asignado' },
},
required: ['title', 'description', 'priority'],
},
},
},
];
Seguridad en Function Calling
// 1. Validar argumentos con Zod
const weatherArgs = z.object({
city: z.string().min(1).max(100),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
});
// 2. Whitelist de funciones permitidas
const ALLOWED_FUNCTIONS = new Set(['get_weather', 'search_database']);
function executeToolCall(name: string, args: unknown) {
if (!ALLOWED_FUNCTIONS.has(name)) {
throw new Error(`Function not allowed: ${name}`);
}
// Validar args según la función
const validated = schemas[name].parse(args);
return toolFunctions[name](validated);
}
// 3. SQL injection prevention
async function runSqlQuery({ query }: { query: string }) {
// Solo permitir SELECT
if (!query.trim().toUpperCase().startsWith('SELECT')) {
throw new Error('Solo se permiten consultas SELECT');
}
// Bloquear operaciones peligrosas
const forbidden = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'TRUNCATE'];
if (forbidden.some(f => query.toUpperCase().includes(f))) {
throw new Error('Operación no permitida');
}
return db.query(query);
}
// 4. Confirmación humana para acciones destructivas
async function sendEmail(args: any) {
if (args.to.includes('@external.com')) {
// Requiere confirmación del usuario
return {
requiresConfirmation: true,
message: `¿Confirmas enviar email a ${args.to}?`,
action: args,
};
}
return mailer.send(args);
}
Parallel tool calls
OpenAI puede hacer múltiples llamadas en paralelo:
// El LLM puede decidir llamar varias funciones a la vez:
// "¿Qué clima hace en Madrid y Barcelona?"
// → tool_calls: [
// { function: 'get_weather', arguments: '{"city":"Madrid"}' },
// { function: 'get_weather', arguments: '{"city":"Barcelona"}' }
// ]
// Ejecutar en paralelo
const results = await Promise.all(
toolCalls.map(async (call) => {
const result = await toolFunctions[call.function.name](
JSON.parse(call.function.arguments)
);
return {
role: 'tool' as const,
tool_call_id: call.id,
content: JSON.stringify(result),
};
})
);
messages.push(...results);