Qué hace realmente el function calling en un LLM

Pregúntale a un modelo “qué tiempo hace ahora en Austin” sin ninguna conexión con el mundo exterior, y tiene dos opciones: decir que no lo sabe, o inventarse algo. El function calling le da una tercera opción: el modelo describe lo que necesita, se lo pasa a tu código y espera una respuesta real antes de contestar.

El nombre es un poco engañoso. El modelo no llama a nada. Emite un bloque de datos estructurados que dice qué tool quiere y con qué argumentos, y tu aplicación decide si lo ejecuta y qué hace con el resultado. “Tool use” es el término más preciso, y es el que usa la documentación de Anthropic; OpenAI y Google suelen decir function calling. El mecanismo es el mismo bajo los dos nombres. El modelo solo produce una petición, nunca el efecto.

Por qué el modelo pide tools pero nunca los ejecuta

Imagina a un cliente en un mostrador que puede señalar platos en el menú y decir cuántos quiere, pero nunca entra detrás del mostrador. El modelo es el cliente: lee el menú que le has dado (las definiciones de los tools), elige un plato y dice cuántos quiere (los argumentos). Tu código es la persona detrás del mostrador que prepara el pedido, y que puede rechazar uno que no tiene sentido.

Esa separación es todo el modelo de seguridad. Una tool call es una sugerencia que tu código puede validar o rechazar, igual que cualquier otra entrada de una fuente no confiable, porque eso es exactamente lo que es. Un modelo al que una página web hostil le ha hecho un prompt injection solo puede pedir una tool call. No puede saltarse tu validación y llegar directamente a la base de datos.

Cómo definir un tool con JSON Schema

Cada proveedor quiere las mismas tres cosas para cada tool: un nombre, una descripción que el modelo usa para decidir cuándo aplica el tool, y un JSON Schema para los argumentos. Aquí tienes un tool de clima para la OpenAI Chat Completions API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
weather_tool = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current temperature and conditions for a city.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name, e.g. 'Austin, TX'"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["city"],
        },
    },
}

Dedica más tiempo a la descripción que al esquema. El modelo elige un tool comparando la petición del usuario con las descripciones de la lista que le mandaste. Una descripción vaga (“cosas del clima”) se queda fuera frente a un tool mejor descrito que hace otra cosa, o se activa en peticiones que no debería manejar. Escríbela como si tuvieras que explicarle la función a un compañero que va a leer exactamente una frase sobre ella.

Cómo funciona el ciclo de petición-respuesta del function calling

Una tool call necesita al menos dos peticiones. Aquí está el recorrido completo con la API de OpenAI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from openai import OpenAI
import json

client = OpenAI()
messages = [{"role": "user", "content": "What's the weather in Austin?"}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=[weather_tool],
)
message = response.choices[0].message
messages.append(message)

if message.tool_calls:
    for call in message.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(args["city"], args.get("unit", "celsius"))
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

    final = client.chat.completions.create(model="gpt-4o", messages=messages)
    print(final.choices[0].message.content)

La primera respuesta no contesta la pregunta. Llega con finish_reason en tool_calls y sin contenido de texto, porque el modelo decidió que necesita get_weather antes de poder decir algo útil. call.function.arguments llega como una cadena JSON, no como un objeto ya parseado, así que json.loads no es opcional. get_weather lo ejecutas tú (es una función Python normal; el modelo no tiene forma de ejecutarla), y luego añades el resultado como un mensaje con role: "tool" y el tool_call_id correspondiente.

Solo la segunda petición, que ahora lleva ese resultado, te devuelve una frase de verdad. Si añades el mensaje del asistente con su tool_calls pero te olvidas del resultado del tool correspondiente, la API rechaza la siguiente petición en vez de adivinar.

En qué se diferencia el tool use de Anthropic del function calling de OpenAI

Mismo mecanismo, nombres de campo distintos. Si cambias de proveedor o soportas ambos, aquí es donde van a salir tus bugs:

