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:
- name — El nombre de la función (
multiply) - description — El docstring de la función (
"Multiplica dos números.") - 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
| Criterio | Built-in tools | Custom tools (@tool) |
|---|---|---|
| Búsqueda web | ✅ DuckDuckGoSearchResults | Si necesitas una API específica |
| Wikipedia | ✅ WikipediaQueryRun | Si 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
@toolconvierte 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 defpara I/O y.ainvoke()para ejecutarlas - Las tools pueden retornar cualquier tipo serializable: strings, dicts, listas
- Built-in tools como
DuckDuckGoSearchResultscubren tareas genéricas StructuredTool.from_functiones 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
- How to create tools — Guía oficial paso a paso
- Tools Conceptual Guide — Cómo funciona el sistema de tools internamente
- @tool API Reference — Referencia completa del decorador
- StructuredTool API Reference — Referencia de StructuredTool
- Pydantic Field Documentation — Cómo usar Field para descripciones y validaciones
- LangChain Built-in Tools — Catálogo completo de tools pre-construidas
- Tool Calling Conceptual Guide — Cómo funciona el flujo de tool calling
- OpenAI Function Calling — La especificación original que inspira tool calling
Módulo 2 — LangChain & LangGraph: From Chains to Agents