Phase 11: LLM Engineering

Llamadas de funciones y uso de herramientas

Los LLM no pueden hacer nada. Generan mensajes de texto. Esa es toda la capacidad. No pueden comprobar el clima, consultar una base de datos, enviar un correo electrónico, ejecutar un código o leer un archivo. Cada "agente de IA" que hayas visto es un LLM generando JSON que dice a qué función llamar y luego tu código realmente lo llama. El modelo es el cerebro. Las herramientas son las manos. La llamada de la función es el sistema nervioso que las conecta.

Type: Build

Languages: Python

Prerequisites: Phase 11 Lesson 03 (Structured Outputs)

Time: ~75 minutes

Related:Fase 11 · 14 (Modelo de Protocolo de Contexto) cuando una herramienta se comparte entre hosts, se graduan de la llamada de funciones en línea a un servidor MCP. Esta lección cubre el caso en línea; MCP cubre el caso de protocolo.

Objetivos de aprendizaje

  • Implementar un bucle de llamadas de funciones: definir esquemas de herramientas, analizar el JSON de llamadas de herramientas del modelo, ejecutar funciones y devolver resultados
  • Esquemas de herramientas de diseño con descripciones claras y parámetros tipografados que el modelo pueda invocar confiablemente
  • Construir un bucle de agente de múltiples vueltas que enlace múltiples llamadas de función para responder consultas complejas
  • Función de manejo que llama casos de borde: llamadas paralelas a herramientas, propagación de errores y prevención de bucles de herramientas infinitos

El problema

Construye un chatbot y un usuario pregunta: "¿Cuál es el tiempo en Tokio ahora?"

El modelo responde: "No tengo acceso a datos meteorológicos en tiempo real, pero según la estación, es probable que Tokio esté alrededor de 15 grados centígrados... "

Es una alucinación vestida de una declaración de responsabilidad. El modelo no sabe el tiempo. Nunca lo hará. El tiempo cambia cada hora. Los datos de entrenamiento del modelo son de meses.

La respuesta correcta requiere llamar a la API OpenWeatherMap, obtener la temperatura actual y devolver el número real. El modelo no puede llamar a las API. Su código puede. La pieza que falta: un protocolo estructurado que permite al modelo decir "Necesito llamar a la API meteorológica con estos argumentos" y permite que su código la ejecute y devuelva el resultado.

El modelo de llamadas de función. El modelo de salida estructurado JSON que describe qué función para invocar con qué argumentos. Su aplicación ejecuta la función. El resultado vuelve a la conversación. El modelo utiliza el resultado para producir su respuesta final.

Sin la llamada de la función, LLM son enciclopedias.

El concepto

La función que llama el bucle

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

sequenceDiagram
    participant U as User
    participant A as Application
    participant M as Model
    participant T as Tool

    U->>A: "What's the weather in Tokyo?"
    A->>M: messages + tool definitions
    M->>A: tool_call: get_weather(city="Tokyo")
    A->>T: Execute get_weather("Tokyo")
    T->>A: {"temp": 18, "condition": "cloudy"}
    A->>M: tool_result + conversation
    M->>A: "It's 18C and cloudy in Tokyo."
    A->>U: Final response

Paso 1: el usuario envía un mensaje. Paso 2: el modelo recibe el mensaje junto con las definiciones de herramientas (Esquema JSON que describe las funciones disponibles). Paso 3: en lugar de responder con texto, el modelo emite una llamada de herramienta, un objeto JSON estructurado con el nombre de la función y los argumentos. Paso 4: su 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, sólo decide qué llamar y con qué argumentos.

Definiciones de herramientas: el contrato de esquema JSON

Cada herramienta está definida por un esquema JSON que le dice al modelo lo que hace la función, qué argumentos necesita y qué tipos deben ser esos argumentos.

json{
  "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"]
    }
  }
}

El descriptionEl modelo los lee para decidir cuándo y cómo usar la herramienta. Una descripción vaga como "tiene tiempo" produce una selección de herramientas peor que "Obtenga el tiempo actual para una ciudad. Retorna la temperatura en Celsius y condiciones".

