Phase 11 - Lesson 09

Function Calling y Uso de Herramientas

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Las LLM no pueden hacer nada por sí mismas. Generan texto. Esa es toda su capacidad. No pueden verificar el clima, consultar una base de datos, enviar un correo electrónico, ejecutar código ni leer un archivo. Cada "agente de IA" que has visto consiste en una LLM que genera un JSON que indica qué función llamar — y luego tu código la llama realmente. El modelo es el cerebro. Las herramientas son las manos. El function calling es el sistema nervioso que los conecta.

Tipo: Build Lenguajes: Python Prerrequisitos: Fase 11 Lección 03 (Structured Outputs) Tiempo: ~75 minutos Relacionado: Fase 11 · 14 (Model Context Protocol) — cuando una herramienta se comparte entre hosts, pasa de una llamada de función inline a un servidor MCP. Esta lección cubre el caso inline; MCP cubre el caso del protocolo.

Objetivos de Aprendizaje

  • Implementar un bucle de function calling: definir esquemas de herramientas, analizar el JSON de llamada a herramientas del modelo, ejecutar funciones y devolver resultados
  • Diseñar esquemas de herramientas con descripciones claras y parámetros tipados que el modelo pueda invocar de manera confiable
  • Construir un bucle de agente de múltiples turnos que encadene varias llamadas a funciones para responder consultas complejas
  • Manejar casos extremos de llamadas a funciones: llamadas paralelas a herramientas, propagación de errores y prevención de bucles infinitos de herramientas

El Problema

Construyes un chatbot. Un usuario pregunta: "¿Cómo está el clima en Tokio en este momento?"

El modelo responde: "No tengo acceso a datos climáticos en tiempo real, pero según la estación, es probable que en Tokio la temperatura ronde los 15 grados Celsius..."

Eso es una alucinación vestida con un descargo de responsabilidad. El modelo no conoce el clima. Nunca lo sabrá. El clima cambia cada hora. Los datos de entrenamiento del modelo tienen meses de antigüedad.

La respuesta correcta requiere llamar a la API de OpenWeatherMap, obtener la temperatura actual y devolver el número real. El modelo no puede llamar a las API. Tu código sí. La pieza que falta: un protocolo estructurado que le permita al modelo decir "Necesito llamar a la API del clima con estos argumentos" y le permita a tu código ejecutarlo y devolver el resultado.

Esto es function calling. El modelo genera un JSON estructurado que describe qué función invocar y con qué argumentos. Tu aplicación ejecuta la función. El resultado vuelve a la conversación. El modelo utiliza el resultado para producir su respuesta final.

Sin function calling, las LLM son enciclopedias. Con él, se convierten en agentes.

El Concepto

El Bucle de Function Calling

Cada interacción de uso de herramientas sigue el mismo bucle de 5 pasos.

sequenceDiagram
    participant U as Usuario
    participant A as Aplicación
    participant M as Modelo
    participant T as Herramienta

    U->>A: "¿Cómo está el clima en Tokio?"
    A->>M: mensajes + definiciones de herramientas
    M->>A: llamada a herramienta: get_weather(city="Tokyo")
    A->>T: Ejecutar get_weather("Tokyo")
    T->>A: {"temp": 18, "condition": "cloudy"}
    A->>M: resultado de herramienta + conversación
    M->>A: "Está a 18°C y nublado en Tokio."
    A->>U: Respuesta final

Paso 1: el usuario envía un mensaje. Paso 2: el modelo recibe el mensaje junto con las definiciones de las herramientas (el JSON Schema que describe las funciones disponibles). Paso 3: en lugar de responder con texto, el modelo genera una llamada a una herramienta — un objeto JSON estructurado con el nombre de la función y los argumentos. Paso 4: tu código ejecuta la función y captura el resultado. Paso 5: el resultado vuelve al modelo, que ahora tiene datos reales para producir su respuesta final.

El modelo nunca ejecuta nada. Solo decide qué llamar y con qué argumentos. Tu código es el ejecutor.

Definiciones de Herramientas: El Contrato de JSON Schema

Cada herramienta se define mediante un JSON Schema que le indica al modelo qué hace la función, qué argumentos acepta y de qué tipo deben ser esos argumentos.

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get current weather for a city. Returns temperature in Celsius and conditions.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "City name, e.g. 'Tokyo' or 'San Francisco'"
        },
        "units": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "Temperature units"
        }
      },
      "required": ["city"]
    }
  }
}

