🔁 AI Engineering

Build a ReAct Agent with LangGraph — The Tool-Calling Loop

📅 Aug 5, 2026 ⏱ 7 min read

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:

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