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
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")