13. Conectar herramientas al LLM
En el Capítulo 12 construimos herramientas que un agente puede usar. Ahora es el momento de conectar estas herramientas al LLM para que el agente pueda usarlas de verdad.
Por muy bien construida que esté una herramienta, el LLM no puede usarla sin saber que existe. Necesita conocer qué hay disponible y elegir la herramienta adecuada para cada solicitud. Sin embargo, como aprendimos en el Capítulo 12, el LLM no ejecuta las herramientas directamente. En su lugar, solicita "llama a esta herramienta con estos argumentos", y nuestro código del agente invoca la herramienta. Todo este mecanismo se denomina tool calling.
En este capítulo vincularemos herramientas al LLM, ejecutaremos las solicitudes de llamada a herramientas y devolveremos los resultados al LLM, implementando el ciclo completo. Este ciclo es la base del bucle del agente que construiremos en el Capítulo 14.
13.1) Vincular herramientas e inspeccionar las solicitudes de llamada a herramientas
Para que el LLM use herramientas, primero necesita saber qué hay disponible. Cuando le pasamos el name, la description y el esquema de entrada de cada herramienta al LLM, este aprende cuándo y cómo usar cada herramienta. Este proceso se llama vinculación de herramientas (tool binding).
Una vez vinculadas las herramientas, el LLM hará una de dos cosas cuando reciba una pregunta. Si no se necesita ninguna herramienta, responde con texto como de costumbre. Si se necesita una herramienta, devuelve una solicitud estructurada: "llama a esta herramienta con estos argumentos". Nuestro agente entonces inspecciona esta solicitud e invoca la herramienta especificada.
Empecemos por cómo vincular herramientas.
13.1.1) Vincular herramientas con bind_tools()
La vinculación de herramientas se gestiona con un solo método: bind_tools(). Todo modelo de chat que soporte tool calling proporciona este método. Llamar a bind_tools() devuelve un nuevo objeto de modelo con las herramientas dadas vinculadas a él.
Vinculemos la herramienta get_weather que construimos en el Capítulo 12.
from langchain_openai import ChatOpenAI
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad dada."""
return f"It's always sunny in {city}!"
# Vincula la herramienta al modelo: se devuelve un nuevo objeto de modelo con la herramienta vinculada
llm_with_tools = llm.bind_tools([get_weather])Eso es todo lo que se necesita. El llm_with_tools devuelto es un modelo que conoce la herramienta get_weather. Ten en cuenta que el llm original no ha cambiado en absoluto: sigue sin saber nada de ninguna herramienta.
bind_tools() recibe una lista de herramientas como argumento y devuelve un nuevo objeto de modelo con esas herramientas vinculadas. Internamente, convierte los metadatos de cada herramienta (name, description y el esquema de entrada) a un formato que el proveedor del LLM entiende, de modo que los esquemas de las herramientas se envían junto con cada invocación del modelo.
13.1.2) Inspeccionar las solicitudes de llamada a herramientas
¿Qué ocurre cuando enviamos una pregunta sobre el clima a un modelo que tiene vinculada una herramienta de clima?
response = llm_with_tools.invoke("¿Qué tiempo hace en París?")
print(f"type: {type(response)}\n")
print(f"tool_calls: {response.tool_calls}\n")
print(f"content: {repr(response.content)}\n")Salida:
type: <class 'langchain_core.messages.ai.AIMessage'>
tool_calls: [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}]
content: ''Como llm_with_tools sigue siendo un modelo de chat, devuelve un AIMessage. Pero hay dos cosas distintas en esta respuesta.
Primero, response.tool_calls contiene una lista de solicitudes de llamada a herramientas. Cada solicitud es un diccionario con cuatro claves:
name— el nombre de la herramienta que se debe llamar. Corresponde alnamede la herramienta en sus metadatos.args— los argumentos que se pasarán al llamar a la herramienta. El modelo observó el esquema de entrada y la pregunta del usuario para construir{'city': 'Paris'}.id— un identificador único para esta llamada. El LLM lo usa más adelante para emparejar los resultados de la ejecución de la herramienta con las solicitudes de llamada a herramientas.type— siempre'tool_call'.
Segundo, response.content es una cadena vacía. Esta respuesta no es una respuesta de texto final, es una solicitud de llamada a herramienta.
Para determinar si la respuesta es una solicitud de llamada a herramienta, comprueba tool_calls en lugar de content. Si tool_calls no está vacío, es una solicitud de llamada a herramienta. Si está vacío, el LLM ha proporcionado una respuesta de texto directamente.
13.2) Ejecutar herramientas y devolver los resultados
El LLM ha solicitado una llamada a la herramienta get_weather. Ahora nuestro código del agente necesita ejecutar la herramienta y devolver el resultado al LLM.
Procesar una solicitud de llamada a herramienta implica tres pasos:
- Extraer el nombre de la herramienta y los argumentos de
tool_callsy ejecutar la herramienta correspondiente. - Convertir el resultado de la ejecución en un
ToolMessage. - Enviar la conversación completa (pregunta del usuario + solicitud de llamada a herramienta del LLM + resultado de la ejecución de la herramienta) de vuelta al LLM para recibir una respuesta.
Aquí tienes un diagrama de este proceso:
13.2.1) Ejecutar la herramienta y crear un ToolMessage
Primero, extraigamos el nombre de la herramienta y los argumentos de tool_calls y ejecutemos la herramienta correspondiente. Para buscar una herramienta por su nombre, creamos un diccionario indexado por el nombre de la herramienta.
# Un diccionario para buscar herramientas por nombre
tool_map = {get_weather.name: get_weather}A continuación, sacamos una solicitud de llamada a herramienta de tool_calls, encontramos la herramienta correspondiente en tool_map y la ejecutamos.
if response.tool_calls:
tool_call = response.tool_calls[0]
# {'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
print(result) # Salida: It's always sunny in Paris!Para devolver el resultado de la ejecución de la herramienta al LLM, necesitamos envolverlo en un ToolMessage. Un ToolMessage tiene dos campos obligatorios:
content— el resultado de la ejecución de la herramienta como cadena.tool_call_id— eliddetool_calls. El LLM lo usa para identificar a qué solicitud pertenece el resultado.
from langchain_core.messages import ToolMessage
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
tool_message = ToolMessage(
content=result,
tool_call_id=tool_call["id"], # debe coincidir con el id de la solicitud
)
print(tool_message)Salida:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'En el código anterior, pasamos args para ejecutar la herramienta y luego combinamos el resultado con el tool_call_id para construir el ToolMessage.
Hay una forma más sencilla de hacer esto en un solo paso. En lugar de pasar solo args a .invoke(), pasa el diccionario tool_call completo. LangChain ejecutará la herramienta y devolverá un ToolMessage automáticamente.
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
print(tool_message)Salida:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'El mismo resultado, pero el código es mucho más simple y no hay posibilidad de un desajuste de tool_call_id. Usaremos este enfoque de aquí en adelante.
13.2.2) Enviar los resultados de la herramienta al LLM
Ahora pasemos el resultado de la ejecución de la herramienta al LLM. Enviamos el historial completo de la conversación —la pregunta del usuario, la solicitud de llamada a herramienta del LLM y el resultado de la ejecución de la herramienta— de vuelta al LLM, y este genera una respuesta basada en el resultado de la herramienta.
Juntemos todo el proceso de principio a fin en un solo fragmento de código.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
# Define las herramientas y vincúlalas
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad dada."""
return f"It's always sunny in {city}!"
llm_with_tools = llm.bind_tools([get_weather])
tool_map = {get_weather.name: get_weather}
# Paso 1: Envía la pregunta del usuario y recibe la respuesta del LLM
messages = [HumanMessage("¿Qué tiempo hace en París?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
# Paso 2: Si se solicitó una llamada a herramienta, ejecuta la herramienta y añade el resultado
if ai_msg.tool_calls:
tool_call = ai_msg.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
# Paso 3: Envía el historial completo de la conversación de vuelta al LLM
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Salida:
El tiempo actual en París es soleado. ¡Que tengas un buen día!El LLM generó una respuesta de texto final basada en el resultado de la ejecución de la herramienta. En 13.1, content era una cadena vacía, pero después de devolver el resultado de la herramienta, recibimos una respuesta real.
Comprobemos qué mensajes hay en la lista messages en el Paso 3.
for m in messages:
print(f"{type(m).__name__}: {m.content!r}")Salida:
HumanMessage: "¿Qué tiempo hace en París?"
AIMessage: ''
ToolMessage: "It's always sunny in Paris!"Hay tres mensajes almacenados en orden: la pregunta del usuario, la solicitud de llamada a herramienta del LLM (contenido vacío) y el resultado de la ejecución de la herramienta. El LLM vio la pregunta del usuario, decidió que necesitaba una herramienta y solicitó una llamada a get_weather. El código del agente ejecutó get_weather y añadió el resultado a la lista de mensajes. Una vez que tuvo todo lo que necesitaba, el LLM generó su respuesta final.
13.3) Dar al LLM múltiples herramientas
Los agentes del mundo real normalmente tienen múltiples herramientas. Todo lo que hemos cubierto hasta ahora funciona exactamente igual con múltiples herramientas.
Añadamos calculate y search_web junto a get_weather para vincular tres herramientas.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad dada."""
return f"It's always sunny in {city}!"
@tool
def calculate(expression: str) -> str:
"""Evalúa una expresión aritmética simple como '123 * 456'."""
return str(eval(expression)) # Advertencia: eval() es un riesgo de seguridad. No usar en producción.
@tool
def search_web(query: str) -> str:
"""Busca en la web información actual sobre un tema."""
return f"Top result for '{query}': ..."
tools = [get_weather, calculate, search_web]
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}Hagamos una pregunta de cálculo.
response = llm_with_tools.invoke("¿Cuánto es 123 multiplicado por 456?")
print(response.tool_calls)Salida:
[{'name': 'calculate', 'args': {'expression': '123 * 456'}, 'id': 'call_xyz789', 'type': 'tool_call'}]El LLM solicitó una llamada a calculate. Comparó la description de cada herramienta con la pregunta del usuario y determinó que calculate era la herramienta adecuada.
Podemos ejecutar la herramienta y obtener una respuesta final usando el mismo patrón de 13.2.
messages = [HumanMessage("¿Cuánto es 123 multiplicado por 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content) # Salida: 123 multiplicado por 456 es 56.088.En 13.2 solo manejamos la primera solicitud con tool_calls[0], pero aquí iteramos sobre todas las solicitudes con un bucle for. La razón es que el LLM puede solicitar múltiples llamadas a herramientas a la vez.
Múltiples solicitudes de herramientas simultáneas
Veamos qué ocurre cuando se solicitan múltiples herramientas al mismo tiempo. El código es el mismo, solo cambia la pregunta.
messages = [HumanMessage("¿Qué tiempo hace en Tokio, y cuánto es 123 * 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
print(f"Tool: {tool_call['name']}")
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Salida:
Tool: get_weather
Tool: calculate
El tiempo en Tokio es actualmente soleado, y 123 multiplicado por 456 es 56.088.El LLM solicitó tanto get_weather como calculate, y el bucle for ejecutó todas las solicitudes de tool_calls y añadió sus resultados a la lista de mensajes. El LLM luego usó estos resultados para generar la respuesta final.