Los campos description son críticos. El modelo los lee para decidir cuándo y cómo usar la herramienta. Una descripción vaga como "gets weather" produce una peor selección de herramientas que "Get current weather for a city. Returns temperature in Celsius and conditions." La descripción es un prompt para la selección de herramientas.

Comparación de Proveedores

Todos los proveedores principales admiten function calling, pero la interfaz de la API difiere.

Proveedor Parámetro de la API Formato de Llamada a Herramienta Llamadas Paralelas Llamada Forzada
OpenAI (GPT-5, o4) tools tool_calls[].function Sí (múltiples por turno) tool_choice="required"
Anthropic (Claude 4.6/4.7) tools content[].type="tool_use" Sí (múltiples bloques) tool_choice={"type":"any"}
Google (Gemini 3) function_declarations functionCall function_calling_config
Open-weight (Llama 4, Qwen3, DeepSeek-V3) tools nativo en Llama 4; Hermes o ChatML en otros Mixto Depende del modelo Basado en prompt o tool_choice si se admite

Para 2026, los tres proveedores de código cerrado han convergido en formatos basados en JSON-Schema casi idénticos. Llama 4 se distribuye con un campo tools nativo que coincide con la estructura de OpenAI. Los ajustes finos (fine-tunes) open-weight todavía varían; el formato Hermes (NousResearch) es el más común para fine-tunes de terceros. Para herramientas compartidas entre hosts, prefiere MCP (Fase 11 · 14) en lugar de llamadas de función inline — el servidor es el mismo para todos ellos.

Selección de Herramientas: Auto, Required, Específica

Tú controlas cuándo el modelo utiliza las herramientas.

Auto (por defecto): el modelo decide si llama a una herramienta o responde directamente. "¿Cuánto es 2+2?" -- responde directamente. "¿Cómo está el clima?" -- llama a la herramienta.

Required: el modelo debe llamar al menos a una herramienta. Utiliza esto cuando sepas que la intención del usuario requiere una herramienta. Evita que el modelo intente adivinar en lugar de buscar datos reales.

Función específica: obliga al modelo a llamar a una función en particular. tool_choice={"type": "function", "function": {"name": "get_weather"}} garantiza que se llame a la herramienta del clima, independientemente de la consulta. Utiliza esto para el enrutamiento, cuando la lógica previa ya ha determinado qué herramienta se necesita.

Llamadas Paralelas a Funciones

GPT-4o y Claude pueden llamar a múltiples funciones en un solo turno. Un usuario pregunta: "¿Cómo está el clima en Tokio y en Nueva York?" El modelo genera dos llamadas a herramientas simultáneamente:

[
  {"name": "get_weather", "arguments": {"city": "Tokyo"}},
  {"name": "get_weather", "arguments": {"city": "New York"}}
]

Tu código ejecuta ambas (idealmente de forma concurrente), devuelve ambos resultados y el modelo sintetiza una sola respuesta. Esto reduce los viajes de ida y vuelta (round trips) de 2 a 1. Para agentes con 5 a 10 llamadas a herramientas por consulta, la llamada paralela reduce la latencia entre un 60% y un 80%.

Structured Outputs vs Function Calling

La Lección 03 cubrió structured outputs (salidas estructuradas). El function calling utiliza el mismo mecanismo de JSON Schema, pero con un propósito diferente.

Structured outputs: obligan al modelo a producir datos con una estructura específica. La salida es el producto final. Ejemplo: extraer información del producto a partir de texto en forma de {name, price, in_stock}.

Function calling: el modelo declara la intención de ejecutar una acción. La salida es un paso intermedio. Ejemplo: get_weather(city="Tokyo") — el modelo solicita una acción, no produce la respuesta final.

Utiliza structured outputs cuando desees extraer datos. Utiliza function calling cuando desees que el modelo interactúe con sistemas externos.

Seguridad: Las Reglas No Negociables

El function calling es la capacidad más peligrosa que le puedes otorgar a una LLM. El modelo elige qué ejecutar. Si tu conjunto de herramientas incluye consultas a bases de datos, el modelo las construye. Si incluye comandos de shell, el modelo los escribe.

Regla 1: Nunca pases SQL generado por el modelo directamente a una base de datos. El modelo puede generar y generará DROP TABLE, inyecciones de UNION o consultas que devuelvan todas las filas. Siempre parametriza. Siempre valida. Siempre utiliza una lista de permitidos (allowlist) de operaciones.

