LangChain Agents Teaching AI to Make Decisions
Tools let an AI use external capabilities. Agents take this further — they let the AI decide which tools to use, in what order, and how many times, all on its own. Instead of you hard-coding a fixed sequence of steps, you give the agent a goal and a set of tools, and it figures out the plan. This is one of the most powerful patterns in LangChain, and it is also the one that requires the most careful design.
The Detective Analogy
A detective does not receive a fixed script for solving cases. They start with a question ("Who committed the crime?"), use whatever investigative tools are available (interviews, evidence analysis, records lookup), follow the evidence wherever it leads, and keep going until they reach a conclusion. A LangChain Agent works exactly the same way — start with a goal, use available tools, reason about the results, take the next action, and repeat until the task is complete.
Agent Reasoning Loop (ReAct Pattern): START │ ▼ Think: "What do I need to do to answer this?" │ ▼ Act: Call a tool │ ▼ Observe: See the tool result │ ▼ Think again: "Do I have enough to answer now?" │ ├── No → Act: Call another tool → loop back │ └── Yes → Generate final answer
The ReAct Pattern
The most widely used agent architecture is called ReAct (Reasoning and Acting). The agent alternates between reasoning about what to do next and acting by calling a tool. This cycle continues until the agent decides it has enough information to give a final answer.
Example: "What is the population of Tokyo and how does it compare to London?"
Thought 1: "I need to find Tokyo's population."
Action 1: search_web("population of Tokyo 2024")
Observation 1: "Tokyo's population is approximately 13.96 million in the city proper."
Thought 2: "Now I need London's population."
Action 2: search_web("population of London 2024")
Observation 2: "London's population is approximately 9 million."
Thought 3: "I have both numbers. I can now compare them."
Final Answer: "Tokyo has approximately 13.96 million people, roughly
1.55 times the population of London at 9 million."
Building an Agent with LangChain
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_community.tools import DuckDuckGoSearchRun
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
load_dotenv()
# Define tools
@tool
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression and return the result.
Examples: '2 + 2', '100 * 0.15', 'sqrt(144)'
Use this for any math calculation the user requests."""
try:
# Use eval carefully — only for math in a controlled environment
import math
result = eval(expression, {"__builtins__": {}}, vars(math))
return str(result)
except Exception as e:
return f"Calculation error: {e}"
search = DuckDuckGoSearchRun()
tools = [search, calculate]
# Build the agent prompt
prompt = ChatPromptTemplate.from_messages([
("system",
"You are a helpful research assistant. You have access to web search and "
"a calculator. Use them to find accurate answers. Always think step by step."),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
# Create the agent and executor
model = ChatOpenAI(model="gpt-4o", temperature=0)
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # Print the reasoning steps
max_iterations=10, # Safety limit: stop after 10 steps
handle_parsing_errors=True
)
# Run the agent
result = agent_executor.invoke({
"input": "What is the current population of India? And what is 1.5% of that number?",
"chat_history": []
})
print("\nFinal answer:", result["output"])
Setting verbose=True shows every reasoning step the agent takes. This is invaluable for debugging and for understanding how agents make decisions. In production, set it to False to keep logs clean.
Understanding AgentExecutor
The AgentExecutor is the loop that runs the agent. It handles the cycle of calling the agent, executing tool calls, feeding results back, and running again until the agent produces a final answer or hits the max_iterations limit.
AgentExecutor internals:
iteration = 0
while True:
iteration += 1
if iteration > max_iterations:
return "Agent stopped: too many steps"
# Ask the agent what to do next
agent_output = agent.invoke(current_messages)
if agent_output is a final answer:
return agent_output
if agent_output has tool calls:
for tool_call in agent_output.tool_calls:
result = execute_tool(tool_call)
add result to messages
# Loop continues
Memory in Agents
Agents support conversation memory using the same pattern as chatbots — a history list passed through MessagesPlaceholder. This lets the agent remember earlier parts of the conversation and avoid repeating searches it already did.
from langchain_core.messages import HumanMessage, AIMessage
chat_history = []
def agent_chat(user_input: str) -> str:
result = agent_executor.invoke({
"input": user_input,
"chat_history": chat_history
})
# Update history
chat_history.append(HumanMessage(content=user_input))
chat_history.append(AIMessage(content=result["output"]))
return result["output"]
print(agent_chat("Search for the latest news about renewable energy."))
print(agent_chat("How does that compare to what you found about solar energy earlier?"))
Controlling Agent Behavior
max_iterations
Set a hard limit on how many steps the agent can take. Without this, a confused agent can loop indefinitely, wasting time and API money.
agent_executor = AgentExecutor(agent=agent, tools=tools, max_iterations=7)
max_execution_time
Stop the agent after a certain number of seconds regardless of progress. Essential for web applications with response time requirements.
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
max_execution_time=30 # Stop after 30 seconds
)
early_stopping_method
When the agent hits a limit, decide what to do: "force" makes the model generate a best-effort answer with what it has, "generate" also attempts a partial answer.
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
max_iterations=5,
early_stopping_method="force"
)
Intermediate Steps: Seeing the Agent's Work
Setting return_intermediate_steps=True gives you the full action history — every tool call and result — alongside the final answer. This is useful for building UIs that show users what the agent did.
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
return_intermediate_steps=True,
verbose=False
)
result = agent_executor.invoke({
"input": "What is the current price of gold per ounce?",
"chat_history": []
})
print("Final answer:", result["output"])
print("\nSteps taken:")
for step in result["intermediate_steps"]:
action, observation = step
print(f" Tool: {action.tool}")
print(f" Input: {action.tool_input}")
print(f" Result: {str(observation)[:100]}")
print()
Building a Specialized Research Agent
from langchain_core.tools import tool
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
@tool
def get_page_word_count(url: str) -> str:
"""Fetch a web page and count its words.
Use this to estimate how long a document is."""
import requests
from bs4 import BeautifulSoup
try:
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
text = soup.get_text()
word_count = len(text.split())
return f"The page at {url} contains approximately {word_count} words."
except Exception as e:
return f"Could not fetch page: {e}"
@tool
def summarize_text(text: str) -> str:
"""Summarize a long piece of text into 3 bullet points.
Use this when you have retrieved a lot of text and need to condense it."""
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
summary_model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
summary_prompt = ChatPromptTemplate.from_messages([
("system", "Summarize the text in 3 concise bullet points."),
("human", "{text}")
])
chain = summary_prompt | summary_model | StrOutputParser()
return chain.invoke({"text": text[:3000]}) # Limit to 3000 chars
wiki = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(top_k_results=2))
research_tools = [wiki, search, summarize_text]
Custom Agent with Structured Output
For production applications, you may want the agent to return structured data rather than plain text. Use a structured output model to enforce JSON responses.
from pydantic import BaseModel, Field
from typing import List
class ResearchReport(BaseModel):
topic: str = Field(description="The research topic")
key_findings: List[str] = Field(description="3-5 key facts found")
sources_used: List[str] = Field(description="Tools or sources consulted")
confidence: str = Field(description="High, Medium, or Low confidence in findings")
# Create model with structured output
structured_model = ChatOpenAI(model="gpt-4o").with_structured_output(ResearchReport)
Agent Safety Considerations
Agents that can take actions in the real world — sending emails, modifying databases, making purchases — carry real risk if they make mistakes. Follow these safety practices.
Confirmation for Irreversible Actions
@tool
def delete_record(record_id: str) -> str:
"""Delete a record from the database. This action is permanent and cannot be undone.
IMPORTANT: Only use this after the user has explicitly confirmed they want to delete."""
# Check for confirmation in the conversation context
# In a real app, implement a confirmation mechanism
return f"Record {record_id} deleted permanently."
Rate Limiting Tool Calls
import time
from functools import wraps
call_counts = {}
def rate_limited(max_calls_per_minute: int):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
tool_name = func.__name__
now = time.time()
calls = call_counts.get(tool_name, [])
# Remove calls older than 60 seconds
calls = [t for t in calls if now - t < 60]
if len(calls) >= max_calls_per_minute:
return "Rate limit exceeded. Please wait before making more requests."
calls.append(now)
call_counts[tool_name] = calls
return func(*args, **kwargs)
return wrapper
return decorator
@tool
@rate_limited(max_calls_per_minute=5)
def search_database(query: str) -> str:
"""Search the company database."""
# Database query here
return f"Results for: {query}"
Agent Use Cases
Use Case Tools Needed ────────────────────────────────────────────────────────────────── Research assistant Web search, Wikipedia, summarizer Customer support bot Database lookup, FAQ search, email Data analysis helper Code executor, file reader, chart maker Travel planner Flight search, hotel lookup, weather Personal productivity Calendar, email, notes, reminders Developer assistant Code search, docs lookup, code runner
When Agents Are Not the Right Tool
Agents add complexity and unpredictability compared to fixed chains. Use a chain when the workflow is always the same sequence of steps. Use an agent only when the steps genuinely need to vary based on the situation.
Use a Chain when: - The steps are always the same - Speed is critical - You need predictable, auditable behavior - The task is simple (summarize, classify, extract) Use an Agent when: - Steps depend on results of previous steps - The number of steps varies per task - The AI needs to decide between multiple approaches - The task involves multi-source research
Summary
Agents let AI models autonomously decide which tools to call, in what order, and how many times to achieve a goal. The ReAct pattern alternates between thinking and acting in a loop until the agent has enough information to answer. AgentExecutor manages this loop with safety limits like max_iterations and max_execution_time. return_intermediate_steps reveals every action taken for transparency. Agents are most valuable when task steps genuinely depend on previous results.