Comparación de proveedores

Cada proveedor principal admite llamadas de funciones, pero la superficie de la API es diferente.

ProviderAPI ParameterTool Call FormatParallel CallsForced Calling
OpenAI (GPT-5, o4)toolstool_calls[].functionYes (multiple per turn)tool_choice="required"
Anthropic (Claude 4.6/4.7)toolscontent[].type="tool_use"Yes (multiple blocks)tool_choice={"type":"any"}
Google (Gemini 3)function_declarationsfunctionCallYesfunction_calling_config
Open-weight (Llama 4, Qwen3, DeepSeek-V3)Native tools on Llama 4; Hermes or ChatML on othersMixedModel-dependentPrompt-based or tool_choice if supported

Para 2026, los tres proveedores cerrados se han convergido en formatos basados en JSON-Schema casi idénticos.toolsEl formato Hermes (NousResearch) es el más común para los temas finos de terceros. Para las herramientas compartidas entre hosts, prefiere MCP (Fase 11 · 14) sobre la llamada de funciones en línea el servidor es el mismo para todos ellos.

Elegir las herramientas: automáticas, requeridas, específicas

Tú controlas cuando el modelo usa herramientas.

Auto(por defecto): el modelo decide si llama a una herramienta o responde directamente. "¿Qué es 2 + 2?" - responde directamente. "Cuál es el tiempo?" - llama a la herramienta.

RequiredEl modelo debe llamar al menos a una herramienta. Utilice esto cuando sepa que la intención del usuario requiere una herramienta.

Specific function: obligar al modelo a llamar a una función particular. tool_choice={"type":"function", "function": {"name": "get_weather"}}Se puede usar para el enrutamiento cuando la lógica de aguas arriba ya determina qué herramienta es necesaria.

Llamadas para funciones paralelas

GPT-4o y Claude pueden llamar a múltiples funciones en un solo giro. Un usuario pregunta: "¿Cuál es el tiempo en Tokio y Nueva York?" El modelo emite dos llamadas de herramienta simultáneamente:

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

Su código ejecuta ambos (idealmente simultáneamente), devuelve ambos resultados y el modelo sintetiza una sola respuesta. Esto reduce las viajes de ida y vuelta de 2 a 1. Para los agentes con 5-10 llamadas de herramientas por consulta, las llamadas paralelas reducen la latencia en 60-80%.

Las salidas estructuradas vs. llamadas de función

La lección 03 abarcaba las salidas estructuradas.

Structured outputsEl resultado es el producto final. Ejemplo: extraer información del producto de un texto como {name, price, in_stock}¿ Qué ?

Function callingEl modelo declara la intención de ejecutar una acción.get_weather(city="Tokyo")-- el modelo está solicitando una acción, no produciendo la respuesta final.

Utilice salidas estructuradas cuando desee extraer datos. Utilice llamadas de funciones cuando desee que el modelo interactúe con sistemas externos.

Seguridad: las reglas no negociables

La llamada de funciones es la capacidad más peligrosa que puedes dar a un LLM. El modelo elige qué ejecutar. Si tu conjunto de herramientas incluye consultas de base de datos, el modelo construye las consultas. Si incluye comandos shell, el modelo las escribe.

Rule 1: Never pass model-generated SQL directly to a database.El modelo puede y generará DROP TABLE, inyecciones UNION o consultas que devuelven cada fila. Siempre parametrizar. Siempre validar. Siempre utilizar una lista de permitidos de operaciones.

Rule 2: Allowlist functions.El modelo sólo puede llamar a las funciones que usted define explícitamente. Nunca construya una herramienta genérica "execute any function by name". Si tiene 50 funciones internas, exponga solo las 5 que el usuario necesita.

Rule 3: Validate arguments.El modelo podría pasar por un nombre de ciudad de "; DROP TABLE users; --"Validar todos los argumentos contra los tipos, rangos y formatos esperados antes de la ejecución.

