Now the payoff. We'll build a tool-using agent by hand in LangGraph so you see the actual loop — the same loop hidden inside create_react_agent. Once you've built it once, agents stop being magic.
The shape of a ReAct agent
Two nodes and one decision:
- model node — the LLM thinks; it either answers or requests a tool.
- tools node — runs any tool the model asked for and returns the result.
- conditional edge — after the model: did it ask for a tool? If yes → go to tools, then loop back to the model. If no → we're done (END).
That backward loop (tools → model) is the whole trick. The model keeps calling tools until it has enough to answer.
Step 1: tools and a tool-aware model
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
@tool
def cgpa_to_percentage(cgpa: float) -> float:
"""Convert Anna University CGPA (0-10) to a percentage."""
return cgpa * 10
@tool
def gpa_needed(current_cgpa: float, target_cgpa: float, sems_done: int, sems_left: int) -> float:
"""GPA needed in remaining semesters to reach a target CGPA."""
total = target_cgpa * (sems_done + sems_left)
return round((total - current_cgpa * sems_done) / sems_left, 2)
tools = [cgpa_to_percentage, gpa_needed]
model = init_chat_model("claude-opus-5", model_provider="anthropic").bind_tools(tools)Step 2: nodes
LangGraph ships two helpers that save boilerplate: MessagesState (a ready-made state holding messages) and ToolNode (runs tool calls for you).
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
def call_model(state: MessagesState):
return {"messages": [model.invoke(state["messages"])]}
tool_node = ToolNode(tools)Step 3: wire the loop with a conditional edge
builder = StateGraph(MessagesState)
builder.add_node("model", call_model)
builder.add_node("tools", tool_node)
builder.add_edge(START, "model")
# after "model", tools_condition sends us to "tools" if a tool was
# requested, otherwise to END:
builder.add_conditional_edges("model", tools_condition)
builder.add_edge("tools", "model") # the loop back — key line
agent = builder.compile()tools_condition is a prebuilt router that inspects the last message: tool call present → "tools"; none → END. The tools → model edge feeds results back so the model can decide again.
Step 4: run the agent
result = agent.invoke({"messages": [{"role": "user",
"content": "My CGPA is 7.6 after 4 semesters. What GPA do I need in the "
"next 4 to reach 8.0, and what percentage is 8.0?"}]})
print(result["messages"][-1].content)Trace what happens: model asks for gpa_needed → tools run it → model asks for cgpa_to_percentage → tools run it → model writes a final answer. Multiple loops, all automatic, all visible.
You now understand every agent
Frameworks dress this up, but underneath, an agent is: model node + tool node + a conditional edge that loops until done. Add your own nodes (a validation step, a logging step, a human check) anywhere in that graph. The final post adds memory, human-in-the-loop approval, and multiple agents. Related: your first graph.
← All Articles