Módulo 2: Tools y Tool Calling

Crear Tools con @tool

Descripción de la cápsula

El decorador @tool de LangChain convierte cualquier función Python en una herramienta que un modelo de lenguaje puede invocar. Es la forma más directa y recomendada de crear tools — defines una función normal, le agregas @tool, y LangChain genera automáticamente el schema JSON que el modelo necesita para saber cuándo llamarla y con qué argumentos.

En esta cápsula aprenderás a crear tools básicas, definir schemas de argumentos con Pydantic, crear tools async, retornar datos estructurados, y conocer las tools built-in más útiles del ecosistema. Al terminar, podrás crear cualquier tool custom que necesites para tus aplicaciones.

Todo lo que aprendes aquí es la base del mini-proyecto del módulo: un asistente con tools de clima, búsqueda y calculadora.


Tu primera tool

El decorador @tool

Crear una tool es tan simple como agregar @tool a una función Python:

from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -> int:
    """Multiplica dos números."""
    return a * b

Eso es todo. Con esas 4 líneas tienes una tool funcional. Pero ¿qué genera LangChain internamente? Vamos a inspeccionarla:

from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -> int:
    """Multiplica dos números."""
    return a * b

print(multiply.name)
# Output esperado: multiply

print(multiply.description)
# Output esperado: Multiplica dos números.

print(multiply.args_schema.schema())
# Output esperado:
# {
#     'description': 'Multiplica dos números.',
#     'properties': {
#         'a': {'title': 'A', 'type': 'integer'},
#         'b': {'title': 'B', 'type': 'integer'}
#     },
#     'required': ['a', 'b'],
#     'title': 'multiplySchema',
#     'type': 'object'
# }

LangChain extrae 3 cosas automáticamente de tu función:

  1. name — El nombre de la función (multiply)
  2. description — El docstring de la función ("Multiplica dos números.")
  3. args_schema — Los tipos de los parámetros, convertidos a JSON Schema

El modelo usa estas 3 cosas para decidir cuándo llamar la tool y qué argumentos pasarle.


Por qué importan name y description

El modelo lee el nombre y la descripción de cada tool para decidir cuál usar. Si tu descripción es vaga o incorrecta, el modelo no sabrá cuándo llamarla.

from langchain_core.tools import tool

# ❌ Mala descripción — el modelo no entiende cuándo usarla
@tool
def process(x: str) -> str:
    """Procesa datos."""
    return x.upper()

# ✅ Buena descripción — el modelo sabe exactamente cuándo usarla
@tool
def uppercase_text(text: str) -> str:
    """Convierte un texto a mayúsculas. Úsala cuando el usuario pida poner texto en mayúsculas."""
    return text.upper()

Reglas para buenos nombres y descripciones:

  • ✅ El nombre debe ser descriptivo: get_weather, search_web, calculate_total
  • ✅ La descripción debe explicar qué hace y cuándo usarla
  • ❌ Evita nombres genéricos: process, run, do_stuff
  • ❌ Evita descripciones vagas: "Procesa datos", "Hace una cosa"

Invocar una tool directamente

Puedes ejecutar una tool directamente — útil para testing:

from langchain_core.tools import tool

@tool
def add(a: int, b: int) -> int:
    """Suma dos números."""
    return a + b

result = add.invoke({"a": 3, "b": 5})
print(result)
# Output esperado: 8

Cuando la invocas directamente, usas .invoke() con un diccionario de argumentos. Cuando el modelo la llama, LangChain hace esta conversión por ti.


Argument schemas con Pydantic

Por qué Pydantic

Los type hints básicos (int, str, float) funcionan, pero no le dicen al modelo qué significa cada argumento. Con Pydantic, puedes agregar descripciones detalladas que ayudan al modelo a generar los argumentos correctos.

from langchain_core.tools import tool
from pydantic import BaseModel, Field

class WeatherInput(BaseModel):
    city: str = Field(description="Nombre de la ciudad (ej: 'Madrid', 'Buenos Aires')")
    units: str = Field(
        default="celsius",
        description="Unidad de temperatura: 'celsius' o 'fahrenheit'"
    )