ConceptoOpenAIAnthropic
Campo con la lista de toolstools, cada tool envuelto en {"type": "function", "function": {...}}tools, cada tool plano: {"name", "description", "input_schema"}
Clave del esquema de argumentosparametersinput_schema
Petición del modeloarray tool_calls en el mensaje, arguments como cadena JSONbloque de contenido tool_use, input ya como objeto parseado
Envío del resultadomensaje nuevo, role: "tool", tool_call_idmensaje de usuario nuevo, bloque de contenido type: "tool_result", tool_use_id
Señalar una llamada fallidasin campo dedicado: pon el error en contentis_error: true en el bloque tool_result

La trampa está en el formato de los argumentos. Anthropic te da input ya parseado en un dict de Python o un objeto de JS; OpenAI te da una cadena que tienes que pasar tú mismo por json.loads. Código escrito para uno y apuntado al otro se importa sin problemas y falla en la primera tool call. Si soportas los dos, escribe una única función de normalización por la que pasen ambos caminos antes de que tus implementaciones de tools vean los argumentos.

Cómo funcionan las llamadas paralelas y los ciclos de varios pasos

Las dos APIs pueden devolver más de una tool call en una sola respuesta. Pide el clima de tres ciudades y obtienes tres entradas en tool_calls, no tres idas y vueltas separadas. Ejecútalas (en paralelo, si son independientes) y añade los tres resultados antes de la siguiente petición. Cada ID de llamada que emitió el modelo necesita un resultado; envía uno de tres y la petición falla.

Algunas tareas necesitan varias rondas: buscar un usuario, luego su historial de pedidos, luego marcar el pedido atrasado. Escríbelo como un ciclo que sigue llamando al modelo y ejecutando lo que pide hasta que llega una respuesta sin tool calls. Luego ponle un límite. Nada en la API impide que un modelo pida el mismo tool nueve veces seguidas cuando los resultados no lo acercan a una respuesta. Un límite fijo, por ejemplo 10 o 20 iteraciones, convierte eso en un error claro en vez de en una factura descontrolada.

Errores comunes del function calling

Tratar los argumentos como entrada de confianza. args["city"] sale de un modelo, lo que significa que sale de cualquier texto que haya alimentado la conversación, incluida una página web scrapeada o un documento que el modelo tenía que resumir. Válidalo antes de que toque una consulta a la base de datos o un comando de shell. Un tool que construye SQL interpolando los argumentos del modelo tiene la misma superficie de inyección que uno que construye SQL a partir de un formulario web.

Confiar en el esquema como si fuera una garantía. El modelo es bueno siguiendo required y enum, pero no es perfecto. Tarde o temprano pasará "unit": "F" en vez de "fahrenheit", o se olvidará de un campo obligatorio. Valida, y devuelve un mensaje de error claro como resultado del tool en vez de romper. Dile al modelo qué estaba mal y muchas veces puede reintentarlo con argumentos corregidos en la siguiente ronda.

Dejar que el modelo elija tools destructivos por su cuenta. Si un tool borra datos o envía un correo, dejar tool_choice en automático es la opción por defecto equivocada. Mantén los tools destructivos fuera de la lista hasta que una persona apruebe la acción concreta, o haz que el efecto sea reversible (un borrador en vez de un correo enviado) y que un paso separado lo confirme.

Cuándo no usar function calling

Cada tool call añade una ida y vuelta al modelo más la latencia que tenga el tool. Si el “tool” solo va a devolver un dato estático, pon ese dato en el prompt o en el contexto recuperado y ahórrate la ida y vuelta; para eso está el retrieval de RAG. Y si la decisión es en realidad una clasificación con un conjunto pequeño y fijo de resultados (enrutar este ticket a una de cuatro colas), una llamada tipada y no generativa responde más rápido y más barato que pedirle a un modelo de chat genérico que elija un tool. Para eso están pensados los modelos system one.

El function calling compensa su coste cuando la acción depende de datos en tiempo de ejecución que el modelo no puede tener en el prompt, y el conjunto de acciones posibles es demasiado abierto para codificarlo como una máquina de estados. También es el ciclo que hay detrás de cada agente CLI de código: lee un archivo, ejecuta un test, decide qué hacer después, llama a otro tool.

Empieza con un tool, una función de Python fija por detrás, y un ciclo que se detiene después de una sola ida y vuelta. Resuelve bien la validación de argumentos y el camino de error antes de añadir un segundo tool. La mayoría de los bugs en una configuración con varios tools terminan siendo el primer tool manejando mal argumentos que no esperaba.

Artículos relacionados