Saltar al contenido principal
Intermedio IACódigoTutorial

Function Calling en OpenAI y Claude: Conectar IA con APIs sin Alucinaciones

Tutorial completo de function calling en GPT-4.1 y Claude. Haz que la IA consulte bases de datos, APIs externas y ejecute acciones reales. Con código Node.js paso a paso.

Fran Cobos 9 min de lectura 1710 palabras

Tabla de contenidos

Tu IA puede responder preguntas generales, pero cuando el usuario pregunta “¿cuánto cuesta el vuelo MAD-BCN de mañana?” — inventa un precio. Alucina porque no tiene acceso a datos reales.

Function calling resuelve esto: le das al modelo un catálogo de funciones (buscar vuelos, consultar stock, enviar email) y el modelo decide cuándo usarlas y genera los argumentos correctos. Tú ejecutas la función y devuelves el resultado real.

¿Cómo funciona?

1. Usuario: "¿Cuánto cuesta el vuelo MAD-BCN mañana?"
2. Modelo: "Necesito llamar a buscar_vuelos(origen: 'MAD', destino: 'BCN', fecha: '2026-04-18')"
3. Tu código: ejecuta buscar_vuelos() → API real → [{ precio: 89, aerolinea: 'Vueling' }]
4. Modelo: recibe resultado real → "El vuelo más barato MAD-BCN mañana es 89€ con Vueling"

El modelo nunca ejecuta código directamente — solo genera el nombre de la función y los argumentos en JSON. Tú tienes el control total de la ejecución.

Function Calling con OpenAI (GPT-4.1)

Definir herramientas

import OpenAI from 'openai';

const openai = new OpenAI();

const tools = [
  {
    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", "Barcelona")'
          },
          units: {
            type: 'string',
            enum: ['celsius', 'fahrenheit'],
            description: 'Unidades de temperatura'
          }
        },
        required: ['city'],
        additionalProperties: false
      }
    }
  },
  {
    type: 'function',
    function: {
      name: 'search_products',
      description: 'Busca productos en la base de datos',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'Término de búsqueda' },
          max_price: { type: 'number', description: 'Precio máximo en euros' },
          category: {
            type: 'string',
            enum: ['electrónica', 'ropa', 'hogar', 'deportes']
          }
        },
        required: ['query'],
        additionalProperties: false
      }
    }
  }
];

Implementar las funciones reales

// Tus funciones reales — pueden llamar a APIs, bases de datos, etc.
const availableFunctions = {
  get_weather: async ({ city, units = 'celsius' }) => {
    // Llamada real a API de clima
    const res = await fetch(
      `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_KEY}&q=${encodeURIComponent(city)}`
    );
    const data = await res.json();
    return {
      city,
      temperature: units === 'celsius' ? data.current.temp_c : data.current.temp_f,
      condition: data.current.condition.text,
      units
    };
  },

  search_products: async ({ query, max_price, category }) => {
    // Consulta real a base de datos
    const filters = { query };
    if (max_price) filters.max_price = max_price;
    if (category) filters.category = category;
    // Simulación — en producción sería una query a tu DB
    return [
      { name: 'Auriculares Sony WH-1000XM6', price: 299, category: 'electrónica' },
      { name: 'Auriculares Apple AirPods Pro 3', price: 279, category: 'electrónica' }
    ];
  }
};

El bucle de ejecución

async function chatWithTools(userMessage) {
  const messages = [
    { role: 'system', content: 'Eres un asistente útil. Usa las herramientas cuando necesites datos reales.' },
    { role: 'user', content: userMessage }
  ];

  // Bucle: el modelo puede pedir múltiples funciones
  while (true) {
    const response = await openai.chat.completions.create({
      model: 'gpt-4.1-mini',
      messages,
      tools,
    });

    const choice = response.choices[0];

    // Si el modelo responde con texto, hemos terminado
    if (choice.finish_reason === 'stop') {
      return choice.message.content;
    }

    // Si el modelo pide funciones, ejecutarlas
    if (choice.finish_reason === 'tool_calls') {
      messages.push(choice.message); // Añadir la petición del modelo

      for (const toolCall of choice.message.tool_calls) {
        const functionName = toolCall.function.name;
        const args = JSON.parse(toolCall.function.arguments);

        console.log(`🔧 Ejecutando ${functionName}(${JSON.stringify(args)})`);

        // Ejecutar la función real
        const fn = availableFunctions[functionName];
        if (!fn) {
          throw new Error(`Función desconocida: ${functionName}`);
        }
        const result = await fn(args);

        // Devolver resultado al modelo
        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: JSON.stringify(result)
        });
      }
      // El bucle continúa — el modelo procesará los resultados
    }
  }
}

