Building a ReAct Agent with Tools in LangGraph

Learn how to build a stateful ReAct agent in LangGraph. Define custom tools for weather and math calculations, bind them to an LLM, and loop executions.

Jun 15, 202618 min readFollow

Topics You Will Master

Creating custom tools with parameter schemas in LangChain
Declaring state schemas that accumulate message history
Binding tool definitions to chat models
Implementing the ReAct (Reasoning + Acting) execution pattern

In linear chains and conditional routers are useful, but true agentic behaviors require loop cycles. The ReAct pattern (Reasoning + Acting) is a classic agent design where the LLM decides which action to take (by calling a tool), observes the tool's result, and repeats this cycle until it has collected enough information to answer the user's query.

This guide covers building a stateful ReAct agent in LangGraph. We will define custom tools for retrieving weather data and evaluating math expressions, bind them to a local Qwen 3 model, and compose a cyclic graph that routes between the agent and tools dynamically.

Before starting, ensure you have basic graph routing experience. Refer to Conditional Routing in LangGraph Workflows as a prerequisite.

Master LangGraph and LangChain

Agentic RAG and Chatbot, AI Agent with LangChain v1, Qwen3, Gemma3, DeepSeek-R1, LLAMA 3.2, FAISS Vector Database

Enroll on Udemy →

Environment and Model Setup

First, pull the qwen3 model locally via Ollama:

POWERSHELL
ollama pull qwen3

On Linux/macOS:

BASH
ollama pull qwen3

Note

Tool binding requires models with native function-calling support. At this time, Qwen 3 (or Llama 3.2) is recommended for local tool calling, as reasoning models like DeepSeek-R1 or smaller Gemma 3 variants may not support structured tool binding in Ollama.

Import the required classes to structure graph states and messages:

PYTHON
from typing_extensions import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.prebuilt import ToolNode

# Configuration
BASE_URL = "http://localhost:11434"
MODEL_NAME = "qwen3"

llm = ChatOllama(model=MODEL_NAME, base_url=BASE_URL)

Next, initialize the environment by loading variable configurations:

PYTHON
from dotenv import load_dotenv

load_dotenv()
OUTPUT
True

Diagram showing the ReAct loop: START to agent, the agent looping through tools and back, then ending when done

Defining Custom Tools

Tools are Python functions decorated with @tool from langchain_core.tools. The function's docstring and argument type hints are parsed as the parameter schema that the LLM uses to determine when and how to call the tool.

Diagram showing @tool parsing a function's docstring and type hints into a callable tool schema

Create a centralized tools module my_tools.py containing a weather retrieval tool and a math expression calculator:

PYTHON
from langchain_core.tools import tool
import requests

@tool
def get_weather(location: str) -> str:
    """Get current weather for a location.
    
    Use for queries about weather, temperature, or conditions in any city.
    Examples: "weather in Paris", "temperature in Tokyo", "is it raining in London"
    
    Args:
        location: City name (e.g., "New York", "London", "Tokyo")
    """
    url = f"https://wttr.in/{location}?format=j1"
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.json()

@tool
def calculate(expression: str) -> str:
    """Calculate a mathematical expression.
    
    USE THIS TOOL FOR:
    - Any mathematical calculations or arithmetic operations
    - Queries involving numbers and operators (+, -, *, /, **, %)
    - Evaluating mathematical expressions
    
    Args:
        expression: Math expression like "2 + 2" or "15 * 7" (use standard Python operators)
    """
    try:
        result = eval(expression)
        print(f"[TOOL] calculate ('{expression}') -> '{result}'")
        return result
    except Exception as e:
        return f"Exception has occurred with error: {e}"

Import and register the tools:

PYTHON
import my_tools

all_tools = [my_tools.get_weather, my_tools.calculate]

You can test tools programmatically using .invoke():

PYTHON
my_tools.calculate.invoke({'expression': '2+2*1.4/23-34'})
OUTPUT
[TOOL] calculate ('2+2*1.4/23-34') -> '-31.878260869565217'

Declaring the Agent State

For a cyclic ReAct agent, the state needs to preserve a list of messages. To prevent successive node executions from overwriting the message history, use Annotated with operator.add to specify that state updates must be appended.

Diagram showing operator.add appending each new message so the full reasoning trace is preserved

PYTHON
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]

Creating the Agent Node

The agent node is a function that binds our tools to the LLM using llm.bind_tools(). It then invokes the model and prints tool call traces to make the reasoning steps transparent.

Diagram showing the tools bound to the model so the agent emits either tool calls or a final answer

PYTHON
def agent_node(state: AgentState):
    llm_with_tools = llm.bind_tools(all_tools)
    messages = state['messages']
    response = llm_with_tools.invoke(messages)

    if hasattr(response, 'tool_calls') and response.tool_calls:
        for tc in response.tool_calls:
            print(f"[AGENT] called Tool {tc.get('name', '?')} with args {tc.get('args', '?')}")
    else:
        print(f"[AGENT] Responding...")

    return {'messages': [response]}

Test the agent node in isolation with a simple greeting:

PYTHON
state = {"messages": [HumanMessage("Hi")]}
result = agent_node(state)
OUTPUT
[AGENT] Responding...

Inspect the node response dictionary:

