There's a fundamental difference between a chatbot and an agent:
Chatbot: "I can answer your questions!" Agent: "I can answer your questions AND book your flight AND send the confirmation email AND add it to your calendar."
By 2025, we've moved beyond passive AI that just talks. We now build autonomous agents that reason, plan, use tools, and execute complex workflows. And LangChain is the framework that makes this accessible.
This builds on advanced prompt engineering patterns - specifically Chain-of-Thought and role-based prompting that define agent behavior.
Let me show you how to build agents that actually get work done.
What Even IS an Agent?
An agent is an AI system that:
- Reasons about what needs to be done
- Chooses which tools to use
- Executes actions
- Observes results
- Repeats until the goal is achieved
Think of it like a junior employee you can delegate to. You say "find all overdue invoices and send reminders," and they figure out HOW to do it using the tools available.
The Core Pattern: ReAct (Reason + Act)
Most agents in 2025 use the ReAct pattern. It's a loop:
Thought: "I need to check the weather." Action: Use weather_api tool with location "Nairobi" Observation: Returns "22°C, partly cloudy" Thought: "Now I can answer the user." Response: "It's currently 22°C and partly cloudy in Nairobi."
The magic is the MODEL decides which tool to use based on descriptions you provide.
Setup: The Essentials
pip install langchain langchain-openai langchain-community \
duckduckgo-search python-dotenv
import os
from dotenv import load_dotenv
load_dotenv()
os.environ["OPENAI_API_KEY"] = "sk-your-key-here"
Building Your First Agent: Step by Step
Step 1: Define Tools
Tools are Python functions that your agent can call. Let's create two: web search and a calculator.
from langchain_community.tools import DuckDuckGoSearchRun
from langchain.agents import tool
# Tool 1: Web Search (pre-built)
search = DuckDuckGoSearchRun()
# Tool 2: Custom Calculator
@tool
def calculate(expression: str) -> str:
"""
Evaluates a mathematical expression.
Example: "2 + 2" returns "4"
"""
try:
result = eval(expression) # In production, use safe_eval
return str(result)
except Exception as e:
return f"Error: {str(e)}"
# Tool 3: Custom Order Lookup
@tool
def check_order(order_id: str) -> str:
"""
Returns order status for a given order ID.
Example: check_order("ORD-123")
"""
# Mock database
orders = {
"ORD-123": "Shipped - Arriving Dec 15",
"ORD-456": "Processing",
"ORD-789": "Delivered"
}
return orders.get(order_id, "Order not found")
# Combine tools
tools = [search, calculate, check_order]
Key insight: The docstring is CRITICAL. That's how the agent knows when to use each tool.
Step 2: Initialize the LLM
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini", # Fast and cheap
temperature=0 # Deterministic (important for agents)
)
Why temperature=0? Agents need to be predictable. We don't want creative tool choices; we want the RIGHT tool.
Step 3: Create the Agent
from langchain.agents import create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# Define the agent's behavior
prompt = ChatPromptTemplate.from_messages([
("system", """You are a helpful AI assistant.
You have access to tools to:
- Search the web
- Perform calculations
- Look up order status
Use tools when needed. Be concise and helpful.
If you can't find information, say so clearly."""),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"), # For intermediate steps
])
# Create the agent
agent = create_openai_tools_agent(llm, tools, prompt)
Notice how this system prompt follows the CO-STAR prompting framework - defining Context, Objective, Style, Tone, Audience, and Response format for consistent agent behavior.
Step 4: The Agent Executor (The Runtime)
The executor manages the think-act-observe loop:
from langchain.agents import AgentExecutor
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # See the agent's thinking process
handle_parsing_errors=True, # Recover from mistakes
max_iterations=5 # Prevent infinite loops
)
Step 5: Run It!
# Test 1: Simple question (no tools needed)
response = agent_executor.invoke({
"input": "Hello! What can you help me with?"
})
print(response["output"])
# Test 2: Requires calculation
response = agent_executor.invoke({
"input": "What is 25% of 8,420?"
})
print(response["output"])
# Test 3: Requires web search
response = agent_executor.invoke({
"input": "What's the current price of Bitcoin?"
})
print(response["output"])
# Test 4: Requires order lookup
response = agent_executor.invoke({
"input": "Where is my order ORD-123?"
})
print(response["output"])
What you'll see (with verbose=True):
> Entering new AgentExecutor chain...
Thought: I need to look up order ORD-123
Action: check_order
Action Input: "ORD-123"
Observation: Shipped - Arriving Dec 15
Thought: I now know the answer
Final Answer: Your order ORD-123 has been shipped and is arriving on December 15.
> Finished chain.
Beautiful.
Memory: Making Agents Remember Conversations
Right now, every invoke() is isolated. The agent has no memory of previous interactions.
from langchain.memory import ConversationBufferMemory
from langchain.agents import create_openai_tools_agent, AgentExecutor
# Create memory
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
# Update prompt to include history
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant with memory."),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
# Recreate agent with memory-aware prompt
agent = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
verbose=True
)
# Now it remembers!
agent_executor.invoke({"input": "My name is Jiji"})
agent_executor.invoke({"input": "What's my name?"})
# Returns: "Your name is Jiji!"
For agents that need to access large knowledge bases, consider implementing RAG (Retrieval-Augmented Generation) to give agents long-term memory beyond conversation context.
Advanced Tool: API Integration
Let's build a tool that hits a real API:
import requests
@tool
def get_github_user(username: str) -> str:
"""
Fetches GitHub user information.
Example: get_github_user("torvalds")
"""
try:
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code == 200:
data = response.json()
return f"""
Name: {data.get('name')}
Bio: {data.get('bio')}
Public Repos: {data.get('public_repos')}
Followers: {data.get('followers')}
"""
return f"User {username} not found"
except Exception as e:
return f"Error: {str(e)}"
Add it to tools and watch your agent become a GitHub stalker:
tools.append(get_github_user)
response = agent_executor.invoke({
"input": "Tell me about the GitHub user 'gvanrossum'"
})
Learn more about integrating OpenAI and other AI APIs for production agent tools.
Error Handling: When Things Go Wrong
Agents fail. APIs timeout. Tools crash. Handle it gracefully.
from langchain.agents import AgentExecutor
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=True, # Crucial
max_iterations=10,
max_execution_time=30, # Timeout after 30 seconds
)
# Wrap in try-except
def safe_invoke(query):
try:
return agent_executor.invoke({"input": query})["output"]
except Exception as e:
return f"I encountered an error: {str(e)}. Please try rephrasing your question."
Agent Types: Choosing the Right One
LangChain offers several agent types:
1. OpenAI Tools Agent (The Standard)
What we've been using. Works with GPT-4's native function calling. Fast, reliable.
Use when: You're using OpenAI models and want the best performance.
2. ReAct Agent (The Classic)
Uses prompt engineering instead of function calling. Works with ANY model.
from langchain.agents import create_react_agent
agent = create_react_agent(llm, tools, prompt)
Use when: You're using non-OpenAI models (Anthropic Claude, open-source models).
3. Conversational Agent
Specialized for chat applications with strong memory management.
Use when: Building customer support bots or personal assistants.
Real-World Use Case: Customer Support Agent
Let's build something production-ready:
from langchain.agents import tool
from langchain_openai import ChatOpenAI
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# Tools
@tool
def lookup_order(order_id: str) -> str:
"""Look up order status by order ID"""
# In production, hit your actual database
return f"Order {order_id}: Shipped, arriving Dec 20"
@tool
def create_ticket(issue: str) -> str:
"""Create a support ticket for complex issues"""
# In production, hit your ticketing system (Zendesk, etc.)
ticket_id = "TIK-" + str(hash(issue))[:8]
return f"Support ticket {ticket_id} created. A human will follow up within 24 hours."
@tool
def check_warranty(product_id: str) -> str:
"""Check warranty status for a product"""
return f"Product {product_id} has 2 years warranty remaining"
# System prompt
system_prompt = """You are SupportBot for TechCorp.
PERSONALITY:
- Professional but friendly
- Patient and helpful
- Never make promises you can't keep
RULES:
1. Always greet users warmly
2. Use tools to look up actual information (don't guess)
3. For complex issues beyond your tools, create a support ticket
4. Always ask for order ID before looking up orders
5. If you don't know, say so and offer to connect them with a human
KNOWLEDGE:
- Return period: 30 days
- Shipping: 3-5 business days for Kenya
- Support hours: Mon-Fri 9am-6pm EAT
"""
tools = [lookup_order, create_ticket, check_warranty]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = create_openai_tools_agent(llm, tools, prompt)
support_agent = AgentExecutor(
agent=agent,
tools=tools,
verbose=False, # Don't show thinking to customers
handle_parsing_errors=True
)
# Simulate customer interaction
print(support_agent.invoke({"input": "Hi, I want to return my laptop"})["output"])
print()
print(support_agent.invoke({"input": "Order ORD-999"})["output"])
Performance Optimization
1. Parallel Tool Execution
If agent needs multiple independent tools, run them in parallel:
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
return_intermediate_steps=True,
max_iterations=15,
# Enable parallel execution
early_stopping_method="generate"
)
2. Tool Description Optimization
The better your tool descriptions, the fewer mistakes:
Bad:
@tool
def get_data(id: str) -> str:
"""Gets data"""
...
Good:
@tool
def get_customer_profile(customer_id: str) -> str:
"""
Retrieves customer profile information from the database.
Args:
customer_id: Unique customer identifier (format: CUST-12345)
Returns:
JSON string with customer name, email, tier, and account status
Example:
get_customer_profile("CUST-12345")
Returns: {"name": "John Doe", "email": "john@example.com", ...}
"""
...
3. Caching Tool Results
Don't call expensive APIs repeatedly for the same input:
from functools import lru_cache
@lru_cache(maxsize=100)
@tool
def expensive_api_call(query: str) -> str:
"""Calls expensive external API"""
# Results cached for identical queries
return api_response
Testing Your Agent
Unit test individual tools:
def test_order_lookup():
result = check_order.invoke("ORD-123")
assert "Shipped" in result
def test_calculator():
result = calculate.invoke("2 + 2")
assert result == "4"
Integration test the full agent:
def test_agent_end_to_end():
response = agent_executor.invoke({
"input": "What is 15% of 200 and where is order ORD-123?"
})
assert "30" in response["output"] # 15% of 200
assert "Shipped" in response["output"] # Order status
Common Pitfalls
1. Tool Descriptions Are Vague Agent won't know when to use the tool. Fix: Write crystal-clear docstrings with examples.
2. No Max Iterations Agent gets stuck in a loop. Fix: Set max_iterations=10 or lower.
3. No Error Handling One tool crash kills the entire agent. Fix: Use handle_parsing_errors=True and try-except in tools.
4. Tools Do Too Much One tool that "does everything" confuses the agent. Fix: Keep tools single-purpose. Many small tools > one giant tool.
5. Hallucination Despite Tools Agent makes up answers instead of using tools. Fix: Add to system prompt: "You MUST use tools for factual information. Never guess."
The Bottom Line
Agents in 2025 aren't just chatbots with extra steps. They're autonomous systems that reason, plan, and execute.
The recipe:
- Define clear, single-purpose tools
- Write detailed tool descriptions
- Use ReAct pattern (Thought → Action → Observation → Response)
- Add memory for conversational context
- Handle errors gracefully
- Test thoroughly
Start with 2-3 simple tools. Get the loop working. Then expand.
Within days, you can have an agent that books appointments, queries databases, sends emails, and generates reports. All by just describing what tools exist and letting the model figure out HOW to use them.
When you're ready to deploy these agents to production, consider building a serverless API with AWS Lambda to handle agent invocations cost-effectively.
The future isn't AI that just talks. It's AI that acts.