// Uso
const respuesta = await chatWithTools('¿Qué tiempo hace en Madrid?');
console.log(respuesta);
// → "En Madrid ahora mismo hay 23°C con cielo despejado."

Tool Use con Claude (Anthropic)

Claude usa el mismo concepto pero con sintaxis diferente:

import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic();

const tools = [
  {
    name: 'get_weather',
    description: 'Obtiene el clima actual de una ciudad',
    input_schema: {
      type: 'object',
      properties: {
        city: { type: 'string', description: 'Nombre de la ciudad' },
        units: { type: 'string', enum: ['celsius', 'fahrenheit'] }
      },
      required: ['city']
    }
  }
];

async function chatWithToolsClaude(userMessage) {
  const messages = [{ role: 'user', content: userMessage }];

  while (true) {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 4096,
      tools,
      messages,
    });

    // Si termina sin pedir herramientas
    if (response.stop_reason === 'end_turn') {
      const textBlock = response.content.find(b => b.type === 'text');
      return textBlock?.text || '';
    }

    // Si pide herramientas
    if (response.stop_reason === 'tool_use') {
      messages.push({ role: 'assistant', content: response.content });

      const toolResults = [];
      for (const block of response.content) {
        if (block.type !== 'tool_use') continue;

        const fn = availableFunctions[block.name];
        const result = await fn(block.input);

        toolResults.push({
          type: 'tool_result',
          tool_use_id: block.id,
          content: JSON.stringify(result)
        });
      }

      messages.push({ role: 'user', content: toolResults });
    }
  }
}

Parallel function calling

GPT-4.1 puede pedir múltiples funciones a la vez si las necesita. Por ejemplo: “¿Qué tiempo hace en Madrid y Barcelona?”

// El modelo devuelve tool_calls con 2 funciones:
// tool_calls: [
//   { function: { name: 'get_weather', arguments: '{"city":"Madrid"}' } },
//   { function: { name: 'get_weather', arguments: '{"city":"Barcelona"}' } }
// ]

// Ejecutar en paralelo para mejor rendimiento
const results = await Promise.all(
  choice.message.tool_calls.map(async (toolCall) => {
    const fn = availableFunctions[toolCall.function.name];
    const args = JSON.parse(toolCall.function.arguments);
    const result = await fn(args);
    return {
      role: 'tool',
      tool_call_id: toolCall.id,
      content: JSON.stringify(result)
    };
  })
);

messages.push(choice.message, ...results);

Function calling + streaming

Puedes combinar function calling con streaming para que el usuario vea la respuesta final en tiempo real:

async function streamWithTools(userMessage, onToken) {
  const messages = [
    { role: 'system', content: 'Usa las herramientas cuando necesites datos reales.' },
    { role: 'user', content: userMessage }
  ];

  // Fase 1: obtener tool calls (sin streaming)
  const initial = await openai.chat.completions.create({
    model: 'gpt-4.1-mini',
    messages,
    tools,
  });

  if (initial.choices[0].finish_reason === 'tool_calls') {
    messages.push(initial.choices[0].message);

    // Ejecutar funciones
    for (const tc of initial.choices[0].message.tool_calls) {
      const fn = availableFunctions[tc.function.name];
      const result = await fn(JSON.parse(tc.function.arguments));
      messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });
    }
  }

  // Fase 2: respuesta final con streaming
  const stream = await openai.chat.completions.create({
    model: 'gpt-4.1-mini',
    messages,
    stream: true,
  });

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content;
    if (content) onToken(content);
  }
}

Para una implementación completa de streaming con SSE hasta el navegador, consulta el tutorial de streaming SSE con ChatGPT y Claude en Node.js.

Seguridad: validar antes de ejecutar