@tool(args_schema=WeatherInput)
def get_weather(city: str, units: str = "celsius") -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es 22°{'C' if units == 'celsius' else 'F'}, soleado"

print(get_weather.args_schema.schema())
# Output esperado:
# {
#     'description': 'Obtiene el clima actual de una ciudad.',
#     'properties': {
#         'city': {
#             'description': "Nombre de la ciudad (ej: 'Madrid', 'Buenos Aires')",
#             'title': 'City',
#             'type': 'string'
#         },
#         'units': {
#             'default': 'celsius',
#             'description': "Unidad de temperatura: 'celsius' o 'fahrenheit'",
#             'title': 'Units',
#             'type': 'string'
#         }
#     },
#     'required': ['city'],
#     'title': 'WeatherInput',
#     'type': 'object'
# }

El Field(description=...) es lo que marca la diferencia — el modelo lee esas descripciones para entender qué valor poner en cada argumento.


Tools con múltiples parámetros

Con Pydantic defines exactamente cuáles parámetros son requeridos y cuáles tienen default:

from langchain_core.tools import tool
from pydantic import BaseModel, Field

class SearchInput(BaseModel):
    query: str = Field(description="Término de búsqueda")
    max_results: int = Field(default=5, description="Número máximo de resultados (1-20)")
    language: str = Field(default="es", description="Idioma de los resultados: 'es', 'en', 'fr'")

@tool(args_schema=SearchInput)
def web_search(query: str, max_results: int = 5, language: str = "es") -> str:
    """Busca información en la web. Usa esta herramienta cuando necesites datos actualizados."""
    return f"Resultados para '{query}' (max: {max_results}, idioma: {language})"

result = web_search.invoke({"query": "LangChain tutorial"})
print(result)
# Output esperado: Resultados para 'LangChain tutorial' (max: 5, idioma: es)

result = web_search.invoke({"query": "AI news", "max_results": 3, "language": "en"})
print(result)
# Output esperado: Resultados para 'AI news' (max: 3, idioma: en)

Tools con tipos restringidos

Puedes usar Literal para restringir valores válidos — reduce errores del modelo significativamente:

from typing import Literal
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class ConversionInput(BaseModel):
    amount: float = Field(description="Cantidad a convertir")
    from_currency: Literal["USD", "EUR", "MXN", "ARS"] = Field(
        description="Moneda de origen"
    )
    to_currency: Literal["USD", "EUR", "MXN", "ARS"] = Field(
        description="Moneda de destino"
    )

@tool(args_schema=ConversionInput)
def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
    """Convierte una cantidad entre monedas. Soporta USD, EUR, MXN y ARS."""
    rates = {
        ("USD", "MXN"): 17.5,
        ("USD", "EUR"): 0.92,
        ("EUR", "USD"): 1.09,
        ("MXN", "USD"): 0.057,
    }
    rate = rates.get((from_currency, to_currency), 1.0)
    result = amount * rate
    return f"{amount} {from_currency} = {result:.2f} {to_currency}"

result = convert_currency.invoke({
    "amount": 100,
    "from_currency": "USD",
    "to_currency": "MXN"
})
print(result)
# Output esperado: 100 USD = 1750.00 MXN

Cuando usas Literal, el modelo sabe exactamente qué valores puede pasar.


Tools async

Si tu tool hace llamadas a APIs externas, querrás que sea async para no bloquear el event loop:

import asyncio
from langchain_core.tools import tool

@tool
async def fetch_price(symbol: str) -> str:
    """Obtiene el precio actual de una acción. Usa el símbolo de ticker (ej: 'AAPL', 'GOOGL')."""
    await asyncio.sleep(0.1)  # Simulando llamada a API
    prices = {"AAPL": 195.50, "GOOGL": 175.20, "MSFT": 425.80}
    price = prices.get(symbol.upper(), None)
    if price is None:
        return f"No se encontró el símbolo '{symbol}'"
    return f"{symbol.upper()}: ${price}"

result = asyncio.run(fetch_price.ainvoke({"symbol": "AAPL"}))
print(result)
# Output esperado: AAPL: $195.5

Para tools async, usas async def y las invocas con .ainvoke(). LangChain detecta automáticamente si la tool es sync o async.


Tools que retornan datos estructurados