Rule 4: Sanitize tool results.Si una herramienta devuelve datos sensibles (claves API, PII, errores internos), filtrelos antes de enviarlos de nuevo al modelo.

Rule 5: Rate limit tool calls.Un modelo en un bucle puede llamar a las herramientas cientos de veces. Establezca un máximo (10-20 llamadas por conversación es razonable). Rompa bucles infinitos.

Tratamiento de errores

Las herramientas fallan, las API se detienen, las bases de datos se desactivan, los archivos no existen, el modelo necesita saber cuándo falla una herramienta y por qué.

Regresar errores como resultados de herramientas estructuradas, no excepciones:

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

Los modelos son buenos en autocorregirse de mensajes de error estructurados. Son mal en recuperarse de respuestas vacías o errores genéricos de "algo salió mal".

MCP: Modelo de protocolo de contexto

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, consumidas por clientes MCP (como Claude Code, Cursor o su aplicación).

Un servidor MCP puede exponer herramientas a cualquier cliente compatible. Un servidor MCP Postgres da acceso a cualquier base de datos de agentes compatible con MCP. Un servidor MCP GitHub da acceso al repositorio de cualquier agente. Las herramientas se definen una vez, se utilizan en todas partes.

MCP es para llamar a la función que HTTP es a la red. Estandariza la capa de transporte para que las herramientas se vuelvan portátiles.

Construye el mismo

Paso 1: Definir el registro de herramientas

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

pythonimport ast
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 del tiempo, simulador de búsqueda en la web, lector de archivos y ejecutor de código.

pythondef 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": "TOK0
}


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."}
    try:
        tree = ast.parse(code)
    except SyntaxError as e:
        return {"error": True, "message": f"SyntaxError: {e}", "code": "SYNTAX_ERROR"}
    unsafe_names = {"exec", "eval", "compile", "__import__", "open", "globals", "locals", "vars", "getattr", "setattr", "delattr"}
    for node in ast.walk(tree):
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            return {"error": True, "message": "Forbidden operation: import is not allowed", "code": "SECURITY_VIOLATION"}
        if isinstance(node, ast.Attribute) and node.attr.startswith("__") and node.attr.endswith("__"):
            return {"error": True, "message": "Forbidden operation: dunder attribute access is not allowed", "code": "SECURITY_VIOLATION"}
        if isinstance(node, ast.Name) and node.id in unsafe_names:
            return {"error": True, "message": f"Forbidden operation: {node.id} is not allowed", "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}"}

Una lista de bloqueo de substring lee el código como texto, por lo que pierde cualquier cosa que la coincidencia de cadenas no escriba literalmente.importLos datos de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información de la información.__class__y __globals__Las cadenas que se remontan al intérprete real) y los nombres construidos no seguros por estructura en lugar de por ortografía. Aun así, trate esto como un filtro de enseñanza, no como un límite real. Cualquier guardia en proceso comparte el intérprete con el código que ejecuta, y un llamador determinado todavía puede encontrar objetos accesibles. Los sistemas de producción ejecutan código no confiable en un proceso o contenedor separado (un subproceso con privilegios retirados, gVisor, Firecracker o un ejecutor de código alojado), donde un escape aterriza al atacante en una caja de desecho en lugar de su servicio.

Paso 3: Registre todas las herramientas

pythondef 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", "Run a small Python snippet behind a static-analysis guard and a restricted interpreter. This is a teaching filter, not real isolation. 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: Construye la función llamada bucle

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

pythondef 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 los argumentos

Construir un validador que compruebe los argumentos de llamada de herramienta contra el esquema JSON antes de la ejecución.

pythondef 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

pythondef 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'}")

Usalo

Llamada de la función OpenAI

python# 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 de la herramienta como response.choices[0].message.tool_callsCada llamada tiene un número .idel modelo utiliza esta identificación para hacer coincidir los resultados con las llamadas. GPT-4o puede devolver múltiples llamadas de herramientas en una sola respuesta - iteración y ejecución de todas ellas.

El uso de herramientas antropológicas

