Tool Calling Agent vs ReAct Agent
Compare LangChain's tool calling agent and ReAct agent. Understand the underlying mechanics, when to use each, and how to configure parallel tool calls.
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
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
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):
// 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:
@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 resultsParallel tool calls in the API response:
# 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 sumComparison: 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
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")Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.