12: Construir herramientas para tu agente
En la Parte IV, vamos a construir un agente(agent): una IA que averigua qué hacer con la petición de un usuario y luego lo hace, en lugar de simplemente responder a ella como un chatbot.
Aquí está la diferencia en acción. Supongamos que un usuario pregunta: "Por favor, cancela el pedido #12345." Un chatbot diría algo como: "Ve a Mi Página > Historial de pedidos y haz clic en el botón 'Cancelar' para ese pedido." A partir de ahí, le toca al usuario seguir esos pasos por su cuenta. Un agente hace el trabajo en su lugar: busca el pedido, comprueba si es elegible para cancelación y lo cancela. Toma acción.
Lo que hace eso posible son las herramientas(tools): una herramienta que busca un pedido, una herramienta que cancela uno, una herramienta que envía un correo electrónico. Con las herramientas, un LLM deja de estar limitado a generar texto y empieza a conseguir hacer cosas de verdad.
Lo construiremos pieza a pieza a lo largo de la Parte IV: primero las herramientas que usará el agente (este capítulo), luego conectar esas herramientas a un LLM (Capítulo 13) y, por último, el bucle del agente que recorre el ciclo decidir → actuar → observar (Capítulo 14).
Este capítulo cubre la primera pieza: definir herramientas, conectarlas a datos reales y gestionar errores de forma segura.
12.1) Definir herramientas con el decorador @tool
12.1.1) ¿Cómo funciona una herramienta?
Acabamos de ver a un agente buscar y cancelar un pedido usando herramientas. Antes de seguir, vale la pena aclarar una cosa: cuando la gente dice "el LLM usa una herramienta", suena como si el LLM fuera quien la llama directamente. No es así. El LLM nunca ejecuta nada por sí mismo: todo lo que hace es pedir que se llame a una herramienta con ciertos argumentos. La ejecución real ocurre en nuestro código.
Para que eso funcione, el LLM tiene que saber qué herramientas existen y cuándo se aplica cada una. Así que cada herramienta viene con tres piezas de metadatos:
name: un identificador corto comoget_orderque el LLM usa para especificar qué herramienta quiere.description: una frase que describe qué hace la herramienta y cuándo usarla. Esto es lo que el LLM lee para elegir la herramienta adecuada para la tarea.- Esquema de entrada(input schema): cuáles son los parámetros de la herramienta: sus nombres, tipos y significado. El LLM necesita esto para rellenar los argumentos correctamente.
Nada de esto requiere trabajo extra por tu parte. name viene directamente del nombre de la función, description viene de su docstring, y el esquema de entrada viene de las anotaciones de tipo de los parámetros. Lo único que tienes que hacer es adjuntar el decorador @tool de LangChain.
12.1.2) Construir tu primera herramienta
Pongámoslo en práctica. Escribe una función con anotaciones de tipo y un docstring, y luego decórala con @tool.
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad dada."""
return f"¡Siempre hace sol en {city}!"Veamos qué generó @tool para nosotros.
print(get_weather.name)
# Salida: get_weather
print(get_weather.description)
# Salida: Obtiene el clima actual de una ciudad dada.
print(get_weather.args)
# Salida: {'city': {'title': 'City', 'type': 'string'}}El nombre de la función get_weather se convirtió en su name, el docstring se convirtió en su description, y la anotación de tipo city: str se convirtió en su esquema de entrada. Las tres piezas de metadatos de la sección anterior se generaron automáticamente. Esto es lo que el LLM usa para elegir una herramienta y rellenar sus argumentos.
Una vez que @tool está adjunto, la función se convierte en un objeto herramienta de LangChain, lo que significa que ya no puedes llamarla como una función normal: get_weather("Paris") no funcionará. En su lugar, la llamas con .invoke(), el mismo método de ejecución estándar que usamos para las cadenas en el Capítulo 6. Los argumentos van como un diccionario:
result = get_weather.invoke({"city": "Paris"})
print(result)
# Salida: ¡Siempre hace sol en Paris!12.1.3) Personalizar name y description
Por defecto, name viene del nombre de la función y description del docstring. Puedes sobrescribir ambos.
Pasa un nombre como primer argumento a @tool:
@tool("web_search")
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para: {query}"
print(search.name)
# Salida: web_searchTambién puedes sobrescribir la descripción usando el parámetro description. Esto es útil cuando quieres mantener el docstring como una nota para otros desarrolladores mientras le das al LLM algo más adaptado:
@tool("calculator", description="Realiza operaciones aritméticas. Úsala para cualquier problema matemático.")
def calc(expression: str) -> str:
"""Evalúa una cadena con una expresión matemática."""
return str(eval(expression)) # ADVERTENCIA: eval() no es seguro. Nunca lo uses en producción.Usa snake_case para los nombres de las herramientas: algunos proveedores de LLM rechazan nombres con espacios o caracteres especiales.
12.1.4) Definir un esquema de entrada con Pydantic
Cuando una herramienta toma varios parámetros, o quieres describir cada uno individualmente, define el esquema de entrada con un modelo de Pydantic en su lugar. Este es el mismo BaseModel y Field que usamos para la salida estructurada en el Capítulo 7.
from pydantic import BaseModel, Field
from langchain.tools import tool
class WeatherInput(BaseModel):
"""Entrada para consultas de clima."""
location: str = Field(description="Nombre de la ciudad (p. ej., Seúl, Tokio)")
units: str = Field(default="celsius", description="Unidad de temperatura (celsius o fahrenheit)")
@tool(args_schema=WeatherInput)
def get_weather_detailed(location: str, units: str = "celsius") -> str:
"""Obtiene el clima actual con una unidad de temperatura elegida."""
temp = 22 if units == "celsius" else 72
return f"Clima actual en {location}: {temp} grados {units[0].upper()}"Lo que escribas en Field(description=...) se convierte en parte del esquema de entrada que el LLM lee, así sabe exactamente qué significa cada parámetro. La mayoría de las veces, las anotaciones de tipo y un docstring claro son todo lo que necesitas: recurre a args_schema solo cuando necesites ese nivel extra de detalle por parámetro.
12.2) Gestionar errores de herramientas
En el mundo real, las herramientas pueden fallar: se cae una conexión a la base de datos, o aparece una entrada que no habías previsto. En esta sección, gestionaremos estos errores dentro de la propia herramienta, para que el agente pueda responder con sensatez en lugar de detenerse en seco. Primero, configuremos las funciones de las que dependerán nuestras herramientas.
12.2.1) Configuración: funciones de búsqueda de productos
# product_service.py
PRODUCTS = {
1: {"name": "Wireless Mouse", "price": 29.99, "stock": 120},
2: {"name": "Mechanical Keyboard", "price": 89.99, "stock": 0},
3: {"name": "USB-C Hub", "price": 45.50, "stock": 35},
4: {"name": "Laptop Stand", "price": 39.00, "stock": 8},
}
def fetch_product(product_id: int) -> dict:
"""Busca la información de un producto por su ID."""
product = PRODUCTS.get(product_id)
if product is None:
raise ValueError(f"Producto con ID {product_id} no encontrado.")
return product
def fetch_stock(product_id: int) -> int:
"""Devuelve la cantidad en stock de un producto."""
product = PRODUCTS.get(product_id)
if product is None:
raise ValueError(f"Producto con ID {product_id} no encontrado.")
return product["stock"]Ambas funciones lanzan un ValueError cuando se les da un ID de producto que no existe.
12.2.2) Gestionar errores en una herramienta
Envolvamos fetch_product en una herramienta get_product.
from langchain.tools import tool
from product_service import fetch_product
@tool
def get_product(product_id: int) -> str:
"""Busca un producto por su ID. Devuelve su nombre, precio y nivel de stock."""
product = fetch_product(product_id)
return f"Producto {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} en stock."Con un ID válido, funciona como se espera.
print(get_product.invoke({"product_id": 1}))
# Salida: Producto 1: Wireless Mouse — $29.99, 120 en stock.Pero pasa un ID que no existe, y fetch_product lanza un ValueError que nada captura: la ejecución del agente se detiene justo ahí.
print(get_product.invoke({"product_id": 99}))
# ValueError: Producto con ID 99 no encontrado.La solución es sencilla: captura la excepción dentro de la herramienta y devuelve una cadena que el LLM pueda entender, en lugar de dejar que se propague. Tanto en caso de éxito como de fallo, la herramienta siempre devuelve una cadena, y el LLM usa esa cadena para decidir qué hacer a continuación.
from langchain.tools import tool
from product_service import fetch_product
@tool
def get_product(product_id: int) -> str:
"""Busca un producto por su ID. Devuelve su nombre, precio y nivel de stock."""
try:
product = fetch_product(product_id)
return f"Producto {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} en stock."
except ValueError as e:
return f"Error: {e}"
except Exception as e:
return f"Error inesperado al buscar el producto {product_id}: {e}"print(get_product.invoke({"product_id": 1}))
# Salida: Producto 1: Wireless Mouse — $29.99, 120 en stock.
print(get_product.invoke({"product_id": 99}))
# Salida: Error: Producto con ID 99 no encontrado.Un ID inexistente ya no lanza una excepción: en su lugar devuelve un mensaje de error que el LLM puede entender.
Apliquemos el mismo patrón a check_stock:
from product_service import fetch_stock
@tool
def check_stock(product_id: int) -> str:
"""Comprueba si un producto está actualmente en stock."""
try:
stock = fetch_stock(product_id)
if stock > 0:
return f"{stock} unidades disponibles."
return "Agotado."
except ValueError as e:
return f"Error: {e}"
except Exception as e:
return f"Error inesperado al comprobar el stock del producto {product_id}: {e}"Las herramientas construidas de esta manera se pueden probar directamente con .invoke(). Asegúrate de que una herramienta funciona correctamente por sí sola antes de conectarla a un LLM. De lo contrario, cuando algo salga mal dentro del agente más adelante, no podrás distinguir si la herramienta está rota o si el modelo simplemente tomó una mala decisión.