agente → (condicional) → tools → agente. La arista de vuelta
es la pieza que convierte un cálculo en una respuesta: sin ella el grafo termina
con el resultado bruto y sin nadie que lo interprete.
from langchain_core.tools import tool
@tool
def multiplicar(a: int, b: int) -> int:
"""Multiplica dos números enteros."""
return a * b
tools = [multiplicar]
llm_con_herramientas = llm.bind_tools(tools)
bind_tools no ejecuta nada. Serializa el nombre, la firma tipada y el
docstring de cada función y los manda al modelo en cada
petición. El modelo no ve tu código: ve esa descripción.
El modelo responde con un AIMessage que puede llevar
tool_calls: una lista de peticiones con nombre y argumentos. Sigue siendo
texto generado — nada se ha ejecutado todavía.
from langgraph.prebuilt import ToolNode
nodo_herramientas = ToolNode(tools)
Lee el último mensaje del estado, busca cada tool_call, llama a la
función que corresponde y devuelve un ToolMessage por llamada, con
el tool_call_id correcto. Ese emparejamiento por id es lo que permite al
modelo saber qué resultado responde a qué petición cuando ha pedido
varias a la vez.
La lista que pasas a ToolNode y la que pasas a bind_tools
tienen que ser la misma. Si divergen, el modelo pide una herramienta que el nodo no
conoce y revienta en ejecución, no al compilar.
def router_herramientas(state: MessagesState) -> str:
ultimo = state["messages"][-1]
if getattr(ultimo, "tool_calls", None):
return "ir_a_herramientas"
return "terminar"
LangGraph trae este router hecho:
from langgraph.prebuilt import tools_condition. Devuelve
"tools" o END, así que si nombras "tools" a
tu nodo puedes pasarlo directamente sin mapa. Escribirlo a mano solo compensa cuando
necesitas una tercera rama — por ejemplo, un tope de iteraciones.
builder.add_conditional_edges("agente", router_herramientas,
{"ir_a_herramientas": "tools", "terminar": END})
builder.add_edge("tools", "agente") # <- sin esto, el grafo acaba tras calcular
Es incondicional a propósito: después de ejecutar herramientas
siempre se vuelve al modelo, y es él quien decide si ya puede
responder o si necesita otra ronda. Ahí está el ciclo, y ahí
está el riesgo: un modelo que insista en llamar herramientas gira hasta el
recursion_limit. El contador de iteraciones de la lección anterior
aplica igual aquí.
El troubleshooting de la guía recomienda «modelos capaces de Tool Calling
(GPT-3.5-Turbo o superior)». Comprueba dos cosas antes de darlo por bueno: si
gpt-3.5-turbo sigue disponible en la API que estás usando, y si
«superior» significa algo técnico o solo más caro.
La guía accede además a ultimo_mensaje.tool_calls sin
comprobar el tipo del mensaje, mientras el enunciado del reto del módulo usa
hasattr(...) para exactamente lo mismo. ¿Por qué el propio
material se protege en un sitio y no en el otro? ¿Qué mensaje puede
quedar el último del historial y no tener ese atributo?
Bórrale el docstring a @tool def multiplicar y vuelve a preguntar algo que
requiera multiplicar. Anota si el modelo llama a la herramienta, si la llama con
argumentos equivocados, o si responde a mano.
Después comenta la línea builder.add_edge("tools", "agente") y
ejecuta el mismo caso. Mira qué hay en
resultado["messages"][-1] y responde: ¿el grafo ha fallado, o ha
terminado correctamente en un sitio que no te sirve?