Back to Writing From Basic Tool Calling to Advanced ReAct Agents: A Complete Implementation Guide

From Basic Tool Calling to Advanced ReAct Agents: A Complete Implementation Guide

The landscape of AI agents has evolved dramatically, with tool calling capabilities transforming how Large Language Models (LLMs) interact with external systems. But what if I told you that understanding this evolution—from basic tool calling to sophisticated ReAct agents—could unlock the next level of your AI development journey?

Today, we're diving deep into this progression, moving systematically from fundamental concepts to building production-ready intelligent agents that can reason, act, and adapt. This tutorial is based on practical examples from our interactive Jupyter notebook that you can run and experiment with.

Contents

Understanding the Foundation: Tool Calling

Before we dive into implementations, let's establish why tool calling represents such a paradigm shift in AI development. Traditional LLMs are limited to their training data and cannot interact with real-time information or external systems. Tool calling bridges this gap, enabling models to:

  • Access live data through APIs and databases
  • Perform computations beyond their training scope
  • Execute actions in external environments
  • Maintain context across multi-step workflows

The key insight is that tool calling transforms LLMs from static knowledge repositories into dynamic, interactive agents capable of solving complex, real-world problems.

Basic Tool Calling with OpenAI

Let's start with the fundamentals. Here's how to implement basic tool calling using OpenAI's API:

Step 1: Define Your Tool

from typing import Union

def calculator(operation: str, a: float, b: float) -> Union[float, str]:
    """A simple calculator function"""
    if operation == "add":
        return a + b
    elif operation == "subtract":
        return a - b
    elif operation == "multiply":
        return a * b
    elif operation == "divide":
        if b != 0:
            return a / b
        else:
            return "Error: Division by zero"
    else:
        return "Error: Unsupported operation"

Step 2: Create the Tool Schema

The LLM needs to understand what tools are available and how to use them:

tool_schema = {
    "type": "function",
    "function": {
        "name": "calculator",
        "description": "Perform basic arithmetic operations (add, subtract, multiply, divide)",
        "parameters": {
            "type": "object",
            "properties": {
                "operation": {
                    "type": "string",
                    "enum": ["add", "subtract", "multiply", "divide"],
                    "description": "The arithmetic operation to perform"
                },
                "a": {
                    "type": "number",
                    "description": "The first number"
                },
                "b": {
                    "type": "number",
                    "description": "The second number"
                }
            },
            "required": ["operation", "a", "b"]
        }
    }
}

Step 3: Implement the Tool Calling Flow

import json
from openai import OpenAI

client = OpenAI()

def call_llm_with_tools(user_message: str):
    # Send message to LLM with tool available
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": user_message}],
        tools=[tool_schema],
        tool_choice="auto"
    )

    return response

def execute_tool_calls(response):
    """Execute tool calls from LLM response"""
    message = response.choices[0].message

    if message.tool_calls:
        for tool_call in message.tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            if function_name == "calculator":
                result = calculator(
                    function_args["operation"],
                    function_args["a"],
                    function_args["b"]
                )
                return result
    else:
        return message.content

# Usage example
user_query = "What is 25 + 17?"
response = call_llm_with_tools(user_query)
print(f"LLM Response:{response}")
result = execute_tool_calls(response)
print(f"Final Answer: {result}")

Live Output:

👤 User: What is 25 + 17?

🤖 LLM Response: ChatCompletionMessage(
    content=None,
    tool_calls=[
        ChatCompletionMessageToolCall(
            id='call_cM7FTqTUCbPRHSdLUMnDkzx8',
            function=Function(arguments='{"operation":"add","a":25,"b":17}', name='calculator'),
            type='function'
        )
    ]
)

🔧 LLM decided to use a tool!
🛠️ Tool: calculator
📥 Arguments: {'operation': 'add', 'a': 25, 'b': 17}
📊 Result: 42

✅ Final Answer: 42

