Learnixo

LangChain Mastery · Lesson 22 of 33

Tool Calling Agent vs ReAct Agent

Two Agent Paradigms

LangChain supports two primary agent paradigms that differ fundamentally in how the LLM decides to use tools:

ReAct Agent: The LLM outputs formatted text (Thought/Action/Action Input/Observation) and a parser extracts the tool calls.

Tool Calling Agent: The model API's native function/tool calling feature is used. The model returns structured JSON for tool calls, not text.


ReAct Agent: Text-Based Tool Calling

Python
from langchain import hub
from langchain.agents import create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def search_drug_database(query: str) -> str:
    """Search the clinical drug database. Use for drug information, dosing, and mechanisms."""
    return f"Database results for '{query}': [Clinical data would be here]"

@tool
def calculate_renal_dose(drug: str, egfr: float) -> str:
    """Calculate dose adjustment for renal impairment. Provide drug name and eGFR in mL/min."""
    if egfr >= 60:
        return f"{drug}: No dose adjustment needed (eGFR {egfr})"
    elif egfr >= 30:
        return f"{drug}: Reduce dose by 25-50% (eGFR {egfr})"
    else:
        return f"{drug}: Consider avoiding or reduce dose by 50-75% (eGFR {egfr})"

tools = [search_drug_database, calculate_renal_dose]
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# ReAct prompt from LangChain Hub
# (hwchase17/react defines the Thought/Action/Observation format)
react_prompt = hub.pull("hwchase17/react")

# Create ReAct agent
react_agent = create_react_agent(llm, tools, react_prompt)
react_executor = AgentExecutor(
    agent=react_agent,
    tools=tools,
    verbose=True,
    handle_parsing_errors=True,   # Important: malformed outputs cause errors without this
)

result = react_executor.invoke({
    "input": "What is the vancomycin dose for a patient with eGFR of 45 mL/min?"
})

What the LLM actually outputs (ReAct):

Thought: I need to find vancomycin dosing, then adjust for eGFR 45.
Action: search_drug_database
Action Input: vancomycin dosing
Observation: Vancomycin: Standard dose 15-20 mg/kg every 8-12 hours.

Thought: Now I need to calculate the renal adjustment for eGFR 45.
Action: calculate_renal_dose
Action Input: {"drug": "vancomycin", "egfr": 45.0}
Observation: vancomycin: Reduce dose by 25-50% (eGFR 45)

Thought: I have all the information needed.
Final Answer: For a patient with eGFR 45, the vancomycin dose should be reduced by 25-50%...

Tool Calling Agent: Structured JSON Tool Calls

Python
from langchain.agents import create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate

# Prompt structure for tool calling agent
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a clinical pharmacist. Use tools to answer drug questions precisely."),
    ("placeholder", "{chat_history}"),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),  # Tool calls accumulate here
])

# Create tool calling agent
tool_calling_agent = create_tool_calling_agent(llm, tools, prompt)
tc_executor = AgentExecutor(
    agent=tool_calling_agent,
    tools=tools,
    verbose=True,
)

result = tc_executor.invoke({
    "input": "What is the vancomycin dose for a patient with eGFR 45?",
    "chat_history": [],
})

What happens at the API level (tool calling):

JSON
// Model response (before parsing):
{
  "role": "assistant",
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "search_drug_database",
        "arguments": "{\"query\": \"vancomycin dosing standard\"}"
      }
    }
  ]
}

// Tool result appended:
{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "Vancomycin: 15-20 mg/kg every 8-12 hours"
}

// Model decides next step (second tool call):
{
  "role": "assistant", 
  "tool_calls": [
    {
      "function": {
        "name": "calculate_renal_dose",
        "arguments": "{\"drug\": \"vancomycin\", \"egfr\": 45.0}"
      }
    }
  ]
}

Parallel Tool Calls

Tool calling agents (not ReAct) support calling multiple tools simultaneously:

Python
@tool
def get_mechanism(drug: str) -> str:
    """Get the mechanism of action of a drug."""
    return f"{drug} mechanism: [mechanistic details]"

@tool
def get_side_effects(drug: str) -> str:
    """Get the main side effects of a drug."""
    return f"{drug} side effects: [adverse effects list]"

@tool
def get_contraindications(drug: str) -> str:
    """Get contraindications for a drug."""
    return f"{drug} contraindications: [contraindication list]"

parallel_tools = [get_mechanism, get_side_effects, get_contraindications]
parallel_agent = create_tool_calling_agent(
    ChatOpenAI(model="gpt-4o", temperature=0),
    parallel_tools,
    prompt,
)
parallel_executor = AgentExecutor(agent=parallel_agent, tools=parallel_tools, verbose=True)

# With a query asking for multiple things, the model may call all 3 tools simultaneously
result = parallel_executor.invoke({
    "input": "Give me the mechanism, side effects, and contraindications of metformin.",
    "chat_history": [],
})

# What happens:
# Model decides: "I need mechanism, side effects, AND contraindications simultaneously"
# Three tool calls are made in one API response (parallel execution)
# Results are collected
# Model generates comprehensive answer from all three results

Parallel tool calls in the API response:

Python
# Single API response with multiple tool calls:
response.tool_calls = [
    {"id": "call_1", "name": "get_mechanism", "args": {"drug": "metformin"}},
    {"id": "call_2", "name": "get_side_effects", "args": {"drug": "metformin"}},
    {"id": "call_3", "name": "get_contraindications", "args": {"drug": "metformin"}},
]
# All three executed concurrently
# Latency  max(tool1_time, tool2_time, tool3_time)  not sum

Comparison: ReAct vs Tool Calling

| Aspect | ReAct | Tool Calling | |---|---|---| | Mechanism | Text parsing (Thought/Action/Observation) | Native model API (JSON tool calls) | | Reliability | Lower — parser can fail on malformed output | Higher — structured JSON | | Parallel tools | No — sequential only | Yes — multiple tools per response | | Model support | Any model (even non-chat) | Requires tool-calling support (GPT-4, Claude, Gemini) | | Visibility | Full thought process visible in text | Less visible (JSON not as human-readable) | | Debugging | Easier — see LLM's reasoning text | Harder — reasoning is implicit | | Error handling | handle_parsing_errors=True needed | More robust | | Prompt sensitivity | High — depends on exact prompt format | Low — model handles structure |


When to Use Each

Use Tool Calling Agent when:

  • Model supports native tool calling (GPT-4o, Claude, Gemini)
  • You need parallel tool execution
  • Reliability is critical (production systems)
  • Complex tool schemas with Pydantic validation

Use ReAct Agent when:

  • Model doesn't support native tool calling
  • You want to see the explicit reasoning chain (debugging, audit)
  • Building on top of open-source models
  • Educational/research contexts

Recommended default: Tool calling agent with GPT-4o or Claude in production.


Forcing Tool Use

Python
from langchain_openai import ChatOpenAI

# Force the model to always call at least one tool
model = ChatOpenAI(model="gpt-4o")

# tool_choice="required": model MUST call a tool (prevents direct answer)
model_forced = model.bind_tools(tools, tool_choice="required")

# tool_choice={"name": "get_mechanism"}: force specific tool
model_specific = model.bind_tools(tools, tool_choice={"type": "function", "function": {"name": "get_mechanism"}})

# tool_choice="auto" (default): model decides whether to call tools
model_auto = model.bind_tools(tools, tool_choice="auto")

# tool_choice="none": disable tool calling
model_no_tools = model.bind_tools(tools, tool_choice="none")