Una tool puede retornar cualquier tipo de dato serializable — strings, diccionarios, listas:

from langchain_core.tools import tool

@tool
def get_user_info(user_id: int) -> dict:
    """Obtiene información de un usuario por su ID."""
    users = {
        1: {"name": "María García", "email": "maria@example.com", "plan": "pro"},
        2: {"name": "Carlos López", "email": "carlos@example.com", "plan": "free"},
    }
    user = users.get(user_id)
    if user is None:
        return {"error": f"Usuario con ID {user_id} no encontrado"}
    return user

result = get_user_info.invoke({"user_id": 1})
print(result)
# Output esperado: {'name': 'María García', 'email': 'maria@example.com', 'plan': 'pro'}

result = get_user_info.invoke({"user_id": 99})
print(result)
# Output esperado: {'error': 'Usuario con ID 99 no encontrado'}

El modelo recibirá el diccionario como string y lo integrará en su respuesta. Retornar datos estructurados es útil cuando el modelo necesita extraer información específica del resultado.


Personalizar name y description

Puedes sobrescribir el nombre y la descripción automáticos pasando argumentos a @tool:

from langchain_core.tools import tool

@tool("calculadora_basica")
def calc(expression: str) -> str:
    """Evalúa una expresión matemática simple. Soporta +, -, *, / y paréntesis.

    Ejemplos de input: '2 + 3', '(10 * 5) / 2', '100 - 37'
    """
    try:
        result = eval(expression)
        return str(result)
    except Exception as e:
        return f"Error evaluando '{expression}': {e}"

print(calc.name)
# Output esperado: calculadora_basica

result = calc.invoke({"expression": "(10 * 5) / 2"})
print(result)
# Output esperado: 25.0

Built-in tools

LangChain incluye tools pre-construidas para tareas comunes. No necesitas crearlas desde cero.

DuckDuckGoSearchResults

pip install duckduckgo-search
from langchain_community.tools import DuckDuckGoSearchResults

search = DuckDuckGoSearchResults(max_results=3)
print(search.name)
# Output esperado: duckduckgo_results_json

result = search.invoke("LangChain v1.2 release")
print(result)
# Output esperado: [snippet: ..., title: ..., link: ...]

WikipediaQueryRun

pip install wikipedia
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper

wiki = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=500))
print(wiki.name)
# Output esperado: wikipedia

result = wiki.invoke("Python programming language")
print(result[:200])
# Output esperado: Page: Python (programming language)
# Summary: Python is a high-level, general-purpose programming language...

Cuándo usar built-in vs custom

CriterioBuilt-in toolsCustom tools (@tool)
Búsqueda web✅ DuckDuckGoSearchResultsSi necesitas una API específica
Wikipedia✅ WikipediaQueryRunSi necesitas otra fuente
Tu base de datos✅ Solo tú conoces tu schema
Tu API interna✅ Solo tú conoces tus endpoints
Cálculos custom✅ Tu lógica de negocio

En la mayoría de proyectos reales, usarás una mezcla de ambas.


@tool vs StructuredTool.from_function

@tool es la forma recomendada, pero existe una alternativa programática: StructuredTool.from_function. Ambas producen el mismo resultado.

from langchain_core.tools import tool, StructuredTool

# Opción 1: @tool (recomendado — más conciso)
@tool
def add_v1(a: int, b: int) -> int:
    """Suma dos números."""
    return a + b

# Opción 2: StructuredTool.from_function (programático)
def add_v2_func(a: int, b: int) -> int:
    """Suma dos números."""
    return a + b

add_v2 = StructuredTool.from_function(
    func=add_v2_func,
    name="add_v2",
    description="Suma dos números."
)

print(add_v1.invoke({"a": 2, "b": 3}))  # 5
print(add_v2.invoke({"a": 2, "b": 3}))  # 5

¿Cuándo usar StructuredTool.from_function? Cuando necesitas crear tools dinámicamente — por ejemplo, generar N tools a partir de un config:

from langchain_core.tools import StructuredTool