python# import anthropic
#
# client = anthropic.Anthropic()
#
# response = client.messages.create(
#     model="claude-sonnet-5",
#     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-5",
#     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)}]},
#     ],
# )

Antropic devuelve las llamadas de la herramienta como bloques de contenido con type: "tool_use". El resultado de la herramienta entra en un mensaje del usuario con type: "tool_result". Observe la diferencia clave: usos antropológicos input_schemapara las definiciones de parámetros de herramientas, mientras que OpenAI utiliza parameters¿ Qué ?

Integración de los PMP

python# 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 herramientas del consumo de herramientas. El servidor Postgres conoce SQL. El servidor GitHub conoce la API. Su agente solo descubre y llama a las herramientas - no necesita código específico de proveedor para cada integración.

Envío

Esta lección produceoutputs/prompt-tool-designer.md-- una plantilla de solicitud reutilizable para diseñar definiciones de herramientas. Darle una descripción de lo que desea que haga una herramienta, y produce 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 la implementación de funciones de llamada en producción, que cubra el diseño de herramientas, el manejo de errores, la seguridad y los patrones específicos del proveedor.

Los ejercicios

  1. Add a 6th tool: database query.Implemente una herramienta SQL simulada con una tabla en memoria. La herramienta acepta un nombre de tabla y condiciones de filtro (no SQL crudo). Valida que el nombre de tabla está en una lista de permisos y que los operadores de filtro están restringidos a =¿ Qué ?>¿ Qué ?<¿ Qué ?>=¿ Qué ?<=. Regresa las filas correspondientes como JSON.
  1. Implement retry with error feedback.Cuando una llamada de herramienta falla (por ejemplo, la ciudad no se encuentra), envíe el mensaje de error de nuevo a la función de decisión del modelo y déjalo corregir sus argumentos.
  1. Build a multi-step agent.Algunas consultas requieren llamadas de herramientas de cadena: "Lea el archivo de configuración y dime qué modelo está configurado, luego busca en la web el precio de ese modelo". Implemente un bucle que se ejecuta hasta que el modelo decida que no se necesitan más herramientas, pasando los resultados acumulados en cada paso de decisión. Limite a 10 iteraciones para evitar bucles infinitos.
  1. Measure tool selection accuracy.Crea 30 consultas de prueba con nombres de herramientas esperados. ejecuta la función de decisión en las 30 y mide el porcentaje de tiempo que selecciona la herramienta correcta. Identifique qué consultas causan más confusión entre las herramientas.
  1. Implement tool call caching.Si se llama la misma herramienta con argumentos idénticos dentro de 60 segundos, devuelva el resultado almacenado en caché en lugar de volver a ejecutarlo.(tool_name, frozenset(args.items()))Mide las tasas de caché en una conversación con 20 consultas.

Términos clave

TermWhat people sayWhat it actually means
Function calling"Tool use"The model outputs structured JSON describing a function to invoke with specific arguments -- your code executes it, not the model
Tool definition"Function schema"A JSON Schema object describing a tool's name, purpose, parameters, and types -- the model reads this to decide when and how to use the tool
Tool choice"Calling mode"Controls whether the model must call a tool (required), may call a tool (auto), or must call a specific tool (named)
Parallel calling"Multi-tool"The model outputs multiple tool calls in a single turn, reducing round trips -- GPT-4o and Claude both support this
Tool result"Function output"The return value from executing a tool, sent back to the model as a message so it can use real data in its response
Argument validation"Input checking"Verifying that model-generated arguments match the expected types, ranges, and constraints before executing the tool
MCP"Tool protocol"Model Context Protocol -- Anthropic's open standard for exposing tools via servers that any compatible client can discover and call
Agent loop"ReAct loop"The iterative cycle of model-decides-tool, code-executes-tool, result-feeds-back until the model has enough information to respond
Tool poisoning"Prompt injection via tools"An attack where tool results contain instructions that manipulate the model's behavior -- sanitize all tool outputs
Rate limiting"Call budget"Setting a maximum number of tool calls per conversation to prevent infinite loops and runaway API costs

Leer más

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.