Getting Started with Agents
December 8, 2025 ยท View on GitHub
In this chapter, you'll learn to build AI agents that can reason about problems, select appropriate tools, and work iteratively towards solutions. You'll understand the ReAct (Reasoning + Acting) pattern by implementing agent loops step-by-step, and discover how agents autonomously choose tools to accomplish complex tasks. These skills enable you to build autonomous AI systems that can handle complex, multi-step tasks.
Prerequisites
- Completed Function Calling & Tools
๐ฏ Learning Objectives
By the end of this chapter, you'll be able to:
- โ Understand what AI agents are and how they work
- โ Implement the ReAct (Reasoning + Acting) pattern
- โ Build agent loops that iterate until solving a problem
- โ Give agents multiple tools and let them choose the right one
- โ Use create_agent() for production-ready agent systems
- โ Implement middleware patterns for agent customization
- โ Build multi-step, autonomous AI systems
๐ The Manager with Specialists Analogy
Imagine you're a project manager with a team of specialists:
- ๐ Data Analyst - can query databases
- ๐ Researcher - can search the web
- ๐งฎ Accountant - can do calculations
- โ๏ธ Assistant - can send emails
When someone asks: "What's our revenue growth this quarter compared to last year?"
You (the manager) don't do everything yourself. You:
- Reason: "I need data from the database and calculations"
- Act: Ask the Data Analyst for revenue data
- Observe: Review the data received
- Reason: "Now I need to calculate the percentage change"
- Act: Ask the Accountant to do the math
- Observe: Get the calculated result
- Reason: "Now I have the answer"
- Respond: Give the final answer
AI Agents work the same way!
They:
- Think about what needs to be done (Reasoning)
- Choose the right tool (Decision Making)
- Use the tool (Acting)
- Evaluate the result (Observation)
- Repeat until they have the answer
- Respond to the user
Both project managers and AI agents delegate tasks to specialists/tools, following the same iterative pattern.
๐ค What Are Agents?
Standard LLM (No Agency or Tools)
User: "What's the current weather in Paris?"
LLM: "I cannot access real-time weather data. I can only provide general information..."
Agent with Tools
User: "What's the current weather in Paris?"
Agent: [Thinks] "I need to use the weather tool"
Agent: [Uses] get_weather(city="Paris")
Agent: [Observes] "18ยฐC, partly cloudy"
Agent: [Responds] "It's currently 18ยฐC and partly cloudy in Paris"
Agents with tools can access real-time data and take actions, while standard LLMs are limited to their training data.
๐ง The ReAct Pattern
ReAct = Reasoning + Acting
Agents follow this iterative loop:
1. Thought: What should I do next?
2. Action: Use a specific tool
3. Observation: What did the tool return?
4. (Repeat 1-3 as needed)
5. Final Answer: Respond to the user
Example:
User: "Calculate 25 * 17, then tell me if it's a prime number"
Thought 1: I need to calculate 25 * 17
Action 1: calculator(expression="25 * 17")
Observation 1: 425
Thought 2: I need to check if 425 is prime
Action 2: is_prime(number=425)
Observation 2: False (divisible by 5)
Final Answer: "25 * 17 equals 425, which is not a prime number
because it's divisible by 5."
The ReAct pattern: Agents iteratively reason about what to do, act by using tools, observe results, and repeat until they have the answer.
๐ Building Agents with create_agent()
LangChain Python provides create_agent() from langchain.agents - a high-level API that handles the ReAct loop automatically. This is the recommended approach for building production agents.
What create_agent() does for you:
- โ Manages the ReAct loop (Thought โ Action โ Observation โ Repeat)
- โ Handles message history automatically
- โ Implements iteration limits to prevent infinite loops
- โ Provides production-ready error handling
- โ Returns clean, structured responses
Example 1: Basic Agent with create_agent()
Let's see how to use create_agent() to create an autonomous agent that handles the ReAct loop (Thought โ Action โ Observation) automatically.
Key code you'll work with:
# Create agent using create_agent() - that's it!
agent = create_agent(
model,
tools=[calculator], # Pass tools to the agent
)
# Use the agent with messages array
response = agent.invoke({"messages": [HumanMessage(content=query)]})
# Get the final answer from the last message
last_message = response["messages"][-1]
Code: code/01_create_agent_basic.py
Run: python 05-agents/code/01_create_agent_basic.py
Example code:
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from dotenv import load_dotenv
import os
load_dotenv()
# Define a calculator tool for the agent
@tool
def calculator(expression: str) -> str:
"""A calculator that can perform basic arithmetic operations.
Args:
expression: The mathematical expression to evaluate
"""
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
def main():
# Create agent using create_agent() - that's it!
agent = create_agent(
model=os.getenv("AI_MODEL"),
tools=[calculator],
system_prompt="You are a helpful math assistant.",
)
# Use the agent with messages array
query = "What is 125 * 8?"
response = agent.invoke({
"messages": [HumanMessage(content=query)]
})
# Get the final answer from the last message
last_message = response["messages"][-1]
print(f"Agent: {last_message.content}")
if __name__ == "__main__":
main()
๐ค Try with GitHub Copilot Chat: Want to explore this code further? Open this file in your editor and ask Copilot:
- "What does create_agent() do under the hood?"
- "How does create_agent() handle iteration limits and prevent infinite loops?"
Expected Output
๐ค Agent with create_agent() Example
๐ค User: What is 125 * 8?
๐ค Agent: 125 ร 8 = 1000
โ
Under the hood:
create_agent() implements the ReAct pattern (Thought โ Action โ Observation)
and handles all the boilerplate for you.
How It Works
What's happening behind the scenes:
- Agent receives query: "What is 125 * 8?"
- Reasons: Determines it needs the calculator tool
- Acts: Executes
calculator(expression="125 * 8") - Observes: Gets result "1000"
- Responds: Formats natural language response
Example 2: create_agent() with Multiple Tools
Let's see how to give an agent multiple tools using tools=[tool1, tool2, tool3] and observe how it autonomously selects the right one.
Key code you'll work with:
# Create agent with all three tools - agent auto-selects the right one
agent = create_agent(
model,
tools=[calculator, get_weather, search], # Multiple tools!
)
# Agent automatically picks the correct tool for each query
queries = [
"What is 50 * 25?", # โ Uses calculator
"What's the weather in Tokyo?", # โ Uses get_weather
"Tell me about LangChain", # โ Uses search
]
Code: code/02_create_agent_multi_tool.py
Run: python 05-agents/code/02_create_agent_multi_tool.py
Example code:
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from dotenv import load_dotenv
import os
load_dotenv()
@tool
def calculator(expression: str) -> str:
"""Perform mathematical calculations."""
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
temps = {"Seattle": 62, "Paris": 18, "Tokyo": 24}
temp = temps.get(city, 72)
return f"Current weather in {city}: {temp}ยฐF"
@tool
def search(query: str) -> str:
"""Search for information about a topic."""
return f"LangChain is a Python framework for building AI applications with LLMs."
def main():
# Create agent with all three tools
agent = create_agent(
model=os.getenv("AI_MODEL"),
tools=[calculator, get_weather, search],
system_prompt="You are a helpful assistant with access to multiple tools.",
)
# Agent automatically picks the correct tool for each query
queries = [
"What is 50 * 25?", # โ Uses calculator
"What's the weather in Tokyo?", # โ Uses get_weather
"Tell me about LangChain", # โ Uses search
]
for query in queries:
response = agent.invoke({
"messages": [HumanMessage(content=query)]
})
last_message = response["messages"][-1]
print(f"User: {query}")
print(f"Agent: {last_message.content}\n")
if __name__ == "__main__":
main()
Expected Output
๐๏ธ Multi-Tool Agent with create_agent()
๐ค User: What is 50 * 25?
๐ค Agent: 50 multiplied by 25 equals 1250.
๐ค User: What's the weather in Tokyo?
๐ค Agent: Current weather in Tokyo: 24ยฐF
๐ค User: Tell me about LangChain
๐ค Agent: LangChain is a Python framework for building applications with large
language models (LLMs).
๐ก What just happened:
โข The agent automatically selected the right tool for each query
โข Calculator for math (50 * 25)
โข Weather tool for Tokyo weather
โข Search tool for LangChain information
โข All with the same agent instance!
How It Works
What's happening:
- Agent receives query: "What is 50 * 25?"
- Reads tool descriptions: Reviews all available tools
- Selects best match: Calculator tool (description mentions "mathematical calculations")
- Executes tool: Runs calculator with the expression
- Returns natural response: Formats the result in natural language
Tool Selection Logic:
- The agent uses tool names and descriptions to match queries to tools
- More specific descriptions โ Better tool selection
- The LLM decides which tool fits best based on semantic meaning
- You can give your agent multiple tools, and it will intelligently pick the right one for each task
๐ค Try with GitHub Copilot Chat: Want to explore this code further? Open this file in your editor and ask Copilot:
- "How does the agent decide which tool to use?"
- "How can I add error handling if a tool fails?"
Example 3: Manual ReAct Loop (Understanding the Pattern)
To understand what create_agent() does under the hood, let's implement the ReAct loop manually.
Code: code/03_manual_react.py
Run: python 05-agents/code/03_manual_react.py
Example code:
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from dotenv import load_dotenv
import os
load_dotenv()
@tool
def calculator(expression: str) -> str:
"""Perform mathematical calculations."""
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
@tool
def is_prime(number: int) -> str:
"""Check if a number is prime."""
if number < 2:
return "False"
for i in range(2, int(number ** 0.5) + 1):
if number % i == 0:
return f"False (divisible by {i})"
return "True"
def run_react_loop(query: str, tools: list, max_iterations: int = 5):
"""Manually implement the ReAct loop."""
model = ChatOpenAI(
model=os.getenv("AI_MODEL"),
base_url=os.getenv("AI_ENDPOINT"),
api_key=os.getenv("AI_API_KEY")
)
# Create tool lookup
tools_by_name = {t.name: t for t in tools}
# Bind tools to model
model_with_tools = model.bind_tools(tools)
# Initialize messages
messages = [HumanMessage(content=query)]
for iteration in range(max_iterations):
print(f"\n--- Iteration {iteration + 1} ---")
# Step 1: Call the model
response = model_with_tools.invoke(messages)
messages.append(response)
# Step 2: Check if there are tool calls
if not response.tool_calls:
print("No more tool calls - Final answer ready")
return response.content
# Step 3: Execute each tool call
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
print(f"Action: {tool_name}({tool_args})")
# Execute the tool
tool_result = tools_by_name[tool_name].invoke(tool_args)
print(f"Observation: {tool_result}")
# Add tool result to messages
messages.append(
ToolMessage(content=str(tool_result), tool_call_id=tool_call["id"])
)
return "Max iterations reached"
def main():
tools = [calculator, is_prime]
query = "Calculate 25 * 17, then tell me if the result is a prime number"
print(f"Query: {query}")
result = run_react_loop(query, tools)
print(f"\n๐ค Final Answer: {result}")
if __name__ == "__main__":
main()
Expected Output
Query: Calculate 25 * 17, then tell me if the result is a prime number
--- Iteration 1 ---
Action: calculator({'expression': '25 * 17'})
Observation: 425
--- Iteration 2 ---
Action: is_prime({'number': 425})
Observation: False (divisible by 5)
--- Iteration 3 ---
No more tool calls - Final answer ready
๐ค Final Answer: 25 * 17 equals 425, which is not a prime number
because it is divisible by 5.
๐ค Try with GitHub Copilot Chat: Want to explore this code further? Open this file in your editor and ask Copilot:
- "Walk me through what happens in each iteration of this loop"
- "How does the agent know when to stop calling tools?"
๐ง Additional Agent Patterns
Now that you understand how to build basic agents with single and multiple tools, let's explore an additional pattern for production applications: middleware. Middleware lets you add behavior like logging, error handling, and dynamic model selection without modifying your tools or agent core logic.
Example 4: create_agent() with Middleware
This example shows how to use middleware with create_agent() for production scenarios like dynamic model selection based on conversation complexity and graceful error handling.
Key code you'll work with:
# Middleware intercepts agent behavior without changing tools
class DynamicModelMiddleware(AgentMiddleware):
def wrap_model_call(self, request, handler):
if len(request.state["messages"]) > 10:
# Switch to more capable model for complex conversations
return handler(request.override(model=advanced_model))
return handler(request)
# Create agent with middleware - adds behavior like logging & error handling
agent = create_agent(
model,
tools=[calculator, search],
middleware=[DynamicModelMiddleware(), ToolErrorMiddleware()], # Plugin-style behavior!
)
Code: code/04_agent_with_middleware.py
Run: python 05-agents/code/04_agent_with_middleware.py
Example code:
What is middleware? Middleware intercepts and modifies agent behavior without changing your tools or agent logic. Think of it as "plugins" for your agent.
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest
from langchain.agents.middleware.types import ModelResponse
from langchain_core.messages import ToolMessage
from typing import Callable, Any
# Middleware 1: Dynamic Model Selection
# Switches to a more capable (and expensive) model for complex conversations
class DynamicModelMiddleware(AgentMiddleware):
def __init__(self, messages_threshold: int = 10):
super().__init__()
self.messages_threshold = messages_threshold
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
message_count = len(request.state["messages"])
print(f" [Middleware] Message count: {message_count}")
# Option for complex conversations (>threshold messages)
if message_count > self.messages_threshold:
print(" [Middleware] Switching to more capable model")
# return handler(request.override(model=advanced_model))
return handler(request)
# Middleware 2: Custom Error Handling
# Catches tool failures and provides helpful fallback messages
class ToolErrorMiddleware(AgentMiddleware):
def wrap_tool_call(
self,
request: Any,
handler: Callable[[Any], ToolMessage],
) -> ToolMessage:
try:
return handler(request)
except Exception as e:
tool_name = request.tool_call.get("name", "unknown")
print(f" [Middleware] Tool '{tool_name}' failed: {e}")
# Return graceful fallback instead of crashing
return ToolMessage(
content=f"I encountered an error: {e}. Let me try a different approach.",
tool_call_id=request.tool_call.get("id", ""),
)
# Create agent with both middleware
agent = create_agent(
model,
tools=[calculator, search],
middleware=[DynamicModelMiddleware(), ToolErrorMiddleware()]
)
๐ค Try with GitHub Copilot Chat: Want to explore this code further? Open this file in your editor and ask Copilot:
- "How would I add request logging middleware?"
- "Can middleware modify tool arguments before execution?"
Expected Output
When you run python 05-agents/code/04_agent_with_middleware.py:
๐ง Agent with Middleware Example
Test 1: Simple calculation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ค User: What is 25 * 8?
[Middleware] Message count: 1
[Middleware] โ Using current model
๐ค Agent: 25 multiplied by 8 equals 200.
Test 2: Search with error handling
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ค User: Search for information about error handling
[Middleware] Message count: 1
[Middleware] โ Using current model
[Middleware] โ ๏ธ Tool "search" failed: Search service temporarily unavailable
[Middleware] ๐ Returning fallback message
๐ค Agent: I encountered an error while using the search tool. Let me try
a different approach to answer your question about error handling.
๐ก Middleware Benefits:
โข Dynamic model selection โ Cost optimization
โข Error handling โ Graceful degradation
โข Logging โ Easy debugging
โข Flexibility โ Customize behavior without changing tools
โ
Production Use Cases:
โข Switch to cheaper models for simple queries
โข Automatic retries with exponential backoff
โข Request/response logging for monitoring
โข User context injection (auth, permissions)
โข Rate limiting and quota management
How Middleware Works
Middleware Flow:
flowchart TD
A[User Query] --> B[Middleware: Dynamic Model Selection]
B -->|Chooses right model| C[Agent Decision: which tool?]
C --> D[Middleware: Error Handler]
D -->|Wraps tool execution| E[Tool Execution]
E -->|May fail here| F[Middleware: Error Handler]
F -->|Catches errors, returns fallback| G[Agent Response]
Two Middleware Types:
-
wrap_model_call - Intercepts calls TO the model
- Dynamic model selection based on conversation length
- Request logging and monitoring
- Context injection (user permissions, session data)
-
wrap_tool_call - Intercepts tool executions
- Error handling and retries
- Tool result transformation
- Permission checks before tool execution
Production Benefits:
- โ Cost Optimization: Use cheap models for simple tasks, expensive for complex
- โ Resilience: Graceful error handling prevents agent crashes
- โ Observability: Log all requests for debugging and monitoring
- โ Flexibility: Add behavior without modifying tools or agent core logic
When to use middleware:
- Production agents that need reliability
- Multi-tenant applications (different users, different permissions)
- Cost-sensitive applications
- Systems requiring audit logs
๐ง Tool Selection Logic
The agent uses tool names and descriptions to match queries to tools:
| User Query | Tool Selected | Why |
|---|---|---|
| "What is 50 * 25?" | calculator | Matches "mathematical calculations" |
| "Weather in Tokyo?" | get_weather | Matches "weather for a city" |
| "Tell me about X" | search | Matches "search for information" |
Tips for better tool selection:
- Use descriptive names -
get_weathernottool1 - Write clear descriptions - explain what the tool does
- Document parameters - use docstrings with Args sections
- Be specific - more detail helps the LLM choose correctly
Agents intelligently select the right tool based on semantic matching between the query and tool descriptions.
๐ Key Takeaways
- Agents make autonomous decisions - They choose which tools to use and when
- ReAct pattern is the core: Reason โ Act โ Observe โ Repeat until solved
- create_agent() is production-ready - Handles the ReAct loop automatically with built-in error handling
- Tool descriptions matter - Clear descriptions help agents pick the right tool
- Middleware adds flexibility - Plugin-style behavior for logging, error handling, dynamic model selection
- Start simple, scale up - Begin with basic agents, add middleware for production needs
๐บ๏ธ Concept Map
This chapter taught you how agents use the ReAct pattern for autonomous reasoning:
graph TD
A[User Query] --> B[Thought]
B --> C{Need Tool?}
C -->|Yes| D[Action]
D --> E[Observation]
E --> B
C -->|No| F[Answer]
Agents iterate (Think โ Act โ Observe) until they solve the problem.
๐ Assignment
Ready to practice? Complete the challenges in assignment.md!
The assignment includes:
- Research Agent with ReAct Loop - Build an agent from scratch that uses the ReAct pattern to answer questions
- Multi-Step Planning Agent (Bonus) - Build an agent with multiple specialized tools that requires multi-step reasoning
๐ Additional Resources
- LangChain Agents Documentation
- LangChain Middleware Guide - Custom middleware patterns
- ReAct Paper - Original research on Reasoning + Acting pattern
- LangChain create_agent() API - Official API reference
๐ก Want to see manual agent implementations? Check out the samples/ folder for:
- Manual ReAct loop examples - See how agents work under the hood without
create_agent() - Step-by-step agent patterns - Custom loop logic and detailed debugging
- These are great for understanding fundamentals before using
create_agent()
๐ What's Next?
Great work! You've learned how to build autonomous AI agents that use the ReAct pattern to reason about problems and decide which tools to useโwithout hardcoded logic or manual control flow.
Building on Agents
Your agents can choose and use tools, but where do those tools come from?
Next, you'll connect agents to external services via the Model Context Protocol (MCP), create retrieval tools from documents using embeddings and semantic search, and finally build systems where agents intelligently search your knowledge base in agentic RAG systems.
Project Ideas (So Far)
With what you've learned, you can build:
- ๐งฎ Smart calculator - Agent that knows when to use math vs search tools
- ๐ค๏ธ Weather assistant - Agent that coordinates multiple data sources
- ๐ Task coordinator - Agent that manages multiple tools for complex workflows
- ๐ Research helper - Agent that combines calculation, search, and analysis tools
After completing the remaining chapters, you'll add external service integration (MCP) and document search capabilities!
๐ Troubleshooting
Common issues you might encounter when building agents:
"Agent loops forever or hits max iterations"
Cause: Agent doesn't have a stopping condition or tools don't return useful results
Fixes:
- Check your stopping condition:
if not response.tool_calls or len(response.tool_calls) == 0:
# Agent has finished - no more tools needed
break
- Lower
max_iterationsto fail fast during development:
max_iterations = 3 # Start small, increase if needed
- Ensure tools return meaningful results - vague outputs confuse the agent
"Tool not found" error
Cause: Tool name mismatch between what LLM generates and what you defined
Fix: Verify the tool name exactly matches:
@tool
def calculator(expression: str) -> str: # Name must match exactly
"""Perform mathematical calculations."""
# ...
Agent makes wrong tool choices
Cause: Tool descriptions aren't clear enough
Fix: Improve tool descriptions with specific use cases:
# โ Vague
"""Does calculations"""
# โ
Clear
"""Perform mathematical calculations like addition, multiplication, percentages.
Use this when you need to compute numbers."""
Agent gets stuck repeating the same tool
Cause: Tool doesn't provide enough information for agent to progress
Fix: Ensure tool results are descriptive:
# โ Not helpful
return "42"
# โ
Descriptive
return "The calculation result is 42. This is the answer to 6 * 7."
๐ฆ Dependencies
Make sure you have the required packages:
pip install langchain langchain-openai python-dotenv
๐บ๏ธ Navigation
โ Previous: Function Calling & Tools | Back to Main | Next: Model Context Protocol (MCP) โ
๐ฌ Questions or stuck?
If you get stuck or have any questions about building AI apps, join:
If you have product feedback or errors while building visit: