¿Cómo llama un LLM a una función?
Los modelos de lenguaje generan texto. Eso es todo lo que hacen a nivel fundamental: predecir el siguiente token dada una secuencia de tokens anteriores. Pero las funciones requieren algo muy diferente. Una función como
get_weather(city="London")
necesita entrada estructurada — argumentos específicos en un formato específico — no un flujo de lenguaje natural. Entonces, ¿cómo cerramos la brecha entre un modelo que produce texto libre y una función que espera argumentos precisamente estructurados?
La respuesta: entrenar al modelo para que produzca
JSON
estructurado (JavaScript Object Notation, un formato ligero de datos que usa pares clave-valor como
{"city": "London"}
) que especifica
which
función llamar y
con qué argumentos
. The model doesn't execute anything. It just produces a structured call specification — a message that says "I'd like to call this function with these parameters." A separate runtime (your application code, the API server, an orchestration framework) reads that specification, executes the actual function, and feeds the result back to the model so it can continue generating.
Esta separación es crucial. El modelo es un generador de texto sin capacidad de ejecutar código, acceder a bases de datos o hacer solicitudes HTTP. El runtime es la capa de ejecución que realmente realiza esas acciones. El trabajo del modelo es decidir when se necesita una llamada a función, which función llamar, y qué argumentos pasar. El trabajo del runtime es ejecutar y devolver resultados.
El protocolo de llamada a funciones
¿Cómo sabe el modelo qué funciones están disponibles? La API proporciona una lista de definiciones de herramientas junto con la conversación. Cada definición describe una función usando JSON Schema (un estándar para describir la estructura de datos JSON): su nombre, una descripción en lenguaje natural de lo que hace y los parámetros que acepta con sus tipos y restricciones. Así es como luce una definición de herramienta del clima:
{
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'London'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["city"]
}
}
El modelo ve estas definiciones de herramientas como parte de su contexto (se inyectan en el prompt, generalmente en un mensaje de sistema o una sección dedicada de herramientas). Cuando el modelo decide que responder la pregunta del usuario requiere llamar a una función, produce un objeto JSON estructurado especificando el nombre de la función y los argumentos:
{
"name": "get_weather",
"arguments": {
"city": "London",
"unit": "celsius"
}
}
El runtime intercepta esta salida, ejecuta la función real
get_weather
función, obtiene el resultado (p. ej.,
{"temp": 12, "condition": "rain", "humidity": 85}
), y lo envía de vuelta al modelo como un nuevo mensaje en la conversación. El modelo entonces usa ese resultado para formular su respuesta final al usuario.
Diferentes proveedores usan formas de API ligeramente diferentes, pero la idea central es idéntica. OpenAI usa un parámetro
tools
en la API de Chat Completions. Anthropic usa un parámetro
tools
en la API de Messages. Google usa
function_declarations
en la API de Gemini. En todos los casos, proporcionas definiciones de herramientas, el modelo produce una llamada estructurada y tu código la ejecuta.
Así es como se ve el flujo completo como pseudocódigo:
# 1. Define available tools
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
]
# 2. Send user message + tools to the model
response = llm.chat(
messages=[{"role": "user", "content": "What's the weather in London?"}],
tools=tools
)
# 3. Model responds with a tool call (not a text answer)
# response.tool_calls = [{"name": "get_weather", "arguments": {"city": "London"}}]
# 4. Your code executes the function
result = get_weather(city="London") # => {"temp": 12, "condition": "rain"}
# 5. Send the result back to the model
final = llm.chat(
messages=[
{"role": "user", "content": "What's the weather in London?"},
{"role": "assistant", "tool_calls": response.tool_calls},
{"role": "tool", "content": json.dumps(result)}
],
tools=tools
)
# 6. Model now responds with natural language:
# "It's 12°C and rainy in London right now."
Y aquí hay una simulación en Python puro que recorre el mismo flujo de principio a fin, usando un modelo simulado y una función de clima simulada, para que puedas ver los datos en cada paso:
import json
# ---- Mock weather function (the "tool") ----
def get_weather(city, unit="celsius"):
"""Simulate a weather API."""
db = {
"london": {"temp": 12, "condition": "rain", "humidity": 85},
"paris": {"temp": 18, "condition": "cloudy", "humidity": 60},
"tokyo": {"temp": 24, "condition": "sunny", "humidity": 45},
}
data = db.get(city.lower(), {"temp": 0, "condition": "unknown", "humidity": 0})
if unit == "fahrenheit":
data = {**data, "temp": round(data["temp"] * 9/5 + 32)}
return data
# ---- Tool registry (what the model sees) ----
tools = {
"get_weather": {
"function": get_weather,
"schema": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": ["city", "unit"]
}
}
}
# ---- Simulate model deciding to call a tool ----
user_query = "What's the weather in London?"
# Step 1: Model "decides" to call get_weather (simulated)
model_tool_call = {"name": "get_weather", "arguments": {"city": "London", "unit": "celsius"}}
print(f"User: {user_query}")
print(f"Model call: {json.dumps(model_tool_call)}")
# Step 2: Runtime executes the function
func = tools[model_tool_call["name"]]["function"]
result = func(**model_tool_call["arguments"])
print(f"Tool result: {json.dumps(result)}")
# Step 3: Model uses the result to respond (simulated)
model_response = (
f"It's currently {result['temp']}°C and {result['condition']} "
f"in London, with {result['humidity']}% humidity."
)
print(f"Model reply: {model_response}")
Cómo los modelos aprenden a usar herramientas
Un modelo de lenguaje base (uno que solo ha sido pre-entrenado en predicción de texto) no sabe cómo producir llamadas estructuradas a funciones. Esa capacidad se añade durante el post-training , específicamente a través de ajuste fino supervisado (SFT) y aprendizaje por refuerzo con retroalimentación humana (RLHF) — las mismas etapas que enseñan al modelo a seguir instrucciones y ser útil. (Para más sobre cómo funciona el SFT, consulta Why Fine-tune? .) Los datos de entrenamiento incluyen miles de ejemplos que muestran el patrón: el usuario hace una pregunta → el modelo produce una llamada estructurada a herramienta → la herramienta devuelve un resultado → el modelo incorpora el resultado en una respuesta en lenguaje natural.
Pero, ¿de dónde vienen esos ejemplos de entrenamiento? Crearlos manualmente es costoso. (Schick et al., 2023) introdujo Toolformer , un método que permite a los modelos de lenguaje enseñarse a sí mismos a usar herramientas. La idea clave es elegante: empezar con un modelo pre-entrenado, permitirle insertar llamadas candidatas a herramientas en varias posiciones de su texto de entrenamiento, ejecutar esas llamadas y luego filter — conservar solo los ejemplos donde la llamada a herramienta realmente redujo la perplejidad del modelo (es decir, la predicción de la siguiente palabra del modelo mejoró con el resultado de la herramienta comparado con sin él). Esto crea un conjunto de datos de alta calidad de ejemplos de uso de herramientas sin anotación humana.
For example, given the sentence "The Eiffel Tower is 330 metres tall", Toolformer might insert a calculator call to verify the arithmetic, or a Wikipedia lookup to confirm the fact. If the tool result makes the model more confident in the continuation, the example is kept. If not, it's discarded. The resulting model learns not just how llamar herramientas sino when llamarlas es realmente útil — una distinción crítica.
Los modelos modernos de llamada a funciones soportan dos capacidades adicionales que vale la pena conocer:
- Llamadas paralelas a herramientas: when a query requires multiple independent function calls (e.g., "compare the weather in London and Paris"), the model can emit both calls simultaneously rather than waiting for one to complete before issuing the next. This reduces latency by executing the calls concurrently.
- Uso forzado de herramientas: the API can require the model to call a specific tool regardless of whether it "wants" to. This is useful for structured data extraction — if you always need the model to output data through a particular tool schema, forced tool use garantiza it won't skip the call and respond in plain text instead.
Salidas estructuradas y modo JSON
Las llamadas a herramientas dependen de que el modelo produzca JSON válido y bien formado. Pero los modelos de lenguaje son generadores probabilísticos de tokens — no garantizan inherentemente la corrección sintáctica. Una llave de cierre faltante, una coma al final, un carácter de comilla sin escapar — cualquiera de estos rompe el parser de JSON, y una llamada a herramienta rota rompe todo el bucle del agente. Si tu agente llama a tres herramientas en secuencia y la segunda llamada produce JSON malformado, todo lo que viene después falla.
Por eso los principales proveedores de API han introducido modos de salida estructurada (a veces llamados modo JSON ) que restringen al modelo para producir JSON válido que coincida con un esquema dado. En lugar de esperar que el modelo formatee las cosas correctamente, el sistema garantiza que lo haga.
¿Cómo funciona esto internamente? La técnica se llama
decodificación restringida
. En cada paso de generación de tokens, el modelo produce una distribución de probabilidad sobre todo su vocabulario (típicamente 50,000–100,000+ tokens). Normalmente, cualquier token puede ser muestreado. Con decodificación restringida, el sistema
enmascara
(establece la probabilidad en cero para) cada token que haría que la salida sea JSON inválido en ese punto. Si el modelo acaba de producir
{"city":
, solo se permiten tokens que comienzan un valor JSON válido (una comilla para una cadena, un dígito para un número, etc.). Tokens como
}
or
,
que crearían un error de sintaxis son bloqueados.
This means the model's output follows a valid JSON path at every single step. It's not a post-hoc fix ("generate freely, then try to parse") — it's a restricción generativa que hace literalmente imposible producir JSON inválido. La contrapartida es una pequeña cantidad de cómputo adicional por token para mantener la máquina de estados de restricción, pero la ganancia en confiabilidad es enorme.
En la práctica, interactúas con esta función a través de parámetros de API. OpenAI proporciona
response_format: { type: "json_schema", json_schema: {...} }
en la API de Chat Completions, que restringe al modelo para producir JSON que coincida con un esquema específico. El sistema de uso de herramientas de Anthropic restringe automáticamente el campo
input
de las llamadas a herramientas para coincidir con el
input_schema
que proporcionas en cada definición de herramienta, dándote salidas validadas por esquema sin un modo separado.
Aquí hay una simulación que muestra cómo la decodificación restringida limita el vocabulario del modelo en cada paso de generación:
import json
# Simulate decodificación restringida for JSON generation
# At each step, show which token types are ALLOWED vs BLOCKED
schema = {
"type": "object",
"properties": {
"city": {"type": "string"},
"temp": {"type": "number"}
},
"required": ["city", "temp"]
}
# Walk through a decodificación restringida trace
steps = [
{
"generated_so_far": "",
"next_token": "{",
"allowed": ["{ (object start)"],
"blocked": ["any letter", "[ (array)", "number", "null"]
},
{
"generated_so_far": "{",
"next_token": '"city"',
"allowed": ['"city"', '"temp"'],
"blocked": ["any non-required key", "} (object needs required keys)", "number"]
},
{
"generated_so_far": '{"city"',
"next_token": ":",
"allowed": [": (key-value separator)"],
"blocked": [", (need value first)", "} (need value first)", "any letter"]
},
{
"generated_so_far": '{"city":',
"next_token": '"London"',
"allowed": ['"any string" (schema says type=string)'],
"blocked": ["number", "true/false", "null", "{ (not an object)"]
},
{
"generated_so_far": '{"city":"London"',
"next_token": ",",
"allowed": [", (more required keys remain)"],
"blocked": ["} (temp is required but missing)"]
},
{
"generated_so_far": '{"city":"London",',
"next_token": '"temp"',
"allowed": ['"temp" (still required)'],
"blocked": ['"city" (already present)', "} (temp still missing)"]
},
{
"generated_so_far": '{"city":"London","temp":',
"next_token": "12",
"allowed": ["any number (schema says type=number)"],
"blocked": ['"string"', "true/false", "null"]
},
{
"generated_so_far": '{"city":"London","temp":12',
"next_token": "}",
"allowed": ["} (all required keys present)"],
"blocked": ["any letter", "number (already complete)"]
},
]
print("Constrained Decoding Trace")
print("=" * 55)
for i, step in enumerate(steps):
print(f"
Step {i+1}: next token = {step['next_token']}")
print(f" So far: {step['generated_so_far'] + step['next_token']}")
print(f" Allowed: {', '.join(step['allowed'])}")
print(f" Blocked: {', '.join(step['blocked'])}")
final = '{"city":"London","temp":12}'
parsed = json.loads(final)
print(f"
Final output: {final}")
print(f"Valid JSON: {True}")
print(f"Parsed: {parsed}")
Diseño de herramientas: ¿Qué hace una buena herramienta?
La capacidad de un modelo para usar una herramienta correctamente depende casi completamente de lo bien que esa herramienta está descrita . Recuerda: el modelo nunca ha visto el código fuente de tu función. Todo lo que sabe es el nombre, la descripción y el esquema de parámetros que proporcionaste. La descripción de la herramienta is la documentación — si la descripción es vaga, el modelo usará la herramienta de forma vaga. Si es precisa, el modelo la usará de forma precisa.
¿Qué separa una herramienta bien diseñada de una mal diseñada? Estos son los principios que más importan en la práctica:
-
Nombres claros y descriptivos:
search_weble dice al modelo exactamente qué hace esta herramienta.fn_042no le dice nada. El modelo selecciona herramientas en parte basándose en la coincidencia del nombre con la intención del usuario, así que un buen nombre es una señal fuerte. -
Descripciones ricas de parámetros:
don't just specify types — include what each parameter means, example values, and constraints. "
city: The city name, e.g. 'London', 'New York'" is far more useful than "city: string". El modelo usa estas descripciones para determinar qué valor pasar. -
Acciones atómicas:
una herramienta debe hacer una cosa bien. Una herramienta que busca en la web
and
resume los resultados combina dos operaciones distintas. Si el modelo solo necesita buscar (no resumir), no tiene forma de usar la mitad de una herramienta. Divídela en
search_webandsummarise_text— el modelo puede componerlas cuando sea necesario. -
Valores de retorno informativos:
devuelve suficiente contexto para que el modelo razone sobre el resultado. Una herramienta del clima que devuelve
{"temp": 12, "condition": "rain", "humidity": 85}es mucho más útil que una que devuelve solo"12". El modelo necesita contexto para formular una respuesta útil. - Mensajes de error claros: when a tool call fails, return a human-readable error like "City 'Londno' not found. Did you mean 'London'?" rather than a raw stack trace. The model will see this error and can either retry with corrected arguments or explain the issue to the user.
Hay una preocupación práctica más que es fácil pasar por alto: cantidad de herramientas . Cada definición de herramienta consume tokens de la ventana de contexto. Más importante aún, los modelos tienen dificultades con la selección cuando se les presentan demasiadas opciones. La investigación y la experiencia práctica muestran que el rendimiento se degrada notablemente con más de 50 herramientas — el modelo comienza a elegir la herramienta incorrecta o a alucinar nombres de herramientas. Mantén el conjunto activo de herramientas enfocado en lo que se necesita para la tarea actual. Si tienes 200 herramientas, usa una capa de enrutamiento que seleccione un subconjunto relevante (digamos, 5–10) basado en la consulta del usuario antes de pasarlas al modelo.
Aquí hay un ejemplo contrastando una definición de herramienta pobre con una buena:
import json
# ---- BAD tool definition ----
bad_tool = {
"name": "fn_042",
"description": "does stuff",
"parameters": {
"type": "object",
"properties": {
"q": {"type": "string"},
"n": {"type": "integer"}
}
}
}
# ---- GOOD tool definition ----
good_tool = {
"name": "search_knowledge_base",
"description": (
"Search the internal knowledge base for documents matching a query. "
"Returns the top-n most relevant documents with titles and snippets. "
"Use this when the user asks about company policies, product specs, "
"or internal procedures."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query, e.g. 'vacation policy for remote employees'"
},
"max_results": {
"type": "integer",
"description": "Number of results to return (1-20, default 5)",
"minimum": 1,
"maximum": 20
}
},
"required": ["query"]
}
}
print("BAD tool definition:")
print(json.dumps(bad_tool, indent=2))
print()
print("Problems:")
print(" - Name 'fn_042' gives the model no hint about what it does")
print(" - Description 'does stuff' is useless for tool selection")
print(" - Parameters 'q' and 'n' are cryptic abbreviations")
print(" - No parameter descriptions or constraints")
print(" - No required fields specified")
print()
print("GOOD tool definition:")
print(json.dumps(good_tool, indent=2))
print()
print("Improvements:")
print(" - Name clearly describes the action: search_knowledge_base")
print(" - Description explains what, when, and what it returns")
print(" - Parameters have full names and descriptions with examples")
print(" - Constraints (min/max) prevent invalid arguments")
print(" - Required fields are explicit")
El bucle de uso de herramientas en la práctica
Now let's put it all together by walking through a complete tool-use interaction, step by step. Consider a user who asks: "What's the weather in London and should I bring an umbrella?"
La interacción se desarrolla en cuatro etapas:
- Etapa 1 — Consulta del usuario: el usuario envía su mensaje. El modelo lo recibe junto con las definiciones de herramientas.
-
Etapa 2 — El modelo decide llamar a una herramienta:
el modelo determina que necesita datos actuales del clima para responder la pregunta. Produce una llamada estructurada:
get_weather(city="London"). -
Etapa 3 — El runtime ejecuta:
tu código llama a la API real del clima. Devuelve
{"temp": 12, "condition": "rain", "humidity": 85}. - Etapa 4 — El modelo responde: the model reads the tool result, reasons that rain means yes to the umbrella question, and generates: "It's 12°C and rainy in London — definitely bring an umbrella!"
Now consider a more complex case: "Compare the weather in London and Paris." The model needs data from two ciudades. Con llamadas paralelas a herramientas, puede emitir ambas llamadas simultáneamente:
- Etapa 1: el usuario pide una comparación.
-
Etapa 2:
el modelo emite
two
llamadas a herramientas en una respuesta:
get_weather(city="London")andget_weather(city="Paris"). - Etapa 3: el runtime ejecuta ambas concurrentemente (ya que son independientes). Ambos resultados se devuelven.
- Etapa 4: model compares the two results and responds: "London is 12°C and rainy while Paris is 18°C and cloudy — Paris is warmer and drier today."
El código a continuación es una simulación completa y ejecutable de este bucle de uso de herramientas. Implementa un modelo simulado (que selecciona herramientas basándose en coincidencia simple de palabras clave) y una API de clima simulada, luego recorre tanto los escenarios de llamada única como de llamadas paralelas:
import json
# ---- Mock weather API ----
def get_weather(city, unit="celsius"):
db = {
"london": {"temp": 12, "condition": "rain", "humidity": 85},
"paris": {"temp": 18, "condition": "cloudy", "humidity": 60},
"tokyo": {"temp": 24, "condition": "sunny", "humidity": 45},
}
data = db.get(city.lower(), {"temp": 0, "condition": "unknown", "humidity": 0})
if unit == "fahrenheit":
data = {**data, "temp": round(data["temp"] * 9/5 + 32)}
data["city"] = city
return data
# ---- Tool registry ----
TOOLS = {"get_weather": get_weather}
# ---- Mock model: decides which tools to call based on query ----
def mock_model_plan(query, available_tools):
"""Simulate a model deciding which tool calls to make."""
calls = []
query_lower = query.lower()
cities = []
for city in ["london", "paris", "tokyo", "new york"]:
if city in query_lower:
cities.append(city.title())
if cities and "get_weather" in available_tools:
for city in cities:
calls.append({"name": "get_weather", "arguments": {"city": city}})
return calls
def mock_model_respond(query, tool_results):
"""Simulate a model generating a response from tool results."""
if len(tool_results) == 1:
r = tool_results[0]
umbrella = " Bring an umbrella!" if r["condition"] == "rain" else ""
return (
f"It's {r['temp']}C and {r['condition']} in {r['city']}, "
f"with {r['humidity']}% humidity.{umbrella}"
)
else:
parts = []
for r in tool_results:
parts.append(f"{r['city']}: {r['temp']}C, {r['condition']}")
comparison = " vs ".join(parts)
warmest = max(tool_results, key=lambda r: r["temp"])
return f"{comparison}. {warmest['city']} is the warmest."
# ---- The tool-use loop ----
def run_agent(query):
print(f"User: {query}")
print("-" * 50)
# Step 1: Model decides on tool calls
tool_calls = mock_model_plan(query, TOOLS)
if not tool_calls:
print("Model: (no tools needed, respond directly)")
return
# Step 2: Show the calls (parallel if multiple)
parallel = len(tool_calls) > 1
print(f"Model decides to call {len(tool_calls)} tool(s)" +
(" in parallel:" if parallel else ":"))
for call in tool_calls:
print(f" -> {call['name']}({call['arguments']})")
# Step 3: Execute all calls (concurrently in real systems)
results = []
for call in tool_calls:
func = TOOLS[call["name"]]
result = func(**call["arguments"])
results.append(result)
print(f" <- {call['name']} returned: {json.dumps(result)}")
# Step 4: Model generates final response
response = mock_model_respond(query, results)
print(f"
Model: {response}")
print()
# ---- Run both scenarios ----
print("=== Scenario 1: Single tool call ===")
print()
run_agent("What's the weather in London and should I bring an umbrella?")
print("=== Scenario 2: Parallel tool calls ===")
print()
run_agent("Compare the weather in London and Paris")
En sistemas de producción, el modelo simulado se reemplaza por una llamada real a la API de un LLM, y las funciones simuladas se reemplazan por solicitudes HTTP reales, consultas a bases de datos o cualquier otra operación con efectos secundarios. Pero la estructura del bucle es exactamente la misma: consulta → planificación → ejecución → respuesta. Este es el patrón fundamental detrás de todo agente que usa herramientas, desde chatbots simples con una sola herramienta hasta sistemas complejos de múltiples pasos que encadenan docenas de herramientas.
Quiz
Pon a prueba tu comprensión de las llamadas a funciones y el uso de herramientas.
Cuando un modelo 'llama a una función', ¿qué produce realmente?
¿Cuál es el propósito de la decodificación restringida en el modo de salida estructurada?
¿Por qué tener demasiadas herramientas (50+) es problemático para un modelo?
En el enfoque de Toolformer, ¿cómo se seleccionan buenos ejemplos de llamadas a herramientas para el entrenamiento?