Regla 2: Utiliza una lista de permitidos para las funciones. El modelo solo debe poder llamar a las funciones que definas explícitamente. Nunca construyas una herramienta genérica de tipo "ejecutar cualquier función por nombre". Si tienes 50 funciones internas, expón únicamente las 5 que el usuario necesita.

Regla 3: Valida los argumentos. El modelo podría pasar un nombre de ciudad como "; DROP TABLE users; --". Valide cada argumento frente a los tipos, rangos y formatos esperados antes de la ejecución.

Regla 4: Sanitiza los resultados de las herramientas. Si una herramienta devuelve datos sensibles (claves API, información de identificación personal - PII, errores internos), fíltralos antes de enviarlos de vuelta al modelo. El modelo incluirá los resultados de la herramienta en su respuesta de forma literal.

Regla 5: Limita la tasa de llamadas a herramientas. Un modelo en un bucle puede llamar a las herramientas cientos de veces. Establece un máximo (de 10 a 20 llamadas por conversación es razonable). Rompe los bucles infinitos.

Manejo de Errores

Las herramientas fallan. Las API se agotan por timeout. Las bases de datos se caen. Los archivos no existen. El modelo necesita saber cuándo falla una herramienta y por qué.

Devuelve los errores como resultados estructurados de la herramienta, no como excepciones:

{
  "error": true,
  "message": "City 'Toky' not found. Did you mean 'Tokyo'?",
  "code": "CITY_NOT_FOUND"
}

El modelo lee esto, ajusta sus argumentos y vuelve a intentarlo. Los modelos son buenos auto-corrigiéndose a partir de mensajes de error estructurados. Son malos recuperándose de respuestas vacías o errores genéricos de "algo salió mal".

MCP: Model Context Protocol

MCP es el estándar abierto de Anthropic para la interoperabilidad de herramientas. En lugar de que cada aplicación defina sus propias herramientas, MCP proporciona un protocolo universal: las herramientas son servidas por servidores MCP y consumidas por clientes MCP (como Claude Code, Cursor o tu aplicación).

Un solo servidor MCP puede exponer herramientas a cualquier cliente compatible. Un servidor MCP de Postgres le da acceso a la base de datos a cualquier agente compatible con MCP. Un servidor MCP de GitHub le da acceso a repositorios a cualquier agente. Las herramientas se definen una vez y se usan en cualquier parte.

MCP es para el function calling lo que HTTP es para las redes. Estandariza la capa de transporte para que las herramientas sean portables.

Build It

Paso 1: Definir el Registro de Herramientas

Construye un registro que almacene las definiciones de las herramientas y sus implementaciones. Cada herramienta tiene una definición de JSON Schema (lo que ve el modelo) y una función de Python (lo que ejecuta tu código).

import json
import math
import time
import hashlib


TOOL_REGISTRY = {}


def register_tool(name, description, parameters, function):
    TOOL_REGISTRY[name] = {
        "definition": {
            "type": "function",
            "function": {
                "name": name,
                "description": description,
                "parameters": parameters,
            },
        },
        "function": function,
    }

Paso 2: Implementar 5 Herramientas

Construye una calculadora, búsqueda de clima, simulador de búsqueda web, lector de archivos y ejecutor de código.

def calculator(expression, precision=2):
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return {"error": True, "message": f"Invalid characters in expression: {expression}"}
    try:
        result = eval(expression, {"__builtins__": {}}, {"math": math})
        return {"result": round(float(result), precision), "expression": expression}
    except Exception as e:
        return {"error": True, "message": str(e)}


WEATHER_DB = {
    "tokyo": {"temp_c": 18, "condition": "cloudy", "humidity": 72, "wind_kph": 14},
    "new york": {"temp_c": 22, "condition": "sunny", "humidity": 45, "wind_kph": 8},
    "london": {"temp_c": 12, "condition": "rainy", "humidity": 88, "wind_kph": 22},
    "san francisco": {"temp_c": 16, "condition": "foggy", "humidity": 80, "wind_kph": 18},
    "sydney": {"temp_c": 25, "condition": "sunny", "humidity": 55, "wind_kph": 10},
}