This basic implementation demonstrates the core pattern: the LLM analyzes the user query, determines that it needs to use a tool, formats the appropriate function call, and we execute it to get the result.

Scaling with LangChain

While the basic approach works, LangChain provides powerful abstractions that make tool calling more robust and scalable. LangChain is a framework for developing applications powered by language models, with extensive tool calling capabilities.

Enhanced Tool Definition

from langchain.tools import tool

@tool
def calculator_tool(operation: str, a: float, b: float) -> Union[float, str]:
    """🧮 Perform basic arithmetic operations (add, subtract, multiply, divide).

    Args:
        operation: The operation to perform (add, subtract, multiply, divide)
        a: First number
        b: Second number

    Returns:
        The result of the arithmetic operation
    """
    # Same implementation as before
    pass

@tool
def get_word_length(word: str) -> int:
    """📏 Get the length of a word.

    Args:
        word: The word to count characters for

    Returns:
        Number of characters in the word
    """
    return len(word)

LangChain Tool Inspection:

# Let's see what LangChain created for us
print("🧮 Calculator tool:")
print(f"Name: {calculator_tool.name}")
print(f"Description: {calculator_tool.description}")

print("\n📏 Word length tool:")
print(f"Name: {get_word_length.name}")
print(f"Description: {get_word_length.description}")

# Output:
# 🧮 Calculator tool:
# Name: calculator_tool
# Description: 🧮 Perform basic arithmetic operations (add, subtract, multiply, divide).
#
# 📏 Word length tool:
# Name: get_word_length
# Description: 📏 Get the length of a word.

Simplified LLM Integration

from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage

# Initialize LLM and bind tools
llm = ChatOpenAI(model="gpt-4o-mini")
tools = [calculator_tool, get_word_length]
llm_with_tools = llm.bind_tools(tools)

# Simple usage
response = llm_with_tools.invoke([HumanMessage(content="What is 20 divided by 4?")])
print(f"Tool calls: {len(response.tool_calls)}")

Building an Agent with LangChain

For more complex scenarios, LangChain provides pre-built agents:

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# Create prompt template
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant that can use tools to answer questions."),
    MessagesPlaceholder(variable_name="chat_history", optional=True),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

# Create and execute agent
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({
    "input": "Calculate 15 * 7, then tell me how many letters are in 'calculator'"
})

Live Output:

> Entering new AgentExecutor chain...

Invoking: `calculator_tool` with `{'operation': 'multiply', 'a': 15, 'b': 7}`

105.0

Invoking: `get_word_length` with `{'word': 'calculator'}`

10
The result of \(15 \times 7\) is \(105\). The word "calculator" has \(10\) letters.

> Finished chain.

This agent automatically handles the tool calling loop, executing multiple tools in sequence to solve complex queries.

Advanced Workflows with LangGraph

LangGraph takes us to the next level by providing fine-grained control over agent workflows with state management. LangGraph is a library for building stateful, multi-actor applications with LLMs, designed as an extension of LangChain. Check out the comprehensive tutorials to learn more about building workflows and agents.

Setting Up LangGraph

from langgraph.graph import StateGraph, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver

# Define state management
class AgentState(MessagesState):
    # MessagesState includes 'messages' field automatically
    pass

# Create the workflow
workflow = StateGraph(AgentState)

# Add nodes
def call_model(state: AgentState):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

tool_node = ToolNode(tools)
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)

# Configure flow
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", tools_condition)
workflow.add_edge("tools", "agent")

# Compile with memory
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

Advanced State Management

def run_langgraph_agent(user_input: str, thread_id: str = "default"):
    config = {"configurable": {"thread_id": thread_id}}

    for event in app.stream(
        {"messages": [HumanMessage(content=user_input)]},
        config=config,
        stream_mode="values"
    ):
        if "messages" in event and event["messages"]:
            latest_message = event["messages"][-1]
            if hasattr(latest_message, 'content') and latest_message.content:
                if isinstance(latest_message, AIMessage):
                    print(f"Agent: {latest_message.content}")

