Agents: How LangChain Agents Work
Understand LangChain agent internals: the reasoning loop, thought-action-observation cycle, how tool calls work, and the difference between ReAct and tool-calling agents.
What is an Agent?
A chain is deterministic ā the same path every time. An agent is non-deterministic ā it decides at runtime which actions to take based on the query.
Chain: Query ā Step1 ā Step2 ā Step3 ā Answer (fixed)
Agent: Query ā Think ā Tool? ā Think ā Tool? ā Think ā Answer (adaptive)
ā________________________ā
loop until doneAn agent consists of:
- LLM (the "brain") ā decides what to do next
- Tools ā actions the LLM can take
- Reasoning loop ā runs until the LLM produces a final answer
The ReAct Pattern
ReAct (Reasoning + Acting) is the foundational agent pattern. The LLM alternates between:
- Thought: Reasoning about what to do
- Action: Calling a tool
- Observation: Receiving the tool's result
User: What is the warfarin dose for a patient with CYP2C9 *1/*3 variant?
Thought: I need to find warfarin dosing and how CYP2C9 *1/*3 affects it.
Action: search_drug_database({"drug": "warfarin", "topic": "pharmacogenomics"})
Observation: CYP2C9 *1/*3 is an intermediate metabolizer ā reduce standard dose by 30-40%.
Thought: I have the genetic info. Now I need the standard dose to calculate the adjustment.
Action: get_standard_dose({"drug": "warfarin", "indication": "anticoagulation"})
Observation: Standard dose: 2-10mg daily, INR-guided.
Thought: I can now answer: standard 2-10mg, but for *1/*3 intermediate metabolizer, reduce by 30-40%.
Final Answer: For CYP2C9 *1/*3 (intermediate metabolizer), start at 30-40% below the standard 2-10mg dose (approximately 1.5-6mg). Titrate based on INR monitoring.Tool Calling Agent vs ReAct Agent
LangChain provides two primary agent types:
ReAct Agent: Uses prompt engineering to get the LLM to output "Thought/Action/Observation" text, then parses it.
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def search_clinical_database(query: str) -> str:
"""Search the clinical pharmacology database for drug information."""
return f"Clinical data for: {query}"
@tool
def check_interactions(drug_a: str, drug_b: str) -> str:
"""Check for interactions between two drugs."""
return f"Interaction data for {drug_a} + {drug_b}"
tools = [search_clinical_database, check_interactions]
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ReAct uses a special prompt format
react_prompt = hub.pull("hwchase17/react")
react_agent = create_react_agent(llm, tools, react_prompt)
executor = AgentExecutor(agent=react_agent, tools=tools, verbose=True)
result = executor.invoke({"input": "What are the interactions between warfarin and aspirin?"})
print(result["output"])Tool Calling Agent: Uses the model's native function/tool calling API. More reliable, less prompt-sensitive.
from langchain.agents import create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
# Standard prompt for tool calling
prompt = ChatPromptTemplate.from_messages([
("system", "You are a clinical pharmacist. Use tools to answer drug questions accurately."),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
tool_calling_agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=tool_calling_agent, tools=tools, verbose=True)
result = executor.invoke({
"input": "What is the warfarin-aspirin interaction and how severe is it?",
"chat_history": [],
})The Reasoning Loop: Step by Step
# What AgentExecutor does internally:
def agent_loop(user_input: str, agent, tools_dict: dict, max_iterations: int = 10) -> str:
"""Simplified view of the agent reasoning loop."""
messages = [{"role": "user", "content": user_input}]
scratchpad = [] # Accumulates: tool calls + observations
for iteration in range(max_iterations):
# 1. LLM decides what to do next
response = agent.invoke({
"input": user_input,
"agent_scratchpad": scratchpad,
"chat_history": [],
})
# 2. Is the LLM done? Return final answer
if not response.tool_calls:
return response.content # Final answer
# 3. Execute each tool call
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
if tool_name not in tools_dict:
observation = f"Error: tool '{tool_name}' not found"
else:
observation = tools_dict[tool_name].invoke(tool_args)
# Add tool call + result to scratchpad
scratchpad.append(("tool_call", tool_name, tool_args))
scratchpad.append(("observation", tool_name, observation))
# Loop continues ā LLM sees scratchpad and decides next step
return "Max iterations reached ā unable to complete task"Agent Scratchpad
The agent_scratchpad placeholder in the prompt holds the accumulated tool calls and observations. This is how the agent "remembers" what it's done so far within a single run:
# What {agent_scratchpad} looks like at runtime:
"""
Tool calls made so far:
Tool: search_clinical_database
Input: {"query": "warfarin pharmacogenomics CYP2C9"}
Output: CYP2C9 *1/*3 reduces metabolism by 50%...
Tool: get_standard_dose
Input: {"drug": "warfarin"}
Output: Standard dose 2-10mg daily...
"""
# The LLM sees this and decides: "I have enough information. Final answer is..."Agent Stopping Conditions
from langchain.agents import AgentExecutor
executor = AgentExecutor(
agent=tool_calling_agent,
tools=tools,
max_iterations=5, # Stop after 5 tool calls
max_execution_time=30.0, # Stop after 30 seconds
early_stopping_method="generate", # Ask LLM to produce best answer when stopped
handle_parsing_errors=True, # Don't crash on malformed tool calls
verbose=True, # Log each step
)
# Output includes metadata about why execution stopped
result = executor.invoke({"input": "...", "chat_history": []})
print(result["output"])Agent vs Chain: When to Use Each
Use a Chain when:
- The steps are known in advance and fixed
- You always need steps A ā B ā C
- Determinism and predictability matter
- Cost and latency are tightly constrained
Use an Agent when:
- The model needs to decide which tools to use
- Different queries need different steps
- The number of steps is unknown in advance
- Tasks require adaptive multi-step reasoning
# Heuristic for choosing
def should_use_agent(query: str) -> bool:
agent_indicators = [
"find", "search", "look up", "compare",
"calculate", "check", "what is the current",
"analyze", "investigate",
]
return any(ind in query.lower() for ind in agent_indicators)
# Rule of thumb:
# "Summarize this document" ā Chain (fixed steps)
# "Answer this drug question using any information you need" ā AgentAgent Output Structure
result = executor.invoke({
"input": "What is the interaction between warfarin and aspirin?",
"chat_history": [],
})
# result is a dict
print(result.keys())
# dict_keys(["input", "output", "chat_history", "intermediate_steps"])
# Final answer
print(result["output"])
# What tools were called (for logging/audit)
for step in result.get("intermediate_steps", []):
action, observation = step
print(f"Tool: {action.tool}")
print(f"Input: {action.tool_input}")
print(f"Output: {observation[:100]}")Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.