def get_weather(city, units="celsius"):
    key = city.lower().strip()
    if key not in WEATHER_DB:
        suggestions = [c for c in WEATHER_DB if c.startswith(key[:3])]
        return {
            "error": True,
            "message": f"City '{city}' not found.",
            "suggestions": suggestions,
            "code": "CITY_NOT_FOUND",
        }
    data = WEATHER_DB[key].copy()
    if units == "fahrenheit":
        data["temp_f"] = round(data["temp_c"] * 9 / 5 + 32, 1)
        del data["temp_c"]
    data["city"] = city
    return data


SEARCH_DB = {
    "python function calling": [
        {"title": "OpenAI Function Calling Guide", "url": "https://platform.openai.com/docs/guides/function-calling", "snippet": "Learn how to connect LLMs to external tools."},
        {"title": "Anthropic Tool Use", "url": "https://docs.anthropic.com/en/docs/tool-use", "snippet": "Claude can interact with external tools and APIs."},
    ],
    "MCP protocol": [
        {"title": "Model Context Protocol", "url": "https://modelcontextprotocol.io", "snippet": "An open standard for connecting AI models to data sources."},
    ],
    "weather API": [
        {"title": "OpenWeatherMap API", "url": "https://openweathermap.org/api", "snippet": "Free weather API with current, forecast, and historical data."},
    ],
}


def web_search(query, max_results=3):
    key = query.lower().strip()
    for db_key, results in SEARCH_DB.items():
        if db_key in key or key in db_key:
            return {"query": query, "results": results[:max_results], "total": len(results)}
    return {"query": query, "results": [], "total": 0}


FILE_SYSTEM = {
    "data/config.json": '{"model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096}',
    "data/users.csv": "name,email,role\nAlice,alice@example.com,admin\nBob,bob@example.com,user",
    "README.md": "# My Project\nA tool-use agent built from scratch.",
}


def read_file(path):
    if ".." in path or path.startswith("/"):
        return {"error": True, "message": "Path traversal not allowed.", "code": "FORBIDDEN"}
    if path not in FILE_SYSTEM:
        available = list(FILE_SYSTEM.keys())
        return {"error": True, "message": f"File '{path}' not found.", "available_files": available, "code": "NOT_FOUND"}
    content = FILE_SYSTEM[path]
    return {"path": path, "content": content, "size_bytes": len(content), "lines": content.count("\n") + 1}


def run_code(code, language="python"):
    if language != "python":
        return {"error": True, "message": f"Language '{language}' not supported. Only 'python' is available."}
    forbidden = ["import os", "import sys", "import subprocess", "exec(", "eval(", "__import__", "open("]
    for pattern in forbidden:
        if pattern in code:
            return {"error": True, "message": f"Forbidden operation: {pattern}", "code": "SECURITY_VIOLATION"}
    try:
        local_vars = {}
        exec(code, {"__builtins__": {"print": print, "range": range, "len": len, "str": str, "int": int, "float": float, "list": list, "dict": dict, "sum": sum, "min": min, "max": max, "abs": abs, "round": round, "sorted": sorted, "enumerate": enumerate, "zip": zip, "map": map, "filter": filter, "math": math}}, local_vars)
        result = local_vars.get("result", None)
        return {"success": True, "result": result, "variables": {k: str(v) for k, v in local_vars.items() if not k.startswith("_")}}
    except Exception as e:
        return {"error": True, "message": f"{type(e).__name__}: {e}"}

Paso 3: Registrar Todas las Herramientas

