What Are AI Agents? Architecture and Patterns
What Are AI Agents? Architecture and Patterns
An AI agent is a system that can perceive its environment, reason about it, and take actions to achieve goals. Unlike chains that follow fixed sequences, agents decide what to do based on their observations. This lesson covers agent fundamentals, the ReAct pattern, planning strategies, and when to use agents.
What Makes Something an Agent?
An agent has:
- Perception: Input from the environment (observations, user questions)
- Reasoning: Processing to decide what to do next (using an LLM)
- Action: Taking steps (calling tools, APIs, computing)
- Feedback loop: Observing results and adapting
Simple example:
def simple_agent(task: str):
"""Simplest agent loop."""
observation = task
while True:
# Reason: ask LLM what to do
action = ask_llm(f"What should I do about: {observation}")
if action == "DONE":
break
# Act: execute the action
result = execute_action(action)
# Observe: see what happened
observation = result
return observation
The ReAct Pattern
ReAct (Reasoning + Acting) is the most popular agent pattern. The model interleaves reasoning thoughts with actions:
Thought: I need to find information about X
Action: search_wikipedia("X")
Observation: [Wikipedia article content]
Thought: Now I have information about X. Let me check for Y
Action: search_wikipedia("Y")
Observation: [Content]
Thought: I now have all information needed
Action: final_answer("The answer is...")
Implementation:
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_react_agent, AgentExecutor
from langchain_core.prompts import PromptTemplate
# Define tools
@tool
def calculator(expression: str) -> float:
"""Evaluate a math expression."""
return eval(expression)
@tool
def search_wikipedia(query: str) -> str:
"""Search Wikipedia."""
# Implementation here
return f"Results for {query}"
# Create agent
tools = [calculator, search_wikipedia]
model = ChatOpenAI(model="gpt-3.5-turbo")
agent = create_react_agent(model, tools, PromptTemplate.from_template(
"""You are a helpful assistant. Use tools to answer questions.
{agent_scratchpad}
Question: {input}"""))
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# Use agent
result = executor.invoke({"input": "What is 2+2? And who invented calculus?"})
print(result)
Tool-Using Agents
Agents become powerful with access to tools:
from langchain_core.tools import tool
from typing import Any
class ToolLibrary:
"""Collection of tools for agents."""
@staticmethod
@tool
def get_weather(city: str) -> str:
"""Get current weather for a city."""
# Mock implementation
return f"Weather in {city}: Sunny, 72F"
@staticmethod
@tool
def search_web(query: str) -> str:
"""Search the web."""
return f"Search results for: {query}"
@staticmethod
@tool
def calculate(operation: str) -> Any:
"""Perform calculation."""
return eval(operation)
@staticmethod
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Email sent to {to}"
# Create agent with tools
tools = [
ToolLibrary.get_weather,
ToolLibrary.search_web,
ToolLibrary.calculate,
ToolLibrary.send_email
]
agent = create_react_agent(model, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
# Agent can now use any of these tools
result = executor.invoke({
"input": "What's the weather in NYC and send me the forecast?"
})
Single vs. Multi-Agent Systems
Single agent: One agent handles everything
- Simpler, fewer moving parts
- Limited specialization
- Good for straightforward tasks
# Single agent doing everything
agent = create_react_agent(model, [tool1, tool2, tool3, ...])
result = agent.invoke({"input": "Complex task..."})
Multi-agent: Multiple specialized agents
- Better specialization
- Can handle complex workflows
- More complex coordination
# Multiple specialized agents
research_agent = Agent(tools=[search, analyze], role="researcher")
writing_agent = Agent(tools=[write, edit], role="writer")
manager_agent = Agent(role="manager", agents=[research_agent, writing_agent])
# Manager coordinates
result = manager_agent.invoke({"task": "Write about AI"})
Agent Loops and Control Flow
Agents follow loops: observe → think → act → repeat
class AgentLoop:
"""Control agent execution loop."""
def __init__(self, model, tools, max_iterations=10):
self.model = model
self.tools = tools
self.max_iterations = max_iterations
self.iteration = 0
def run(self, task: str):
"""Run agent loop."""
state = {"task": task, "history": []}
while self.iteration < self.max_iterations:
# Think: ask model what to do
thought = self.model.invoke(
f"Task: {state['task']}\nHistory: {state['history']}\nWhat's next?"
)
# Check if done
if "DONE" in thought.content or "FINAL_ANSWER" in thought.content:
return thought.content
# Act: execute tool
action = self.extract_action(thought.content)
result = self.execute_tool(action)
# Observe: update history
state["history"].append({"action": action, "result": result})
self.iteration += 1
return "Max iterations reached"
def extract_action(self, response: str) -> str:
"""Parse action from model response."""
if "ACTION:" in response:
return response.split("ACTION:")[1].split("\n")[0].strip()
return None
def execute_tool(self, action: str) -> Any:
"""Execute the action."""
# Parse action and call appropriate tool
return f"Result of {action}"
Planning vs. Acting
Plan-then-execute: Create full plan first, then execute steps
- Predictable, fewer errors
- Less flexible if plan becomes invalid
- Good for defined processes
def plan_and_execute(task: str):
"""Create plan first, then execute."""
# Step 1: Create plan
plan = model.invoke(f"Create a detailed plan for: {task}")
# Step 2: Execute plan
for step in plan.steps:
result = execute_step(step)
if not result.success:
# Adapt if needed
pass
return result
Think-act-observe: Decide next step iteratively
- Flexible, adaptive
- More iterations needed
- Good for open-ended problems
def think_act_observe(task: str):
"""Decide each step based on progress."""
observation = task
while not done:
# Think about what to do next
next_action = model.invoke(f"What's next? Current state: {observation}")
# Act
result = execute_action(next_action)
# Observe
observation = result
return observation
Error Handling and Recovery
Agents need to handle and recover from errors:
class RobustAgent:
"""Agent with error handling."""
def __init__(self, model, tools):
self.model = model
self.tools = tools
self.max_retries = 3
def execute_with_retry(self, action: str):
"""Execute action with retry on failure."""
for attempt in range(self.max_retries):
try:
result = self.execute_tool(action)
return result
except Exception as e:
if attempt < self.max_retries - 1:
# Ask model how to recover
recovery = self.model.invoke(
f"Tool failed: {e}. How to recover?"
)
else:
raise
def execute_tool(self, action: str):
"""Execute a tool, raise on error."""
# Implementation
pass
Key Takeaway
Agents perceive, reason, and act iteratively. The ReAct pattern interleaves thoughts and actions. Tools give agents capabilities. Plan-then-execute works for structured tasks; think-act-observe for open-ended problems. Single agents are simpler; multi-agents handle complexity. Build robust agents with error handling and recovery.
Exercises
-
Simple agent: Build a basic agent that uses 2-3 tools to solve problems.
-
ReAct: Implement a ReAct agent with proper thought/action/observation formatting.
-
Tool design: Create 3 custom tools for agents. Ensure they’re well-defined with clear inputs/outputs.
-
Control flow: Implement agent loops with iteration limits and stopping conditions.
-
Error handling: Add retry logic and recovery to agent execution.
-
Plan vs. Act: Compare plan-then-execute vs. think-act-observe on the same task.