Building AI Agents: 3 Core Patterns That Actually Work
Learn the three proven architectural patterns for building AI agents: ReAct, Plan-Execute, and Reflection. Includes a complete code example of a file management agent using the ReAct pattern.
Building AI Agents: 3 Core Patterns That Actually Work
AI agents are everywhere these days, but most discussions focus on what they can do rather than how to build them. After working with various agent implementations, I've found that successful agents typically follow one of three core architectural patterns.
Let me walk you through each pattern and show you how to implement one with concrete code.
Pattern 1: ReAct (Reasoning + Acting)
The ReAct pattern combines reasoning and action in a loop. The agent thinks about what to do, takes an action, observes the result, then repeats.
When to use it: Simple to moderate tasks that benefit from step-by-step reasoning.
Structure:
- Thought: Agent reasons about current situation
- Action: Agent executes a tool or function
- Observation: Agent sees the result
- Repeat until task complete
Pattern 2: Plan-Execute
This pattern separates planning from execution. The agent first creates a complete plan, then executes each step.
When to use it: Complex, multi-step tasks where upfront planning reduces errors.
Structure:
- Plan: Create detailed step-by-step plan
- Execute: Run each step in sequence
- Monitor: Check if plan needs adjustment
Pattern 3: Reflection
The agent performs actions, then critiques its own work and iterates.
When to use it: Tasks requiring quality refinement, like writing or code generation.
Structure:
- Generate: Create initial output
- Reflect: Analyze and critique the output
- Refine: Improve based on reflection
- Repeat until satisfied
Building a ReAct Agent: File Manager Example
Let's implement a file management agent using the ReAct pattern. This agent can create, read, and organize files based on natural language requests.
Core Agent Class
import os
import json
from typing import Dict, Any, List
class FileManagerAgent:
def __init__(self, working_directory: str = "./workspace"):
self.working_dir = working_directory
self.max_iterations = 10
# Ensure working directory exists
os.makedirs(working_directory, exist_ok=True)
# Available tools
self.tools = {
"create_file": self.create_file,
"read_file": self.read_file,
"list_files": self.list_files,
"delete_file": self.delete_file
}
def create_file(self, filename: str, content: str) -> str:
"""Create a new file with given content."""
filepath = os.path.join(self.working_dir, filename)
try:
with open(filepath, 'w') as f:
f.write(content)
return f"Successfully created {filename}"
except Exception as e:
return f"Error creating file: {str(e)}"
def read_file(self, filename: str) -> str:
"""Read content from a file."""
filepath = os.path.join(self.working_dir, filename)
try:
with open(filepath, 'r') as f:
content = f.read()
return f"Content of {filename}:\n{content}"
except FileNotFoundError:
return f"File {filename} not found"
except Exception as e:
return f"Error reading file: {str(e)}"
def list_files(self) -> str:
"""List all files in the working directory."""
try:
files = os.listdir(self.working_dir)
if not files:
return "No files found in directory"
return f"Files in directory: {', '.join(files)}"
except Exception as e:
return f"Error listing files: {str(e)}"
def delete_file(self, filename: str) -> str:
"""Delete a file."""
filepath = os.path.join(self.working_dir, filename)
try:
os.remove(filepath)
return f"Successfully deleted {filename}"
except FileNotFoundError:
return f"File {filename} not found"
except Exception as e:
return f"Error deleting file: {str(e)}"
ReAct Loop Implementation
def execute_task(self, task: str) -> str:
"""Execute a task using ReAct pattern."""
conversation = [
f"Task: {task}",
"I need to break this down step by step."
]
for iteration in range(self.max_iterations):
# Thought phase
thought = self._generate_thought(conversation, task)
conversation.append(f"Thought: {thought}")
# Check if task is complete
if "task complete" in thought.lower() or "finished" in thought.lower():
break
# Action phase
action = self._generate_action(conversation)
conversation.append(f"Action: {action}")
# Execute action and observe
observation = self._execute_action(action)
conversation.append(f"Observation: {observation}")
return "\n".join(conversation)
def _generate_thought(self, conversation: List[str], task: str) -> str:
"""Generate next thought based on conversation history."""
# In a real implementation, this would call an LLM
# For this example, we'll use simple heuristics
recent_context = "\n".join(conversation[-3:]) if conversation else ""
if "create" in task.lower() and "file" in task.lower():
if not any("create_file" in line for line in conversation):
return "I need to create a file. Let me identify the filename and content."
elif "Successfully created" in recent_context:
return "File created successfully. Task complete."
if "list" in task.lower() and "file" in task.lower():
if not any("list_files" in line for line in conversation):
return "I need to list the files in the directory."
else:
return "Files listed successfully. Task complete."
return "Let me think about what to do next."
def _generate_action(self, conversation: List[str]) -> str:
"""Generate next action based on conversation."""
# Simple action generation logic
recent = "\n".join(conversation[-2:]).lower()
if "create a file" in recent:
return "create_file('example.txt', 'Hello, world!')"
elif "list the files" in recent:
return "list_files()"
return "list_files()" # Default action
def _execute_action(self, action_str: str) -> str:
"""Parse and execute an action string."""
try:
# Parse action string (simplified)
if action_str.startswith("create_file"):
# Extract filename and content from action string
# This is simplified - real implementation would need better parsing
return self.create_file("example.txt", "Hello, world!")
elif action_str.startswith("list_files"):
return self.list_files()
else:
return "Unknown action"
except Exception as e:
return f"Error executing action: {str(e)}"
Using the Agent
# Create and use the agent
agent = FileManagerAgent()
# Execute a task
result = agent.execute_task("Create a file called hello.txt with a greeting")
print(result)
# Output:
# Task: Create a file called hello.txt with a greeting
# I need to break this down step by step.
# Thought: I need to create a file. Let me identify the filename and content.
# Action: create_file('example.txt', 'Hello, world!')
# Observation: Successfully created example.txt
# Thought: File created successfully. Task complete.
Key Implementation Lessons
1. Keep State Simple
The agent's state should be minimal and observable. In our example, the conversation history serves as both memory and debugging log.
2. Make Tools Robust
Each tool should handle errors gracefully and return informative messages. This helps the agent understand what went wrong and try alternatives.
3. Limit Iterations
Always include a maximum iteration limit to prevent infinite loops. Real agents can get stuck in reasoning cycles.
4. Design for Debugging
The conversation log makes it easy to see exactly what the agent was thinking and why it made specific decisions.
Choosing the Right Pattern
- ReAct: Start here for most tasks. Good balance of simplicity and capability.
- Plan-Execute: Use for complex workflows with clear dependencies between steps.
- Reflection: Add when output quality matters more than speed.
You can also combine patterns. For example, use Plan-Execute with ReAct for individual steps, or add Reflection to any pattern when quality is critical.
What Makes This Work
The key insight is that AI agents are not magic. They're software systems that follow predictable patterns:
- Clear interfaces: Each tool has a specific purpose and clear inputs/outputs
- Observable state: You can always see what the agent is thinking
- Error handling: Things go wrong, and the system handles it gracefully
- Bounded execution: Clear stopping conditions prevent runaway processes
These patterns provide structure for building reliable AI agents that actually solve real problems.