# Usage with conversation memory
# Usage with conversation memory
run_langgraph_agent("Calculate 12 * 8")
run_langgraph_agent("Now add 50 to that result", "default")  # Remembers context

Live Output:

👤 User: Calculate 12 * 8
--------------------------------------------------
🤖 Agent thinking... (processing 1 messages)
🤖 Agent thinking... (processing 3 messages)
🤖 Agent: The result of \( 12 	imes 8 \) is 96.

==================================================

👤 User: Now add 50 to that result
--------------------------------------------------
🤖 Agent thinking... (processing 5 messages)
🤖 Agent thinking... (processing 7 messages)
🤖 Agent: Adding 50 to the previous result gives you \( 96 + 50 = 146 \).

==================================================

👤 User: How many characters are in the word 'LangGraph'?
--------------------------------------------------
🤖 Agent thinking... (processing 9 messages)
🤖 Agent thinking... (processing 11 messages)
🤖 Agent: The word "LangGraph" has 9 characters.

Notice how the agent remembers the previous calculation result (96) when processing the follow-up question. This is the power of LangGraph's state management!


## The ReAct Revolution

ReAct (Reasoning and Acting) represents the current frontier of agent development. Introduced by researchers at Google and Princeton in their groundbreaking 2022 paper "[ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629)", ReAct enables language models to generate both verbal reasoning traces and actions in an interleaved manner.