def create_math_tools() -> list:
    """Genera tools de operaciones matemáticas dinámicamente."""
    operations = {
        "sum": lambda a, b: a + b,
        "subtract": lambda a, b: a - b,
        "multiply": lambda a, b: a * b,
    }
    descriptions = {
        "sum": "Suma dos números",
        "subtract": "Resta el segundo número del primero",
        "multiply": "Multiplica dos números",
    }
    tools = []
    for name, func in operations.items():
        t = StructuredTool.from_function(
            func=func, name=name, description=descriptions[name],
        )
        tools.append(t)
    return tools

math_tools = create_math_tools()
for t in math_tools:
    print(f"{t.name}: {t.description}")
# Output esperado:
# sum: Suma dos números
# subtract: Resta el segundo número del primero
# multiply: Multiplica dos números

Recomendación: Usa @tool como default. Solo usa StructuredTool.from_function cuando necesites generación dinámica.


Conexión con el proyecto

En el Asistente con Herramientas Externas (proyecto de este módulo):

  • Crearás 3 tools custom con @tool: get_weather, web_search, calculate
  • Cada tool tendrá schemas Pydantic con descripciones claras
  • El modelo usará las descripciones para decidir cuál tool llamar según la pregunta del usuario
  • En la siguiente cápsula (03) aprenderás a conectar estas tools al modelo con bind_tools()

Todo lo que aprendes aquí se aplica directamente en la Cápsula 08.


Troubleshooting

Problema 1: El modelo no llama la tool

Causa: La descripción de la tool es vaga o no coincide con la intención del usuario. Solución:

from langchain_core.tools import tool

# ❌ Descripción vaga
@tool
def search(q: str) -> str:
    """Busca cosas."""
    return f"Resultados: {q}"

# ✅ Descripción precisa
@tool
def search_web(query: str) -> str:
    """Busca información actualizada en internet. Usa esta herramienta cuando
    el usuario pregunte sobre datos recientes, noticias, o información que
    pueda haber cambiado desde tu último entrenamiento."""
    return f"Resultados: {query}"

Problema 2: El modelo pasa argumentos incorrectos

Causa: Los parámetros no tienen type hints o las descripciones de Field son ambiguas. Solución:

from langchain_core.tools import tool
from pydantic import BaseModel, Field

# ❌ Sin descripciones — el modelo adivina
@tool
def query_db(table: str, limit: int) -> str:
    """Consulta la base de datos."""
    return f"SELECT * FROM {table} LIMIT {limit}"

# ✅ Con descripciones claras — el modelo sabe qué pasar
class QueryInput(BaseModel):
    table: str = Field(description="Nombre de la tabla: 'users', 'orders', 'products'")
    limit: int = Field(default=10, description="Número de filas a retornar (1-100)")

@tool(args_schema=QueryInput)
def query_db(table: str, limit: int = 10) -> str:
    """Consulta la base de datos del sistema. Retorna filas de la tabla especificada."""
    return f"SELECT * FROM {table} LIMIT {limit}"

Problema 3: La tool falla con datos reales

Causa: No hay manejo de errores dentro de la tool. Solución:

from langchain_core.tools import tool

@tool
def divide(a: float, b: float) -> str:
    """Divide el primer número entre el segundo."""
    if b == 0:
        return "Error: no se puede dividir entre cero"
    return f"{a} / {b} = {a / b:.4f}"

print(divide.invoke({"a": 10, "b": 0}))
# Output esperado: Error: no se puede dividir entre cero

Ejercicios

Ejercicio 1: Tool de saludo personalizado (Fácil)

Crea una tool llamada greet que reciba un nombre (name) y un idioma (language: "es", "en", "fr") y retorne un saludo en ese idioma. Inspecciona su name, description y args_schema.

Ver solución
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from typing import Literal

class GreetInput(BaseModel):
    name: str = Field(description="Nombre de la persona a saludar")
    language: Literal["es", "en", "fr"] = Field(
        default="es",
        description="Idioma del saludo: 'es' (español), 'en' (inglés), 'fr' (francés)"
    )

@tool(args_schema=GreetInput)
def greet(name: str, language: str = "es") -> str:
    """Genera un saludo personalizado en el idioma indicado."""
    greetings = {
        "es": f"¡Hola, {name}! ¿Cómo estás?",
        "en": f"Hello, {name}! How are you?",
        "fr": f"Bonjour, {name}! Comment ça va?",
    }
    return greetings.get(language, f"¡Hola, {name}!")