PLAINTEXT
result
PLAINTEXT
{'messages': [AIMessage(content='Hello! How can I assist you today? 😊', additional_kwargs={}, response_metadata={'model': 'qwen3', 'created_at': '2025-11-05T07:39:40.6214629Z', 'done': True, 'done_reason': 'stop', 'total_duration': 4755028300, 'load_duration': 3235384500, 'prompt_eval_count': 457, 'prompt_eval_duration': 99282700, 'eval_count': 98, 'eval_duration': 1394561700, 'model_name': 'qwen3', 'model_provider': 'ollama'}, id='lc_run--ae4a46fa-01ec-4002-aa08-2ef9c31e85e1-0', usage_metadata={'input_tokens': 457, 'output_tokens': 98, 'total_tokens': 555})]}

If we pass a question requiring a calculation, the agent returns a tool call instead of content:

PYTHON
state = {"messages": [HumanMessage("Hi, what is 2+2?")]}
result = agent_node(state)
result
OUTPUT
[AGENT] called Tool calculate with args {'expression': '2 + 2'}
{'messages': [AIMessage(content='', additional_kwargs={}, response_metadata={'model': 'qwen3', 'created_at': '2025-11-05T07:39:44.4358572Z', 'done': True, 'done_reason': 'stop', 'total_duration': 2323922800, 'load_duration': 70265300, 'prompt_eval_count': 465, 'prompt_eval_duration': 50755600, 'eval_count': 155, 'eval_duration': 2173851600, 'model_name': 'qwen3', 'model_provider': 'ollama'}, id='lc_run--4195f707-661e-4e4f-9717-1eb7fe747a97-0', tool_calls=[{'name': 'calculate', 'args': {'expression': '2 + 2'}, 'id': '51147d12-bbf3-4baa-a2cf-f996409877d8', 'type': 'tool_call'}], usage_metadata={'input_tokens': 465, 'output_tokens': 155, 'total_tokens': 620})]}

Implementing Routing Logic

The conditional router function should_continue checks the last message in the state. If the agent requested tool calls, the workflow routes to "tools". Otherwise, it routes to END to complete the execution.

PYTHON
def should_continue(state: AgentState):
    last = state['messages'][-1]
    if hasattr(last, 'tool_calls') and last.tool_calls:
        return "tools"
    else:
        return END

Composing the ReAct Agent Graph

We define a cyclic graph using StateGraph. We add our agent_node as "agent", and register the prebuilt ToolNode(all_tools) as "tools".

The edges define a circular loop:

  1. Start at the "agent".
  2. Evaluate should_continue.
  3. If it returns "tools", transition to the "tools" node, which executes the requested tool and automatically returns to "agent".
  4. If it returns END, terminate execution.
PYTHON
def create_agent():
    builder = StateGraph(AgentState)

    # Add nodes
    builder.add_node("agent", agent_node)
    builder.add_node("tools", ToolNode(all_tools))

    # Define flow
    builder.add_edge(START, "agent")
    builder.add_conditional_edges("agent", should_continue, ["tools", END])
    builder.add_edge("tools", "agent")

    # Compile the graph
    graph = builder.compile()
    return graph

Instantiate the Compiled StateGraph:

PYTHON
agent = create_agent()
agent
OUTPUT
<langgraph.graph.state.CompiledStateGraph object at 0x0000015338E1FF20>

Invoking the Agent

Now, run the agent with user queries to verify its autonomous decision loops.

Query 1: Single Tool Query

PYTHON
query = "What is the current weather in Mumbai?"
result = agent.invoke({'messages': [HumanMessage(query)]})
OUTPUT
[AGENT] called Tool get_weather with args {'location': 'Mumbai'}
[AGENT] Responding...

Inspect the accumulated trace history and final response content:

PYTHON
result['messages'][-1].pretty_print()
OUTPUT
================================== Ai Message ==================================

The current weather in Mumbai is **clear skies** with a temperature of **26°C (79°F)**. The feels-like temperature is **28°C (82°F)**, and the humidity is at **64%**. Wind is coming from the **WNW** at **7 km/h (4 mph)**. The UV index is low at **0**, indicating minimal sun exposure. 

For the next few days, there's a chance of patchy rain and light showers, but today's weather remains sunny with occasional clouds.

Query 2: Compound Multi-Tool Query

When queried with multiple instructions, the agent executes multiple tools sequentially before formulating the final answer:

PYTHON
query = "What is the current weather in Mumbai? and What is 4*56 and 3-90"
result = agent.invoke({'messages': [HumanMessage(query)]})
OUTPUT
[AGENT] called Tool get_weather with args {'location': 'Mumbai'}
[AGENT] called Tool calculate with args {'expression': '4*56'}
[AGENT] called Tool calculate with args {'expression': '3-90'}
[TOOL] calculate ('4*56') -> '224'
[TOOL] calculate ('3-90') -> '-87'
[AGENT] Responding...

Verify the final combined output text:

PYTHON
result['messages'][-1].pretty_print()
OUTPUT
================================== Ai Message ==================================

The current weather in Mumbai is **30°C** with **Smoke** conditions. 

- **4 × 56 = 224**  
- **3 − 90 = -87**  

Let me know if you need further details! 🌦️🧮

Found this useful? Keep building with me.

New tutorials every week on YouTube — or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments