The agent loop
Now put it together. The model asks for a tool, the tool runs, the answer goes back to the model, and round it goes until there is nothing left to ask for.
You already have every piece. A node that calls the model, from lesson 15. A conditional edge, from lesson 6. An edge pointing backwards, from lesson 7. The only new thing is a ready made node that runs the tools.
The tool, and a model that knows about it
from langchain_core.tools import tool
@tool
def lookup_order(order_id: str) -> str:
"""Look up the status of an order by its id."""
return f"Order {order_id} shipped on 3 March."from pretend_model import PretendModel
model = PretendModel().bind_tools([lookup_order])Two very short functions
The first is the node from lesson 15, unchanged.
def call_model(state):
return {"messages": [model.invoke(state["messages"])]}The second is the router from lesson 6, asking one question about the last message.
from langgraph.graph import END
def needs_a_tool(state):
return "tools" if state["messages"][-1].tool_calls else ENDIt looks at the message the model just produced. tool_calls is empty when the model replied with words and full when it asked for a tool, and that is the entire decision.
Wiring the loop
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode([lookup_order]))ToolNode is a node LangGraph gives you. It reads the requests off the last message, calls the matching functions, and returns their answers as tool messages. You wrote its four line version by hand at the end of lesson 17.
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", needs_a_tool, ["tools", END])
builder.add_edge("tools", "call_model")After the model speaks, either run the tools or stop. Then the last line sends the run back to the model, and that one line is what makes this an agent rather than a script. Without it the run would end holding a tool answer nobody read.
Run it and read all four messages
from langchain_core.messages import HumanMessage
result = builder.compile().invoke({"messages": [HumanMessage("Where is order A17?")]})
for m in result["messages"]:
print(f"{m.type:<6} {m.content!r}")Line one is what you asked.
Line two is the one to stare at. An assistant message with no text in it at all. When a model wants a tool it produces no words, only a request. Everything an agent framework does is arranged around that moment.
Line three is your function's answer, carried back as a tool message. That is the fourth message type from lesson 12, and now there is a tool to produce it.
Line four is the model again, having seen the answer, replying properly. Then needs_a_tool found no request and sent the run to END.
- Add a second tool, ask something that uses it, and watch the loop go round again.
- Remove the
toolstocall_modeledge and see the run stop early. - Print the graph's edges and find the loop.
Slow is fine. Stopping is the only problem.