print(greet.name)
# Output esperado: greet

print(greet.description)
# Output esperado: Genera un saludo personalizado en el idioma indicado.

print(greet.invoke({"name": "María"}))
# Output esperado: ¡Hola, María! ¿Cómo estás?

print(greet.invoke({"name": "John", "language": "en"}))
# Output esperado: Hello, John! How are you?

Explicación: Literal["es", "en", "fr"] restringe los valores válidos. El modelo ve esta restricción en el schema y solo genera valores de esa lista.

Ejercicio 2: Tool con validación de errores (Fácil)

Crea una tool divide que divida dos números. Debe manejar la división entre cero retornando un mensaje de error en vez de lanzar una excepción.

Ver solución
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class DivideInput(BaseModel):
    a: float = Field(description="Numerador (dividendo)")
    b: float = Field(description="Denominador (divisor)")

@tool(args_schema=DivideInput)
def divide(a: float, b: float) -> str:
    """Divide el primer número entre el segundo. Retorna error si el divisor es cero."""
    if b == 0:
        return "Error: no se puede dividir entre cero"
    return f"{a} / {b} = {a / b:.4f}"

print(divide.invoke({"a": 10, "b": 3}))
# Output esperado: 10.0 / 3.0 = 3.3333

print(divide.invoke({"a": 5, "b": 0}))
# Output esperado: Error: no se puede dividir entre cero

Explicación: Siempre maneja errores dentro de la tool retornando un string descriptivo. Si lanzas una excepción, el modelo no recibe feedback útil sobre qué salió mal.

Ejercicio 3: Tool de consulta con schema complejo (Medio)

Crea una tool search_products que reciba category (Literal), min_price (float), max_price (float) y in_stock (bool). Debe retornar una lista de productos filtrados de un catálogo simulado.

Ver solución
from typing import Literal
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class ProductSearchInput(BaseModel):
    category: Literal["electronics", "clothing", "books"] = Field(
        description="Categoría de producto"
    )
    min_price: float = Field(default=0, description="Precio mínimo en USD")
    max_price: float = Field(default=1000, description="Precio máximo en USD")
    in_stock: bool = Field(default=True, description="Solo mostrar productos en stock")

@tool(args_schema=ProductSearchInput)
def search_products(
    category: str, min_price: float = 0, max_price: float = 1000, in_stock: bool = True
) -> list:
    """Busca productos en el catálogo filtrados por categoría, precio y disponibilidad."""
    catalog = [
        {"name": "Laptop Pro", "category": "electronics", "price": 999.99, "stock": True},
        {"name": "Auriculares BT", "category": "electronics", "price": 79.99, "stock": True},
        {"name": "Cable USB", "category": "electronics", "price": 12.99, "stock": False},
        {"name": "Camiseta Dev", "category": "clothing", "price": 25.99, "stock": True},
        {"name": "Clean Code", "category": "books", "price": 35.00, "stock": True},
        {"name": "Design Patterns", "category": "books", "price": 45.00, "stock": False},
    ]
    results = [
        p for p in catalog
        if p["category"] == category
        and min_price <= p["price"] <= max_price
        and (not in_stock or p["stock"])
    ]
    return results

result = search_products.invoke({"category": "electronics", "max_price": 100})
print(result)
# Output esperado:
# [{'name': 'Auriculares BT', 'category': 'electronics', 'price': 79.99, 'stock': True}]

result = search_products.invoke({"category": "books", "in_stock": False})
print(result)
# Output esperado:
# [
#     {'name': 'Clean Code', 'category': 'books', 'price': 35.0, 'stock': True},
#     {'name': 'Design Patterns', 'category': 'books', 'price': 45.0, 'stock': False}
# ]

Explicación: Con in_stock=False se muestran todos los productos sin filtrar por stock. Los Literal types restringen los valores válidos de categoría.

Ejercicio 4: Tool completa para el proyecto (Difícil)

Crea la tool get_weather que se usará en el proyecto del módulo. Debe recibir city y units (celsius/fahrenheit), retornar un dict con temperatura, condición y humedad, e incluir manejo de errores para ciudades no encontradas.