def register_all_tools():
    register_tool(
        "calculator", "Evaluate a mathematical expression. Supports +, -, *, /, parentheses, and decimals. Returns the numeric result.",
        {"type": "object", "properties": {"expression": {"type": "string", "description": "Math expression, e.g. '(10 + 5) * 3'"}, "precision": {"type": "integer", "description": "Decimal places in result", "default": 2}}, "required": ["expression"]},
        calculator,
    )
    register_tool(
        "get_weather", "Get current weather for a city. Returns temperature, condition, humidity, and wind speed.",
        {"type": "object", "properties": {"city": {"type": "string", "description": "City name, e.g. 'Tokyo' or 'San Francisco'"}, "units": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units, defaults to celsius"}}, "required": ["city"]},
        get_weather,
    )
    register_tool(
        "web_search", "Search the web for information. Returns a list of results with title, URL, and snippet.",
        {"type": "object", "properties": {"query": {"type": "string", "description": "Search query"}, "max_results": {"type": "integer", "description": "Maximum results to return", "default": 3}}, "required": ["query"]},
        web_search,
    )
    register_tool(
        "read_file", "Read the contents of a file. Returns the file content, size, and line count.",
        {"type": "object", "properties": {"path": {"type": "string", "description": "Relative file path, e.g. 'data/config.json'"}}, "required": ["path"]},
        read_file,
    )
    register_tool(
        "run_code", "Execute Python code in a sandboxed environment. Set a 'result' variable to return output.",
        {"type": "object", "properties": {"code": {"type": "string", "description": "Python code to execute"}, "language": {"type": "string", "enum": ["python"], "description": "Programming language"}}, "required": ["code"]},
        run_code,
    )

Paso 4: Construir el Bucle de Function Calling

Este es el motor central. Simula al modelo decidiendo qué herramienta llamar, ejecuta la herramienta y devuelve los resultados.

def simulate_model_decision(user_message, tools, conversation_history):
    msg = user_message.lower()

    if any(word in msg for word in ["weather", "temperature", "forecast"]):
        cities = []
        for city in WEATHER_DB:
            if city in msg:
                cities.append(city)
        if not cities:
            for word in msg.split():
                if word.capitalize() in [c.title() for c in WEATHER_DB]:
                    cities.append(word)
        if not cities:
            cities = ["tokyo"]
        calls = []
        for city in cities:
            calls.append({"name": "get_weather", "arguments": {"city": city.title()}})
        return calls

    if any(word in msg for word in ["calculate", "compute", "math", "what is", "how much"]):
        for token in msg.split():
            if any(c in token for c in "+-*/"):
                return [{"name": "calculator", "arguments": {"expression": token}}]
        if "+" in msg or "-" in msg or "*" in msg or "/" in msg:
            expr = "".join(c for c in msg if c in "0123456789+-*/.() ")
            if expr.strip():
                return [{"name": "calculator", "arguments": {"expression": expr.strip()}}]
        return [{"name": "calculator", "arguments": {"expression": "0"}}]

    if any(word in msg for word in ["search", "find", "look up", "google"]):
        query = msg.replace("search for", "").replace("look up", "").replace("find", "").strip()
        return [{"name": "web_search", "arguments": {"query": query}}]

    if any(word in msg for word in ["read", "file", "open", "cat", "show"]):
        for path in FILE_SYSTEM:
            if path.split("/")[-1].split(".")[0] in msg:
                return [{"name": "read_file", "arguments": {"path": path}}]
        return [{"name": "read_file", "arguments": {"path": "README.md"}}]

    if any(word in msg for word in ["run", "execute", "code", "python"]):
        return [{"name": "run_code", "arguments": {"code": "result = 'Hello from the sandbox!'", "language": "python"}}]

    return []


def execute_tool_call(tool_call):
    name = tool_call["name"]
    args = tool_call["arguments"]

    if name not in TOOL_REGISTRY:
        return {"error": True, "message": f"Unknown tool: {name}", "code": "UNKNOWN_TOOL"}

    tool = TOOL_REGISTRY[name]
    func = tool["function"]
    start = time.time()

    try:
        result = func(**args)
    except TypeError as e:
        result = {"error": True, "message": f"Invalid arguments: {e}"}

    elapsed_ms = round((time.time() - start) * 1000, 2)
    return {"tool": name, "result": result, "execution_time_ms": elapsed_ms}


def run_function_calling_loop(user_message, max_iterations=5):
    conversation = [{"role": "user", "content": user_message}]
    tool_definitions = [t["definition"] for t in TOOL_REGISTRY.values()]
    all_tool_results = []

    for iteration in range(max_iterations):
        tool_calls = simulate_model_decision(user_message, tool_definitions, conversation)

        if not tool_calls:
            break

        results = []
        for call in tool_calls:
            result = execute_tool_call(call)
            results.append(result)

        conversation.append({"role": "assistant", "content": None, "tool_calls": tool_calls})

        for result in results:
            conversation.append({"role": "tool", "content": json.dumps(result["result"]), "tool_name": result["tool"]})

        all_tool_results.extend(results)
        break

    return {"conversation": conversation, "tool_results": all_tool_results, "iterations": iteration + 1 if tool_calls else 0}

Paso 5: Validación de Argumentos

Construye un validador que verifique los argumentos de la llamada a la herramienta frente al JSON Schema antes de la ejecución.

def validate_tool_arguments(tool_name, arguments):
    if tool_name not in TOOL_REGISTRY:
        return [f"Unknown tool: {tool_name}"]

    schema = TOOL_REGISTRY[tool_name]["definition"]["function"]["parameters"]
    errors = []

    if not isinstance(arguments, dict):
        return [f"Arguments must be an object, got {type(arguments).__name__}"]

    for required_field in schema.get("required", []):
        if required_field not in arguments:
            errors.append(f"Missing required argument: {required_field}")

    properties = schema.get("properties", {})
    for arg_name, arg_value in arguments.items():
        if arg_name not in properties:
            errors.append(f"Unknown argument: {arg_name}")
            continue

        prop_schema = properties[arg_name]
        expected_type = prop_schema.get("type")

        type_checks = {"string": str, "integer": int, "number": (int, float), "boolean": bool, "array": list, "object": dict}
        if expected_type in type_checks:
            if not isinstance(arg_value, type_checks[expected_type]):
                errors.append(f"Argument '{arg_name}': expected {expected_type}, got {type(arg_value).__name__}")

        if "enum" in prop_schema and arg_value not in prop_schema["enum"]:
            errors.append(f"Argument '{arg_name}': '{arg_value}' not in {prop_schema['enum']}")

    return errors

Paso 6: Ejecutar la Demostración

def run_demo():
    register_all_tools()

    print("=" * 60)
    print("  Function Calling & Tool Use Demo")
    print("=" * 60)

    print("\n--- Registered Tools ---")
    for name, tool in TOOL_REGISTRY.items():
        desc = tool["definition"]["function"]["description"][:60]
        params = list(tool["definition"]["function"]["parameters"].get("properties", {}).keys())
        print(f"  {name}: {desc}...")
        print(f"    params: {params}")

    print(f"\n--- Argument Validation ---")
    validation_tests = [
        ("get_weather", {"city": "Tokyo"}, "Valid call"),
        ("get_weather", {}, "Missing required arg"),
        ("get_weather", {"city": "Tokyo", "units": "kelvin"}, "Invalid enum value"),
        ("calculator", {"expression": 123}, "Wrong type (int for string)"),
        ("unknown_tool", {"x": 1}, "Unknown tool"),
    ]
    for tool_name, args, label in validation_tests:
        errors = validate_tool_arguments(tool_name, args)
        status = "VALID" if not errors else f"ERRORS: {errors}"
        print(f"  {label}: {status}")

    print(f"\n--- Tool Execution ---")
    direct_tests = [
        {"name": "calculator", "arguments": {"expression": "(10 + 5) * 3 / 2"}},
        {"name": "get_weather", "arguments": {"city": "Tokyo"}},
        {"name": "get_weather", "arguments": {"city": "Mars"}},
        {"name": "web_search", "arguments": {"query": "python function calling"}},
        {"name": "read_file", "arguments": {"path": "data/config.json"}},
        {"name": "read_file", "arguments": {"path": "../etc/passwd"}},
        {"name": "run_code", "arguments": {"code": "result = sum(range(1, 101))"}},
        {"name": "run_code", "arguments": {"code": "import os; os.system('rm -rf /')"}},
    ]
    for call in direct_tests:
        result = execute_tool_call(call)
        print(f"\n  {call['name']}({json.dumps(call['arguments'])})")
        print(f"    -> {json.dumps(result['result'], indent=None)[:100]}")
        print(f"    time: {result['execution_time_ms']}ms")

    print(f"\n--- Full Function Calling Loop ---")
    test_queries = [
        "What's the weather in Tokyo?",
        "Calculate (100 + 250) * 0.15",
        "Search for MCP protocol",
        "Read the config file",
        "Run some Python code",
        "Tell me a joke",
    ]
    for query in test_queries:
        print(f"\n  User: {query}")
        result = run_function_calling_loop(query)
        if result["tool_results"]:
            for tr in result["tool_results"]:
                print(f"    Tool: {tr['tool']} ({tr['execution_time_ms']}ms)")
                print(f"    Result: {json.dumps(tr['result'], indent=None)[:90]}")
        else:
            print(f"    [No tool called -- direct response]")
        print(f"    Iterations: {result['iterations']}")

    print(f"\n--- Parallel Tool Calls ---")
    multi_city_query = "What's the weather in tokyo and london?"
    print(f"  User: {multi_city_query}")
    result = run_function_calling_loop(multi_city_query)
    print(f"  Tool calls made: {len(result['tool_results'])}")
    for tr in result["tool_results"]:
        city = tr["result"].get("city", "unknown")
        temp = tr["result"].get("temp_c", "N/A")
        print(f"    {city}: {temp}C, {tr['result'].get('condition', 'N/A')}")

    print(f"\n--- Security Checks ---")
    security_tests = [
        ("read_file", {"path": "../../etc/passwd"}),
        ("run_code", {"code": "import subprocess; subprocess.run(['ls'])"}),
        ("calculator", {"expression": "__import__('os').system('ls')"}),
    ]
    for tool_name, args in security_tests:
        result = execute_tool_call({"name": tool_name, "arguments": args})
        blocked = result["result"].get("error", False)
        print(f"  {tool_name}({list(args.values())[0][:40]}): {'BLOCKED' if blocked else 'ALLOWED'}")

Use It

OpenAI Function Calling

# from openai import OpenAI
#
# client = OpenAI()
#
# tools = [{
#     "type": "function",
#     "function": {
#         "name": "get_weather",
#         "description": "Get current weather for a city",
#         "parameters": {
#             "type": "object",
#             "properties": {
#                 "city": {"type": "string"},
#                 "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
#             },
#             "required": ["city"]
#         }
#     }
# }]
#
# response = client.chat.completions.create(
#     model="gpt-4o",
#     messages=[{"role": "user", "content": "Weather in Tokyo?"}],
#     tools=tools,
#     tool_choice="auto",
# )
#
# tool_call = response.choices[0].message.tool_calls[0]
# args = json.loads(tool_call.function.arguments)
# result = get_weather(**args)
#
# final = client.chat.completions.create(
#     model="gpt-4o",
#     messages=[
#         {"role": "user", "content": "Weather in Tokyo?"},
#         response.choices[0].message,
#         {"role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result)},
#     ],
# )
# print(final.choices[0].message.content)

OpenAI devuelve las llamadas a herramientas como response.choices[0].message.tool_calls. Cada llamada tiene un id que debes incluir al devolver el resultado. El modelo utiliza este ID para asociar los resultados con las llamadas. GPT-4o puede devolver múltiples llamadas a herramientas en una sola respuesta — itera y ejecuta cada una de ellas.

Anthropic Tool Use

# import anthropic
#
# client = anthropic.Anthropic()
#
# response = client.messages.create(
#     model="claude-sonnet-4-20250514",
#     max_tokens=1024,
#     tools=[{
#         "name": "get_weather",
#         "description": "Get current weather for a city",
#         "input_schema": {
#             "type": "object",
#             "properties": {
#                 "city": {"type": "string"},
#                 "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
#             },
#             "required": ["city"]
#         }
#     }],
#     messages=[{"role": "user", "content": "Weather in Tokyo?"}],
# )
#
# tool_block = next(b for b in response.content if b.type == "tool_use")
# result = get_weather(**tool_block.input)
#
# final = client.messages.create(
#     model="claude-sonnet-4-20250514",
#     max_tokens=1024,
#     tools=[...],
#     messages=[
#         {"role": "user", "content": "Weather in Tokyo?"},
#         {"role": "assistant", "content": response.content},
#         {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_block.id, "content": json.dumps(result)}]},
#     ],
# )

Anthropic devuelve las llamadas a herramientas como bloques de contenido con type: "tool_use". El resultado de la herramienta se envía en un mensaje del usuario con type: "tool_result". Ten en cuenta la diferencia clave: Anthropic utiliza input_schema para las definiciones de parámetros de herramientas, mientras que OpenAI utiliza parameters.

Integración con MCP

# MCP servers expose tools over a standardized protocol.
# Any MCP-compatible client can discover and call these tools.
#
# Example: connecting to a Postgres MCP server
#
# from mcp import ClientSession, StdioServerParameters
# from mcp.client.stdio import stdio_client
#
# server_params = StdioServerParameters(
#     command="npx",
#     args=["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
# )
#
# async with stdio_client(server_params) as (read, write):
#     async with ClientSession(read, write) as session:
#         await session.initialize()
#         tools = await session.list_tools()
#         result = await session.call_tool("query", {"sql": "SELECT count(*) FROM users"})

MCP desacopla la implementación de la herramienta de su consumo. El servidor Postgres conoce SQL. El servidor GitHub conoce la API. Tu agente solo descubre y llama a las herramientas — no necesita código específico del proveedor para cada integración.

Ship It

Esta lección produce outputs/prompt-tool-designer.md — una plantilla de prompt reutilizable para diseñar definiciones de herramientas. Proporciónale una descripción de lo que deseas que haga una herramienta y producirá la definición completa de JSON Schema con descripciones, tipos y restricciones.

También produce outputs/skill-function-calling-patterns.md — un marco de decisión para implementar llamadas a funciones en producción, que cubre el diseño de herramientas, el manejo de errores, la seguridad y los patrones específicos de los proveedores.

Exercises

  1. Agrega una 6ª herramienta: consulta a base de datos. Implementa una herramienta SQL simulada con una tabla en memoria. La herramienta acepta un nombre de tabla y condiciones de filtrado (no SQL sin procesar). Valida que el nombre de la tabla esté en una lista de permitidos (allowlist) y que los operadores de filtrado estén restringidos a =, >, <, >=, <=. Devuelve las filas coincidentes como JSON.

  2. Implementa reintentos con feedback de errores. Cuando una llamada a una herramienta falle (por ejemplo, ciudad no encontrada), envía el mensaje de error de vuelta a la función de decisión del modelo y permite que corrija sus argumentos. Registra cuántos reintentos toma cada llamada. Establece un máximo de 3 reintentos por llamada a la herramienta.

  3. Construye un agente de múltiples pasos. Algunas consultas requieren encadenar llamadas a herramientas: "Lee el archivo de configuración y dime qué modelo está configurado, luego busca en la web el precio de ese modelo". Implementa un bucle que se ejecute hasta que el modelo decida que no se necesitan más herramientas, pasando los resultados acumulados a cada paso de decisión. Limita a 10 iteraciones para evitar bucles infinitos.

  4. Mide la precisión en la selección de herramientas. Crea 30 consultas de prueba con los nombres de herramientas esperados. Ejecuta tu función de decisión en las 30 consultas y mide en qué porcentaje de ocasiones selecciona la herramienta correcta. Identifica qué consultas causan la mayor confusión entre las herramientas.

  5. Implementa almacenamiento en caché para llamadas a herramientas. Si se llama a la misma herramienta con argumentos idénticos dentro de un lapso de 60 segundos, devuelve el resultado almacenado en caché en lugar de volver a ejecutarla. Utiliza un diccionario indexado por (tool_name, frozenset(args.items())). Mide la tasa de aciertos de la caché en una conversación con 20 consultas.

Key Terms

Término Lo que la gente dice Lo que realmente significa
Function calling "Uso de herramientas" (Tool use) El modelo produce un JSON estructurado que describe una función a invocar con argumentos específicos — tu código la ejecuta, no el modelo
Definición de herramienta "Esquema de función" Un objeto JSON Schema que describe el nombre, propósito, parámetros y tipos de una herramienta — el modelo lo lee para decidir cuándo y cómo usar la herramienta
Selección de herramienta "Modo de llamada" Controla si el modelo debe llamar a una herramienta (required), puede llamar a una herramienta (auto) o debe llamar a una herramienta específica (named)
Llamada paralela "Multi-herramienta" El modelo devuelve múltiples llamadas a herramientas en un solo turno, reduciendo los viajes de ida y vuelta — tanto GPT-4o como Claude lo admiten
Resultado de herramienta "Salida de función" El valor de retorno de ejecutar una herramienta, enviado de vuelta al modelo como un mensaje para que pueda usar datos reales en su respuesta
Validación de argumentos "Comprobación de entrada" Verificar que los argumentos generados por el modelo coincidan con los tipos, rangos y restricciones esperados antes de ejecutar la herramienta
MCP "Protocolo de herramientas" Model Context Protocol — el estándar abierto de Anthropic para exponer herramientas a través de servidores que cualquier cliente compatible puede descubrir y llamar
Bucle de agente "Bucle ReAct" El ciclo iterativo en el que el modelo decide la herramienta, el código ejecuta la herramienta y el resultado alimenta al modelo hasta que tenga suficiente información para responder
Envenenamiento de herramienta "Inyección de prompts vía herramientas" Un ataque donde los resultados de una herramienta contienen instrucciones que manipulan el comportamiento del modelo — sanitiza todas las salidas de las herramientas
Límite de tasa "Presupuesto de llamadas" Establecer un número máximo de llamadas a herramientas por conversación para evitar bucles infinitos y costos excesivos de API

Further Reading

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).