NUNCA ejecutes funciones que el modelo pida sin validación. El modelo podría intentar ejecutar funciones que no has definido o pasar argumentos maliciosos.

// ✅ Whitelist de funciones permitidas
const ALLOWED_FUNCTIONS = new Set(['get_weather', 'search_products']);

function executeToolCall(toolCall) {
  const { name, arguments: rawArgs } = toolCall.function;

  // 1. Verificar que la función existe y está permitida
  if (!ALLOWED_FUNCTIONS.has(name)) {
    throw new Error(`Función no permitida: ${name}`);
  }

  // 2. Parsear y validar argumentos
  const args = JSON.parse(rawArgs);

  // 3. Ejecutar
  return availableFunctions[name](args);
}

Function calling vs. agentes

Function calling es el mecanismo base. Un agente lo usa dentro de un bucle de razonamiento:

AspectoFunction callingAgente
Llamadas1-3 por turnoMúltiples en bucle
DecisiónModelo elige funciónModelo planifica y ejecuta secuencia
ComplejidadBajaAlta
Ejemplo”¿Qué clima hace?” → 1 API call”Planifica un viaje” → buscar vuelos + hotel + clima
FrameworkSDK nativo de OpenAI/ClaudeLangChain, CrewAI

Si necesitas un agente completo que encadene múltiples herramientas, revisa el tutorial de cómo crear un agente de IA con LangChain y Node.js.

Caso práctico: asistente de e-commerce

const ecommerceTools = [
  {
    type: 'function',
    function: {
      name: 'search_products',
      description: 'Busca productos en el catálogo',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string' },
          category: { type: 'string' },
          max_price: { type: 'number' }
        },
        required: ['query'],
        additionalProperties: false
      }
    }
  },
  {
    type: 'function',
    function: {
      name: 'get_order_status',
      description: 'Consulta el estado de un pedido por ID',
      parameters: {
        type: 'object',
        properties: {
          order_id: { type: 'string', description: 'ID del pedido (ej: ORD-12345)' }
        },
        required: ['order_id'],
        additionalProperties: false
      }
    }
  },
  {
    type: 'function',
    function: {
      name: 'check_stock',
      description: 'Verifica el stock disponible de un producto',
      parameters: {
        type: 'object',
        properties: {
          product_id: { type: 'string' }
        },
        required: ['product_id'],
        additionalProperties: false
      }
    }
  }
];

// Ahora el chatbot puede:
// "¿Tienen auriculares por menos de 100€?" → search_products
// "¿Dónde está mi pedido ORD-12345?" → get_order_status
// "¿Hay stock del Sony XM6?" → check_stock + search_products

Errores comunes

1. No manejar funciones desconocidas

El modelo puede alucinar un nombre de función. Siempre verifica contra tu whitelist.

2. No devolver errores al modelo

Si la función falla, devuelve el error para que el modelo pueda informar al usuario:

try {
  const result = await fn(args);
  return { role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) };
} catch (error) {
  return { role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ error: error.message }) };
}

3. Descripciones vagas en las herramientas

El modelo decide qué función usar basándose en description. Si es vaga, elegirá mal:

// ❌ El modelo no sabe cuándo usarla
{ description: 'Hace cosas con datos' }

// ✅ El modelo sabe exactamente cuándo usarla
{ description: 'Busca productos en el catálogo por nombre, categoría o rango de precio. Devuelve nombre, precio y disponibilidad.' }

Para escribir mejores descripciones de herramientas (y prompts en general), revisa los mejores prompts para programar con IA.

Conclusión

Function calling es la pieza clave para pasar de un chatbot que inventa datos a uno que consulta información real:

  1. Define herramientas con schemas JSON claros
  2. Implementa un bucle que ejecute las funciones y devuelva resultados al modelo
  3. Valida siempre que las funciones y argumentos son legítimos
  4. Combina con streaming para UX en tiempo real

Es más sencillo de lo que parece — el 80% del trabajo es definir buenos schemas y descripciones. El modelo se encarga del resto.

Si quieres que las respuestas del modelo sean JSON estructurado (no solo texto), revisa cómo parsear JSON de IA sin errores. Y para ejecutar todo esto con modelos gratuitos en local, Ollama es tu mejor opción.

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.