Ver solución
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from typing import Literal

class WeatherInput(BaseModel):
    city: str = Field(description="Nombre de la ciudad (ej: 'Madrid', 'CDMX', 'Buenos Aires')")
    units: Literal["celsius", "fahrenheit"] = Field(
        default="celsius",
        description="Unidad de temperatura"
    )

@tool(args_schema=WeatherInput)
def get_weather(city: str, units: str = "celsius") -> dict:
    """Obtiene el clima actual de una ciudad. Retorna temperatura, condición y humedad.
    Úsala cuando el usuario pregunte por el clima, tiempo, o temperatura de una ubicación."""
    weather_data = {
        "Madrid": {"temp_c": 22, "condition": "Soleado", "humidity": 45},
        "CDMX": {"temp_c": 18, "condition": "Nublado", "humidity": 65},
        "Buenos Aires": {"temp_c": 15, "condition": "Parcialmente nublado", "humidity": 72},
        "New York": {"temp_c": 10, "condition": "Lluvioso", "humidity": 85},
        "Tokyo": {"temp_c": 25, "condition": "Soleado", "humidity": 55},
    }

    data = weather_data.get(city)
    if data is None:
        return {
            "error": f"Ciudad '{city}' no encontrada",
            "available_cities": list(weather_data.keys())
        }

    temp = data["temp_c"]
    if units == "fahrenheit":
        temp = round(temp * 9 / 5 + 32, 1)

    unit_symbol = "°C" if units == "celsius" else "°F"

    return {
        "city": city,
        "temperature": f"{temp}{unit_symbol}",
        "condition": data["condition"],
        "humidity": f"{data['humidity']}%"
    }

print(get_weather.invoke({"city": "Madrid"}))
# Output esperado:
# {'city': 'Madrid', 'temperature': '22°C', 'condition': 'Soleado', 'humidity': '45%'}

print(get_weather.invoke({"city": "New York", "units": "fahrenheit"}))
# Output esperado:
# {'city': 'New York', 'temperature': '50.0°F', 'condition': 'Lluvioso', 'humidity': '85%'}

print(get_weather.invoke({"city": "Londres"}))
# Output esperado:
# {'error': "Ciudad 'Londres' no encontrada", 'available_cities': ['Madrid', 'CDMX', ...]}

Explicación: Esta tool tiene todo lo que necesitas para el proyecto final: schema Pydantic con descripciones, conversión de unidades, datos estructurados como retorno, y manejo de errores que da feedback útil. En producción, reemplazarías el diccionario hardcodeado por una llamada a una API real como OpenWeatherMap.


Resumen

En esta cápsula aprendiste:

  • El decorador @tool convierte cualquier función Python en una tool invocable por modelos
  • LangChain extrae automáticamente name, description y args_schema de tu función
  • Las descripciones claras son críticas — el modelo las usa para decidir cuándo llamar cada tool
  • Con Pydantic (BaseModel + Field) defines schemas detallados con descripciones por argumento
  • Las tools pueden ser sync o async — usa async def para I/O y .ainvoke() para ejecutarlas
  • Las tools pueden retornar cualquier tipo serializable: strings, dicts, listas
  • Built-in tools como DuckDuckGoSearchResults cubren tareas genéricas
  • StructuredTool.from_function es la alternativa para generar tools dinámicamente
  • Siempre maneja errores dentro de la tool retornando mensajes descriptivos

Próxima cápsula: bind_tools y el flujo de tool calling — aprenderás a conectar estas tools a un modelo y ver cómo el modelo decide cuál llamar.


Recursos adicionales

  1. How to create tools — Guía oficial paso a paso
  2. Tools Conceptual Guide — Cómo funciona el sistema de tools internamente
  3. @tool API Reference — Referencia completa del decorador
  4. StructuredTool API Reference — Referencia de StructuredTool
  5. Pydantic Field Documentation — Cómo usar Field para descripciones y validaciones
  6. LangChain Built-in Tools — Catálogo completo de tools pre-construidas
  7. Tool Calling Conceptual Guide — Cómo funciona el flujo de tool calling
  8. OpenAI Function Calling — La especificación original que inspira tool calling

Módulo 2 — LangChain & LangGraph: From Chains to Agents