Módulo 2: Tools y Tool Calling
Tool Execution Loop
Descripción de la cápsula
En la cápsula anterior aprendiste que el modelo no ejecuta tools — solo te dice qué quiere llamar. Ahora viene la otra mitad del flujo: tú ejecutas la tool y devuelves el resultado al modelo.
El ciclo completo es: el usuario hace una pregunta → el modelo analiza sus tools disponibles → responde con tool_calls indicando qué llamar → tú ejecutas cada tool → creas un ToolMessage con el resultado → envías todo de vuelta al modelo → el modelo genera la respuesta final usando los resultados de las tools.
Este ciclo se llama tool execution loop, y es el patrón fundamental que hace posible que un LLM interactúe con el mundo exterior. Cada vez que ChatGPT busca en la web, ejecuta código, o consulta un archivo — está corriendo exactamente este loop. En esta cápsula vas a implementarlo manualmente, paso a paso, para que entiendas cada pieza antes de que un framework lo automatice por ti.
El flujo completo paso a paso
El tool execution loop tiene 5 pasos:
1. HumanMessage → El usuario hace una pregunta
2. model.invoke() → El modelo analiza y decide qué tools llamar
3. AIMessage → El modelo retorna tool_calls (no ejecuta nada)
4. Tú ejecutas tools → Llamas las funciones y obtienes resultados
5. ToolMessage → Empaquetas los resultados y se los envías al modelo
6. model.invoke() → El modelo genera la respuesta final con los resultados
Veámoslo en código completo:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es soleado, 22°C"
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather])
# Paso 1: Mensaje del usuario
messages = [HumanMessage(content="¿Qué clima hace en Madrid?")]
# Paso 2-3: El modelo decide llamar a get_weather
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Tool calls: {ai_response.tool_calls}")
# [{'name': 'get_weather', 'args': {'city': 'Madrid'}, 'id': 'call_abc123'}]
# Paso 4: Ejecutar la tool
tool_call = ai_response.tool_calls[0]
result = get_weather.invoke(tool_call["args"])
print(f"Resultado de la tool: {result}")
# El clima en Madrid es soleado, 22°C
# Paso 5: Crear ToolMessage con el resultado
tool_message = ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
)
messages.append(tool_message)
# Paso 6: El modelo genera la respuesta final
final_response = model_with_tools.invoke(messages)
print(f"Respuesta final: {final_response.content}")
# El clima en Madrid es soleado, con una temperatura de 22°C.
El modelo ahora tiene toda la información: la pregunta original, su propia decisión de llamar a get_weather, y el resultado de la ejecución. Con todo eso, genera una respuesta natural para el usuario.
ToolMessage: la pieza clave
ToolMessage es el tipo de mensaje que usas para devolver el resultado de una tool al modelo. Viene de langchain_core.messages y tiene dos campos obligatorios:
| Campo | Tipo | Descripción |
|---|---|---|
content | str | El resultado de la ejecución de la tool (siempre string) |
tool_call_id | str | El ID del tool call que originó esta ejecución |
from langchain_core.messages import ToolMessage
tool_message = ToolMessage(
content="El clima en Madrid es soleado, 22°C",
tool_call_id="call_abc123"
)
¿Por qué necesita tool_call_id?
El tool_call_id conecta el resultado con la solicitud original. Cuando el modelo pidió llamar get_weather, generó un ID único (call_abc123). Al devolver el resultado, usas ese mismo ID para que el modelo sepa "este es el resultado de la tool que pedí".
Sin el tool_call_id correcto, el modelo no puede asociar el resultado con su solicitud y la invocación fallará:
from langchain_core.messages import ToolMessage
# Correcto — el ID coincide con el tool call
tool_message = ToolMessage(
content="22°C y soleado",
tool_call_id="call_abc123" # Mismo ID que en ai_response.tool_calls[0]["id"]
)
# Incorrecto — ID inventado que no coincide
tool_message = ToolMessage(
content="22°C y soleado",
tool_call_id="id_inventado" # Error: no coincide con ningún tool call
)
content siempre es string
El resultado de la tool debe ser un string. Si tu tool retorna un número, un diccionario, o cualquier otro tipo, conviértelo a string:
from langchain_core.messages import ToolMessage
import json
result_dict = {"temp": 22, "condition": "soleado", "humidity": 45}
tool_message = ToolMessage(
content=json.dumps(result_dict, ensure_ascii=False),
tool_call_id="call_abc123"
)
print(tool_message.content)
# {"temp": 22, "condition": "soleado", "humidity": 45}
El modelo puede interpretar JSON, listas, tablas — cualquier formato textual.
La lista de mensajes: memoria del loop
El tool execution loop funciona acumulando mensajes en una lista. Cada paso agrega un mensaje nuevo, y el modelo recibe la lista completa en cada invocación:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es soleado, 22°C"
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather])
messages = [HumanMessage(content="¿Qué clima hace en Madrid?")]
print(f"Paso 1 — mensajes: {len(messages)}")
# Paso 1 — mensajes: 1
# [HumanMessage]
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Paso 2 — mensajes: {len(messages)}")
# Paso 2 — mensajes: 2
# [HumanMessage, AIMessage(tool_calls=[...])]
result = get_weather.invoke(ai_response.tool_calls[0]["args"])
tool_msg = ToolMessage(content=str(result), tool_call_id=ai_response.tool_calls[0]["id"])
messages.append(tool_msg)
print(f"Paso 3 — mensajes: {len(messages)}")
# Paso 3 — mensajes: 3
# [HumanMessage, AIMessage(tool_calls=[...]), ToolMessage]
final = model_with_tools.invoke(messages)
messages.append(final)
print(f"Paso 4 — mensajes: {len(messages)}")
# Paso 4 — mensajes: 4
# [HumanMessage, AIMessage(tool_calls), ToolMessage, AIMessage(content)]
for i, msg in enumerate(messages):
print(f" [{i}] {type(msg).__name__}: {msg.content[:50] if msg.content else f'tool_calls={len(msg.tool_calls)}'}")
# Output:
# [0] HumanMessage: ¿Qué clima hace en Madrid?
# [1] AIMessage: tool_calls=1
# [2] ToolMessage: El clima en Madrid es soleado, 22°C
# [3] AIMessage: El clima en Madrid es soleado, con una temperatura
La lista de mensajes es la "memoria" de la conversación. El modelo necesita ver toda la secuencia para entender el contexto: qué pidió el usuario, qué tool decidió llamar, qué resultado obtuvo, y con eso genera la respuesta.
Loop con múltiples tools
Cuando el modelo tiene varias tools disponibles, puede llamar a una diferente según la pregunta. El loop es el mismo — solo necesitas un mapa de tools para saber cuál ejecutar:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es soleado, 22°C"
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
@tool
def search_web(query: str) -> str:
"""Busca información actualizada en internet."""
return f"Resultados para '{query}': LangChain es un framework para LLMs creado en 2022."
tools = [get_weather, calculator, search_web]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
def run_tool_loop(question: str) -> str:
messages = [HumanMessage(content=question)]
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
if not ai_response.tool_calls:
return ai_response.content
for tool_call in ai_response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
tool_fn = tool_map[tool_name]
result = tool_fn.invoke(tool_args)
messages.append(ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
))
final_response = model_with_tools.invoke(messages)
return final_response.content
print(run_tool_loop("¿Qué clima hace en Tokio?"))
# El clima en Tokio es soleado, con una temperatura de 22°C.
print(run_tool_loop("¿Cuánto es 256 * 48?"))
# 256 × 48 = 12,288.
print(run_tool_loop("¿Qué es LangChain?"))
# LangChain es un framework para construir aplicaciones con LLMs, creado en 2022.
print(run_tool_loop("Hola, ¿cómo estás?"))
# ¡Hola! Estoy bien, gracias. ¿En qué puedo ayudarte?
El patrón tool_map
El diccionario tool_map es fundamental:
tools = [get_weather, calculator, search_web]
tool_map = {t.name: t for t in tools}
# {'get_weather': <tool>, 'calculator': <tool>, 'search_web': <tool>}
Cuando el modelo dice "llama a calculator", buscas en tool_map["calculator"] para obtener la función y ejecutarla. Este patrón escala a cualquier número de tools.
Parallel tool calls
A veces, el modelo necesita llamar múltiples tools en un solo turno. Por ejemplo, si preguntas "¿Qué clima hace en Madrid y cuánto es 15 * 37?", el modelo puede generar dos tool calls simultáneamente:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es soleado, 22°C"
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
tools = [get_weather, calculator]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
messages = [HumanMessage(content="¿Qué clima hace en Madrid y cuánto es 15 * 37?")]
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Tool calls: {len(ai_response.tool_calls)}")
for tc in ai_response.tool_calls:
print(f" - {tc['name']}({tc['args']})")
# Tool calls: 2
# - get_weather({'city': 'Madrid'})
# - calculator({'expression': '15 * 37'})
for tool_call in ai_response.tool_calls:
tool_fn = tool_map[tool_call["name"]]
result = tool_fn.invoke(tool_call["args"])
messages.append(ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
))
final = model_with_tools.invoke(messages)
print(f"\nRespuesta final: {final.content}")
# Respuesta final: El clima en Madrid es soleado, con 22°C. Y 15 × 37 = 555.
El loop es idéntico — simplemente iteras sobre todos los tool_calls en lugar de asumir que solo hay uno. Cada tool call genera su propio ToolMessage con su tool_call_id correspondiente.
Loop manual vs agente
El loop manual te da control total, pero es repetitivo. En el Módulo 3 aprenderás a usar create_react_agent que automatiza todo esto por ti:
| Característica | Loop manual | Agente |
|---|---|---|
| Control sobre ejecución | ✅ Total — tú decides si ejecutar | ⚠️ Ejecuta automáticamente |
| Simplicidad de código | ❌ Más boilerplate | ✅ Mínimo código |
| Multi-ronda automática | ❌ Debes implementar el while loop | ✅ Integrado |
| Debugging | ✅ Ves cada paso | ⚠️ Necesitas logging |
Regla: Usa loop manual para aprender, prototipar, y escenarios donde necesitas control granular. Usa agentes para producción y flujos estándar.
Construir una función de loop reutilizable
Consolidemos todo en una función reutilizable que maneja múltiples rondas (por si el modelo necesita llamar más tools tras ver los primeros resultados), errores, y parallel tool calls:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es soleado, 22°C"
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
def tool_execution_loop(
question: str,
tools: list,
model_name: str = "openai:gpt-4.1-mini",
max_rounds: int = 5,
) -> str:
"""Ejecuta el tool execution loop completo con soporte multi-ronda."""
tool_map = {t.name: t for t in tools}
model = init_chat_model(model_name)
model_with_tools = model.bind_tools(tools)
messages = [HumanMessage(content=question)]
for round_num in range(max_rounds):
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
if not ai_response.tool_calls:
return ai_response.content
for tc in ai_response.tool_calls:
if tc["name"] not in tool_map:
result = f"Error: tool '{tc['name']}' no encontrada"
else:
try:
result = str(tool_map[tc["name"]].invoke(tc["args"]))
except Exception as e:
result = f"Error ejecutando {tc['name']}: {e}"
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
return "Límite de rondas alcanzado."
print(tool_execution_loop("¿Qué clima hace en Madrid y cuánto es 100 / 7?", [get_weather, calculator]))
# El clima en Madrid es soleado, con 22°C. Y 100 ÷ 7 ≈ 14.29.
El max_rounds es un safety net para evitar loops infinitos. En la práctica, la mayoría de preguntas se resuelven en 1-2 rondas.
Manejo de errores en tools
Las tools pueden fallar — APIs que no responden, argumentos inválidos, timeouts. La clave es capturar el error y devolverlo como ToolMessage en lugar de dejar que el loop crashee. Cuando el modelo recibe un error como ToolMessage, lo interpreta y genera una respuesta apropiada para el usuario:
for tc in ai_response.tool_calls:
try:
result = tool_map[tc["name"]].invoke(tc["args"])
except Exception as e:
result = f"Error: {e}"
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
Este patrón ya está integrado en la función tool_execution_loop de arriba. Siempre envuelve la ejecución en try/except — nunca dejes que una tool fallida rompa el ciclo completo.
Conexión con el proyecto
En el Asistente con Herramientas Externas (Cápsula 08), implementarás el tool execution loop completo con tres herramientas reales. El asistente será conversacional — mantendrá el historial de mensajes entre turnos para que el usuario pueda hacer preguntas de seguimiento.
El patrón base del proyecto será:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es soleado, 22°C"
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
@tool
def search_web(query: str) -> str:
"""Busca información actualizada en internet."""
return f"Resultados para '{query}': ..."
tools = [get_weather, calculator, search_web]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
assistant = model.bind_tools(tools)
conversation = []
def chat(user_input: str) -> str:
conversation.append(HumanMessage(content=user_input))
response = assistant.invoke(conversation)
conversation.append(response)
while response.tool_calls:
for tc in response.tool_calls:
try:
result = str(tool_map[tc["name"]].invoke(tc["args"]))
except Exception as e:
result = f"Error: {e}"
conversation.append(ToolMessage(content=result, tool_call_id=tc["id"]))
response = assistant.invoke(conversation)
conversation.append(response)
return response.content
En el Módulo 3, aprenderás a usar create_react_agent que automatiza todo este loop — pero entender cómo funciona internamente te dará la base para debugging y personalización.
Troubleshooting
Problema 1: "ToolMessage must have a tool_call_id"
Síntoma: Error al invocar el modelo después de agregar un ToolMessage.
Causa: Creaste el ToolMessage sin tool_call_id, o el ID no coincide con ningún tool call del AIMessage anterior.
Solución: Siempre usa el id del tool call correspondiente:
for tc in ai_response.tool_calls:
result = tool_fn.invoke(tc["args"])
messages.append(ToolMessage(
content=str(result),
tool_call_id=tc["id"] # Siempre del tool call original
))
Problema 2: El modelo ignora el resultado de la tool
Síntoma: La respuesta final del modelo no usa la información de la tool.
Causa: El ToolMessage no se agregó a la lista de mensajes, o se agregó en el orden incorrecto.
Solución: Verifica que la secuencia de mensajes sea: HumanMessage → AIMessage(tool_calls) → ToolMessage(s) → invoke():
print([type(m).__name__ for m in messages])
# ['HumanMessage', 'AIMessage', 'ToolMessage'] ← correcto
# ['HumanMessage', 'ToolMessage', 'AIMessage'] ← incorrecto
Problema 3: "Tool X not found" en el tool_map
Síntoma: KeyError porque tool_call["name"] no está en tool_map.
Causa: El modelo "alucinó" un nombre de tool que no existe, o hay un typo en el nombre.
Solución: Siempre verifica que el nombre existe antes de ejecutar:
for tc in ai_response.tool_calls:
if tc["name"] not in tool_map:
result = f"Error: tool '{tc['name']}' no existe"
else:
result = str(tool_map[tc["name"]].invoke(tc["args"]))
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
Ejercicios
Ejercicio 1: Loop básico paso a paso (Fácil)
Implementa el tool execution loop completo para una sola tool get_population(country: str) que retorne la población de un país. Imprime cada paso del loop (el mensaje del usuario, el tool call, el resultado, y la respuesta final).
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_population(country: str) -> str:
"""Obtiene la población actual de un país."""
populations = {
"México": "129 millones",
"España": "47 millones",
"Argentina": "46 millones",
"Colombia": "52 millones",
}
return populations.get(country, f"No tengo datos de {country}")
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_population])
messages = [HumanMessage(content="¿Cuántas personas viven en México?")]
print(f"Paso 1 — Usuario: {messages[0].content}")
ai_response = model_with_tools.invoke(messages)
messages.append(ai_response)
print(f"Paso 2 — Modelo decide: {ai_response.tool_calls}")
tc = ai_response.tool_calls[0]
result = get_population.invoke(tc["args"])
print(f"Paso 3 — Tool ejecutada: {result}")
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
print(f"Paso 4 — ToolMessage creado")
final = model_with_tools.invoke(messages)
print(f"Paso 5 — Respuesta final: {final.content}")
# Output:
# Paso 1 — Usuario: ¿Cuántas personas viven en México?
# Paso 2 — Modelo decide: [{'name': 'get_population', 'args': {'country': 'México'}, 'id': '...'}]
# Paso 3 — Tool ejecutada: 129 millones
# Paso 4 — ToolMessage creado
# Paso 5 — Respuesta final: La población de México es de aproximadamente 129 millones de personas.
Explicación: Cada paso del loop está claramente separado. El modelo primero pide los datos (tool call), tú los ejecutas y devuelves como ToolMessage, y finalmente el modelo genera una respuesta natural integrando la información.
Ejercicio 2: Loop con dos tools (Fácil)
Crea dos tools: get_weather(city) y get_time(timezone). Implementa el loop para ambas. Prueba con "¿Qué clima hace en Lima?" y "¿Qué hora es en UTC-5?".
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es nublado, 18°C"
@tool
def get_time(timezone: str) -> str:
"""Obtiene la hora actual en una zona horaria."""
return f"En {timezone} son las 14:30"
tools = [get_weather, get_time]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
def run_loop(question: str) -> str:
messages = [HumanMessage(content=question)]
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response.content
for tc in response.tool_calls:
result = tool_map[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
final = model_with_tools.invoke(messages)
return final.content
print(run_loop("¿Qué clima hace en Lima?"))
# El clima en Lima es nublado, con una temperatura de 18°C.
print(run_loop("¿Qué hora es en UTC-5?"))
# En la zona horaria UTC-5 son las 14:30.
print(run_loop("¿Cuál es la capital de Perú?"))
# La capital de Perú es Lima.
Explicación: El tool_map permite despachar dinámicamente cualquier tool que el modelo pida. La tercera pregunta muestra que el modelo responde directamente cuando no necesita tools.
Ejercicio 3: Parallel tool calls (Medio)
Haz una pregunta que requiera dos tools simultáneamente (por ejemplo "¿Qué clima hace en Madrid y cuánto es 99 * 13?"). Imprime cuántos tool calls generó el modelo y muestra el resultado de cada uno antes de la respuesta final.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es soleado, 25°C"
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
tools = [get_weather, calculator]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
messages = [HumanMessage(content="¿Qué clima hace en Madrid y cuánto es 99 * 13?")]
response = model_with_tools.invoke(messages)
messages.append(response)
print(f"Tool calls generados: {len(response.tool_calls)}")
for i, tc in enumerate(response.tool_calls):
result = tool_map[tc["name"]].invoke(tc["args"])
print(f" [{i+1}] {tc['name']}({tc['args']}) → {result}")
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
final = model_with_tools.invoke(messages)
print(f"\nRespuesta final: {final.content}")
# Output:
# Tool calls generados: 2
# [1] get_weather({'city': 'Madrid'}) → El clima en Madrid es soleado, 25°C
# [2] calculator({'expression': '99 * 13'}) → 1287
#
# Respuesta final: El clima en Madrid es soleado, con 25°C. Y 99 × 13 = 1,287.
Explicación: El modelo generó dos tool calls en un solo turno — uno para clima y otro para el cálculo. El loop itera sobre ambos, ejecuta cada tool, y crea un ToolMessage para cada resultado. El modelo recibe ambos resultados y genera una respuesta integrada.
Ejercicio 4: Loop con manejo de errores (Medio)
Modifica el loop para manejar tres tipos de errores: tool no encontrada, excepción en la ejecución, y resultado vacío. Prueba cada caso con tools que fallen intencionalmente.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
if city.lower() == "atlantis":
raise ValueError("Ciudad no encontrada en la base de datos")
if city.lower() == "vacío":
return ""
return f"El clima en {city} es soleado, 22°C"
tools = [get_weather]
tool_map = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)
def safe_loop(question: str) -> str:
messages = [HumanMessage(content=question)]
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response.content
for tc in response.tool_calls:
if tc["name"] not in tool_map:
result = f"Error: la herramienta '{tc['name']}' no existe"
print(f" ⚠️ Tool no encontrada: {tc['name']}")
else:
try:
raw_result = tool_map[tc["name"]].invoke(tc["args"])
result = str(raw_result) if raw_result else "Sin resultado disponible"
if not result.strip():
result = "La herramienta retornó un resultado vacío"
print(f" ⚠️ Resultado vacío de {tc['name']}")
except Exception as e:
result = f"Error ejecutando {tc['name']}: {e}"
print(f" ⚠️ Error: {e}")
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
final = model_with_tools.invoke(messages)
return final.content
print("Test 1 — Ciudad válida:")
print(safe_loop("¿Qué clima hace en Madrid?"))
# Output: El clima en Madrid es soleado, con una temperatura de 22°C.
print("\nTest 2 — Ciudad inválida:")
print(safe_loop("¿Qué clima hace en Atlantis?"))
# Output:
# ⚠️ Error: Ciudad no encontrada en la base de datos
# Lo siento, no pude obtener el clima de Atlantis...
print("\nTest 3 — Resultado vacío:")
print(safe_loop("¿Qué clima hace en Vacío?"))
# Output:
# ⚠️ Resultado vacío de get_weather
# No se pudo obtener información del clima...
Explicación: El manejo de errores tiene tres capas: verificar que la tool existe, capturar excepciones de ejecución, y manejar resultados vacíos. En los tres casos, se envía un mensaje de error como ToolMessage para que el modelo genere una respuesta apropiada.
Resumen
En esta cápsula aprendiste:
- El tool execution loop es el ciclo:
HumanMessage → AIMessage(tool_calls) → ejecutar tools → ToolMessage → respuesta final ToolMessageempaqueta el resultado de una tool y requierecontent(string) ytool_call_id(del tool call original)- La lista de mensajes acumula todos los pasos — el modelo necesita ver la secuencia completa para generar la respuesta final
- El tool_map (
{name: tool_fn}) permite despachar dinámicamente cualquier tool que el modelo solicite - Parallel tool calls funcionan igual — simplemente iteras sobre todos los
tool_callsy creas unToolMessagepor cada uno - Múltiples rondas se manejan con un
whileloop que sigue ejecutando mientras el modelo pida más tools - Error handling es crítico — tools que fallan deben retornar un error como
ToolMessage, no crashear el loop - Loop manual vs agente: el manual te da control total; el agente (Módulo 3) lo automatiza
Próxima cápsula: Parallel Tool Calls y Streaming — aprenderás a manejar múltiples tool calls simultáneos con streaming, acumular tool call chunks, y construir respuestas progresivas cuando hay tools de por medio.
Recursos adicionales
- Tool Calling — LangChain Docs — Guía conceptual completa de tool calling
- How to call tools using ToolCall — Tutorial paso a paso del tool execution loop
- ToolMessage API Reference — Referencia completa de ToolMessage
- How to handle tool errors — Manejo de errores en tool calling
- ReAct Agent — LangGraph — El agente que automatiza el tool execution loop
- OpenAI Function Calling — Cómo funciona tool calling a nivel de API
- Messages — LangChain Docs — Tipos de mensajes: HumanMessage, AIMessage, ToolMessage
- How to pass tool results back to model — Guía de ToolMessage y resultados
Módulo 2 — LangChain & LangGraph: From Chains to Agents