You can explore the original research and code at the [official ReAct project site](https://react-lm.github.io/).

### What Makes ReAct Special?

1. **Transparent Reasoning**: The agent shows its thought process
2. **Dynamic Planning**: Can adjust strategies based on observations
3. **Error Recovery**: Can detect and correct mistakes in real-time
4. **Human Interpretability**: Easy to understand and debug

### Core ReAct Pattern

Thought: I need to solve this step by step Action: Use calculator to multiply 15 by 7 Observation: The result is 105 Thought: Now I need to analyze the word "calculator" Action: Use word length tool on "calculator" Observation: The word has 10 characters Thought: I have all the information needed Final Answer: 15 × 7 = 105, and "calculator" has 10 characters


## Implementation Deep Dive

Let's build a comprehensive ReAct agent with enhanced tools:

### Enhanced Tool Suite

```python
import datetime
import random

@tool
def get_current_time() -> str:
    """🕐 Get the current date and time."""
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

@tool
def text_analyzer(text: str) -> dict:
    """📊 Analyze text and return various statistics."""
    words = text.split()
    sentences = text.split('.') if '.' in text else [text]

    return {
        "character_count": len(text),
        "word_count": len(words),
        "sentence_count": len([s for s in sentences if s.strip()]),
        "average_word_length": sum(len(word) for word in words) / len(words) if words else 0
    }

@tool
def generate_random_number(min_value: int = 1, max_value: int = 100) -> int:
    """🎲 Generate a random number between min_value and max_value."""
    return random.randint(min_value, max_value)

Enhanced Tools Summary:

# Enhanced ReAct agent with comprehensive tooling
react_tools = [
    calculator_tool,
    get_word_length,
    get_current_time,
    generate_random_number,
    text_analyzer
]

print(f"✅ Created {len(react_tools)} tools for ReAct agent:")
for tool in react_tools:
    print(f"  - {tool.name}: {tool.description.split('.')[0]}")

# Output:
# ✅ Created 5 tools for ReAct agent:
#   - calculator_tool: 🧮 Perform basic arithmetic operations (add, subtract, multiply, divide)
#   - get_word_length: 📏 Get the length of a word
#   - get_current_time: 🕐 Get the current date and time
#   - generate_random_number: 🎲 Generate a random number between min_value and max_value
#   - text_analyzer: 📊 Analyze text and return various statistics

Complete ReAct Implementation

# Enhanced ReAct agent with comprehensive tooling
react_tools = [
    calculator_tool,
    get_word_length,
    get_current_time,
    generate_random_number,
    text_analyzer
]

# Update LLM with enhanced tools
llm_with_react_tools = llm.bind_tools(react_tools)

# Create ReAct workflow
react_workflow = StateGraph(AgentState)

def react_agent_node(state: AgentState):
    response = llm_with_react_tools.invoke(state["messages"])
    return {"messages": [response]}

react_tool_node = ToolNode(react_tools)
react_workflow.add_node("agent", react_agent_node)
react_workflow.add_node("tools", react_tool_node)

react_workflow.set_entry_point("agent")
react_workflow.add_conditional_edges("agent", tools_condition)
react_workflow.add_edge("tools", "agent")

react_app = react_workflow.compile(checkpointer=MemorySaver())

# Test complex multi-step reasoning
complex_query = """I need you to:
1. Generate a random number between 10 and 50
2. Multiply that number by 3
3. Tell me what time it is
4. Analyze the text "The quick brown fox jumps over the lazy dog"
"""

result = react_app.invoke({
    "messages": [HumanMessage(content=complex_query)]
}, config={"configurable": {"thread_id": "complex_task"}})

Live ReAct Agent Output:

🎯 Question: I need you to:
1. Generate a random number between 10 and 50
2. Multiply that number by 3
3. Tell me what time it is
4. Analyze the text "The quick brown fox jumps over the lazy dog"

📝 ReAct Agent Reasoning Process:
============================================================

✅ Final Answer:
1. The random number generated between 10 and 50 is **39**.
2. When multiplied by 3, the result is **117**.
3. The current time is **2025-07-25 23:43:18**.
4. The analysis of the text "The quick brown fox jumps over the lazy dog" shows:
   - Character count: **43**
   - Word count: **9**
   - Sentence count: **1**
   - Average word length: **3.89**

**Final Answer**:
- Random number: 39
- Multiplied by 3: 117
- Current time: 2025-07-25 23:43:18
- Text analysis: 43 characters, 9 words, 1 sentence, average word length 3.89.

This showcases the ReAct agent's ability to execute multiple complex operations simultaneously while maintaining clear reasoning about each step.

Production Considerations

When deploying ReAct agents in production, consider these critical factors:

Error Handling and Resilience

@tool
def robust_calculator(operation: str, a: float, b: float) -> dict:
    """Enhanced calculator with comprehensive error handling."""
    try:
        if operation == "add":
            result = a + b
        elif operation == "subtract":
            result = a - b
        elif operation == "multiply":
            result = a * b
        elif operation == "divide":
            if b == 0:
                return {"error": "Division by zero", "success": False}
            result = a / b
        else:
            return {"error": f"Unsupported operation: {operation}", "success": False}

        return {"result": result, "success": True}
    except Exception as e:
        return {"error": str(e), "success": False}

Performance Monitoring

import time
from functools import wraps

def monitor_tool_usage(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        try:
            result = func(*args, **kwargs)
            execution_time = time.time() - start_time
            print(f"Tool {func.__name__} executed in {execution_time:.2f}s")
            return result
        except Exception as e:
            execution_time = time.time() - start_time
            print(f"Tool {func.__name__} failed after {execution_time:.2f}s: {e}")
            raise
    return wrapper

@tool
@monitor_tool_usage
def monitored_calculator(operation: str, a: float, b: float) -> float:
    """Calculator with built-in monitoring."""
    # Implementation here
    pass

Security and Validation

from pydantic import validator

@tool
def secure_text_analyzer(text: str) -> dict:
    """Secure text analyzer with input validation."""
    # Input validation
    if len(text) > 10000:
        raise ValueError("Text too long (max 10,000 characters)")

    if not text.strip():
        raise ValueError("Empty text provided")

    # Sanitize input (remove potential harmful content)
    sanitized_text = text.replace('\x00', '').strip()

    # Proceed with analysis
    return text_analyzer(sanitized_text)

What You've Built

Through this hands-on tutorial, you've experienced the complete evolution of AI agent development:

🔧 Basic Tool Calling Mastery

  • Built a foundation calculator that responds to natural language
  • Watched the LLM parse user intent and automatically generate function calls
  • Saw real-time tool execution with 42 as our first successful result

🦜 LangChain Integration

  • Streamlined tool definitions with the @tool decorator
  • Experienced automatic tool calling loops that handle complex multi-step operations
  • Witnessed the agent calculate 15 × 7 = 105 and count letters in "calculator" (10) sequentially

📊 LangGraph State Management

  • Built stateful conversations that remember context across interactions
  • Saw the agent remember previous calculation results (96) when asked to add 50
  • Experienced the power of persistent memory in agent workflows

🎯 ReAct Agent Intelligence

  • Created an agent that shows its reasoning process transparently
  • Executed complex multi-tool operations:
    • Generated random number: 39
    • Multiplied by 3: 117
    • Retrieved current time: 2025-07-25 23:43:18
    • Analyzed text: 43 characters, 9 words, 1 sentence

What's Next?

The evolution from basic tool calling to ReAct agents represents just the beginning of a broader transformation in AI development. Here are the emerging trends to watch:

1. Multi-Modal Agents

Future agents will seamlessly integrate text, images, audio, and video processing, enabling richer interactions with the world.

2. Collaborative Agent Networks

Multiple specialized agents working together to solve complex problems, each contributing their unique capabilities.

3. Autonomous Learning Agents

Agents that can improve their performance through experience, updating their strategies based on successful interactions.

4. Integration with Knowledge Graphs

Deep integration with structured knowledge representations for more accurate and contextual reasoning.

5. Advanced Planning Algorithms

Moving beyond reactive patterns to proactive planning with look-ahead capabilities and scenario modeling.

Key Takeaways

Building effective AI agents requires understanding the progression from basic tool calling to sophisticated reasoning systems:

  1. Start Simple: Master basic tool calling patterns before moving to complex frameworks
  2. Choose the Right Abstraction: LangChain for rapid development, LangGraph for complex workflows
  3. Embrace ReAct: The reasoning-action pattern provides transparency and robustness
  4. Plan for Production: Consider error handling, monitoring, and security from day one
  5. Stay Current: This field evolves rapidly—continuous learning is essential

The journey from basic tool calling to ReAct agents isn't just about learning new APIs or frameworks. It's about understanding how to build AI systems that can truly think, act, and adapt in our complex world.

Whether you're building customer service bots, data analysis assistants, or complex workflow automation, these patterns and principles will serve as your foundation for creating intelligent, reliable, and scalable AI agents.

Your Next Steps

The journey doesn't end here. Take your newfound knowledge and push the boundaries:

🚀 Immediate Experiments

  1. Clone and Run: Get the interactive notebook running locally
  2. Add Your Tools: Create tools specific to your domain (database queries, API calls, file operations)
  3. Test Edge Cases: See how your agents handle errors, unexpected inputs, and complex reasoning chains

🔧 Advanced Challenges

  • Build a multi-agent system where different ReAct agents specialize in different domains
  • Implement persistent memory that survives beyond single sessions
  • Create tools that can modify other tools dynamically
  • Design agents that can learn from their mistakes and improve over time

🎯 Production Ready

  • Add comprehensive error handling and fallback mechanisms
  • Implement user authentication and permission systems for sensitive tools
  • Monitor agent behavior and tool usage patterns
  • Create automated testing suites for your agent workflows

Ready to build your first ReAct agent? Start with the basic examples in this post, then gradually add complexity as you master each layer. The future of AI development is in your hands.

Additional Resources