The Architecture of Intelligent Systems: 15 Agentic AI Patterns for Production
The Architecture of Intelligent Systems: 15 Agentic AI Patterns for Production
Introduction
The evolution from prompt-based AI to autonomous agents represents one of the most significant shifts in software architecture since microservices. Unlike traditional applications where logic flows through deterministic code paths, agentic systems make decisions, use tools, reason through problems, and adapt their behavior based on context.
Building production-ready AI agents isn’t about stringing together LLM calls—it’s about implementing proven architectural patterns that handle failures gracefully, scale efficiently, and provide predictable behavior despite the non-deterministic nature of language models. These patterns emerge from real production systems processing millions of requests, where reliability and cost efficiency aren’t optional.
This guide explores 15 battle-tested patterns for building intelligent systems that work in production environments where uptime, latency, and cost matter.
Pattern 1: ReAct (Reasoning + Acting)
The ReAct pattern interleaves reasoning and action, allowing agents to think through problems step-by-step while taking actions based on those thoughts.
Architecture:
1
Thought → Action → Observation → Thought → Action → ...
Implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class ReActAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = {tool.name: tool for tool in tools}
self.max_iterations = 10
def run(self, task: str) -> str:
scratchpad = []
for i in range(self.max_iterations):
# Reasoning step
prompt = self._build_prompt(task, scratchpad)
response = self.llm.generate(prompt)
thought = self._extract_thought(response)
action = self._extract_action(response)
scratchpad.append(f"Thought: {thought}")
scratchpad.append(f"Action: {action}")
# Acting step
if action.startswith("Final Answer:"):
return action.replace("Final Answer:", "").strip()
# Execute tool
tool_name, tool_input = self._parse_action(action)
observation = self.tools[tool_name].execute(tool_input)
scratchpad.append(f"Observation: {observation}")
return "Agent exceeded maximum iterations"
def _build_prompt(self, task: str, scratchpad: list) -> str:
return f"""Answer the following question: {task}
You have access to these tools:
{self._format_tools()}
Use this format:
Thought: [your reasoning]
Action: [tool_name: input]
Observation: [result will be provided]
... (repeat Thought/Action/Observation as needed)
Thought: I now know the final answer
Action: Final Answer: [your answer]
Begin!
{chr(10).join(scratchpad)}
Thought:"""
When to Use:
- Complex tasks requiring multi-step reasoning
- Tool usage depends on intermediate results
- Transparency in decision-making is important
Production Considerations:
- Set maximum iterations to prevent infinite loops
- Log all thoughts and actions for debugging
- Implement timeout mechanisms
- Cost: ~3-5x more expensive than single-shot inference
Pattern 2: Plan-and-Execute
Separate planning from execution—create a complete plan upfront, then execute steps sequentially.
Architecture:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class PlanAndExecuteAgent:
def __init__(self, planner_llm, executor_llm, tools):
self.planner = planner_llm
self.executor = executor_llm
self.tools = tools
def run(self, task: str) -> str:
# Planning phase
plan = self.planner.generate(f"""Create a step-by-step plan to: {task}
Available tools: {self._list_tools()}
Return a numbered list of steps.""")
steps = self._parse_plan(plan)
results = []
# Execution phase
for step in steps:
result = self._execute_step(step, results)
results.append(result)
# Replanning if step fails
if result.failed:
plan = self._replan(task, steps, results)
steps = self._parse_plan(plan)
return self._synthesize_final_answer(results)
When to Use:
- Tasks with clear sequential dependencies
- Want to estimate cost/time before execution
- Need to parallelize independent steps
Advantages:
- Predictable execution flow
- Can optimize plan before execution
- Easier to cache and reuse plans
Pattern 3: Reflection and Self-Critique
Agent evaluates its own outputs and iterates until quality threshold is met.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ReflectiveAgent:
def __init__(self, generator_llm, critic_llm):
self.generator = generator_llm
self.critic = critic_llm
self.max_iterations = 3
def generate_with_reflection(self, task: str) -> str:
for iteration in range(self.max_iterations):
# Generate output
output = self.generator.generate(task)
# Self-critique
critique = self.critic.generate(f"""
Evaluate this response to: {task}
Response: {output}
Rate from 1-10 and provide specific improvements needed.
If score >= 8, respond with "APPROVED"
""")
if "APPROVED" in critique:
return output
# Refine based on critique
task = f"{task}\n\nPrevious attempt: {output}\nFeedback: {critique}\nImprove the response."
return output # Return best attempt
Production Use Cases:
- Code generation (syntax validation)
- Content creation (fact-checking, tone adjustment)
- Data extraction (schema validation)
Cost Trade-offs:
- 2-3x token usage vs single-pass
- Significantly higher quality output
- Fewer downstream errors
Pattern 4: Multi-Agent Collaboration
Specialized agents work together, each handling specific domains.
Architecture Patterns:
A. Hierarchical (Manager-Worker)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ManagerAgent:
def __init__(self, specialist_agents):
self.specialists = specialist_agents
def delegate(self, task: str):
# Decompose task
subtasks = self._decompose(task)
# Assign to specialists
results = {}
for subtask in subtasks:
agent = self._select_specialist(subtask)
results[subtask.id] = agent.execute(subtask)
# Synthesize results
return self._synthesize(results)
B. Peer-to-Peer (Debate)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DebateSystem:
def __init__(self, agents):
self.agents = agents
self.moderator = ModeratorAgent()
def debate(self, question: str, rounds: int = 3):
responses = []
for round in range(rounds):
for agent in self.agents:
context = self._format_debate_history(responses)
response = agent.argue(question, context)
responses.append(response)
return self.moderator.synthesize(responses)
When to Use:
- Domain-specific expertise needed (legal + technical + financial)
- Want diverse perspectives (multiple reasoning approaches)
- Need verification (agent checks another agent’s work)
Pattern 5: Tool-Augmented Generation
Agents use external tools to extend their capabilities beyond text generation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from typing import Callable, Dict
class Tool:
def __init__(self, name: str, description: str, func: Callable):
self.name = name
self.description = description
self.func = func
def execute(self, input: str) -> str:
try:
result = self.func(input)
return f"Success: {result}"
except Exception as e:
return f"Error: {str(e)}"
# Example tools
def search_database(query: str) -> str:
"""Search PostgreSQL database"""
# Implementation
return results
def call_api(endpoint: str, params: dict) -> str:
"""Make HTTP API call"""
# Implementation
return response
def execute_python(code: str) -> str:
"""Execute Python in sandbox"""
# Implementation with timeout and resource limits
return output
tools = [
Tool("search_db", "Search company database", search_database),
Tool("api_call", "Call external API", call_api),
Tool("python", "Execute Python code", execute_python),
]
Critical Production Patterns:
- Sandboxing: Execute code in isolated containers
- Timeouts: Prevent hanging tool calls
- Rate limiting: Protect external APIs
- Error handling: Graceful degradation when tools fail
- Logging: Track tool usage for debugging and cost analysis
Pattern 6: Memory and Context Management
Short-term vs Long-term Memory:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AgentMemory:
def __init__(self, vector_db, sql_db):
self.working_memory = [] # Current conversation
self.episodic_memory = vector_db # Past interactions (embeddings)
self.semantic_memory = sql_db # Facts and knowledge
def remember(self, interaction: dict):
# Add to working memory
self.working_memory.append(interaction)
# Trim if exceeds context window
if self._token_count() > self.max_tokens:
self._summarize_old_context()
# Store important interactions long-term
if interaction['importance'] > 0.7:
embedding = self._embed(interaction)
self.episodic_memory.store(embedding, interaction)
def recall(self, query: str, k: int = 5):
# Semantic search for relevant past interactions
query_embedding = self._embed(query)
similar = self.episodic_memory.search(query_embedding, k)
return similar
Context Window Management Strategies:
| Strategy | Use Case | Cost Impact |
|---|---|---|
| Sliding Window | Chatbots, conversations | Low - drops old messages |
| Summarization | Long documents | Medium - periodic LLM calls |
| Retrieval (RAG) | Knowledge bases | Low - only relevant chunks |
| Hierarchical | Multi-session tasks | High - nested summaries |
Pattern 7: Guardrails and Safety Layers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SafeAgent:
def __init__(self, agent, guardrails):
self.agent = agent
self.input_filter = guardrails['input']
self.output_filter = guardrails['output']
self.action_validator = guardrails['actions']
def run(self, user_input: str) -> str:
# Input validation
if not self.input_filter.is_safe(user_input):
return "I cannot process that request"
# Agent execution with action validation
response = self.agent.run(user_input,
action_callback=self.action_validator)
# Output filtering
if not self.output_filter.is_safe(response):
return "I apologize, I cannot provide that information"
return response
Essential Guardrails:
- Prompt Injection Detection: Identify attempts to override instructions
- PII Filtering: Remove sensitive information
- Action Whitelisting: Only allow approved tool usage
- Content Moderation: Block harmful outputs
- Rate Limiting: Prevent abuse
Pattern 8: Streaming and Progressive Response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import asyncio
from typing import AsyncIterator
class StreamingAgent:
async def stream_response(self, query: str) -> AsyncIterator[str]:
# Stream thinking process
async for thought in self.agent.think(query):
yield f"🤔 {thought}\n"
# Stream tool usage
async for action in self.agent.act():
yield f"🔧 Using: {action.tool}\n"
result = await action.execute()
yield f"📊 Result: {result}\n"
# Stream final answer
async for chunk in self.agent.answer():
yield chunk
Why It Matters:
- Perceived performance improvement
- User engagement (see agent thinking)
- Early cancellation (stop bad responses)
- Lower time-to-first-token
Pattern 9: Fallback and Error Recovery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ResilientAgent:
def __init__(self, primary_llm, fallback_llm, cache):
self.primary = primary_llm
self.fallback = fallback_llm
self.cache = cache
def generate(self, prompt: str, max_retries: int = 3):
# Check cache first
if cached := self.cache.get(prompt):
return cached
for attempt in range(max_retries):
try:
response = self.primary.generate(prompt)
self.cache.set(prompt, response)
return response
except RateLimitError:
# Exponential backoff
await asyncio.sleep(2 ** attempt)
except ModelOverloadedError:
# Switch to fallback
response = self.fallback.generate(prompt)
return response
except ContextLengthError:
# Truncate and retry
prompt = self._truncate_prompt(prompt)
raise AgentFailureError("All retry attempts failed")
Pattern 10: Evaluation-Driven Development
Continuous Evaluation Loop:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class AgentEvaluator:
def __init__(self, test_cases, metrics):
self.test_cases = test_cases
self.metrics = metrics
def evaluate(self, agent_version: str) -> dict:
results = {
'accuracy': [],
'latency': [],
'cost': [],
'tool_success_rate': []
}
for test in self.test_cases:
start = time.time()
response = agent.run(test.input)
latency = time.time() - start
results['accuracy'].append(
self._score_response(response, test.expected)
)
results['latency'].append(latency)
results['cost'].append(response.tokens * COST_PER_TOKEN)
return {
metric: np.mean(values)
for metric, values in results.items()
}
Key Metrics to Track:
- Task completion rate
- Average cost per task
- P95 latency
- Tool usage patterns
- Error rates by type
Patterns 11-15: Advanced Techniques
Pattern 11: Constitutional AI (Principle-Based Behavior)
Define principles that guide agent behavior, self-critique against those principles.
Pattern 12: Chain-of-Thought Prompting
Explicit step-by-step reasoning before answering, improves accuracy on complex tasks.
Pattern 13: Few-Shot Learning with Examples
Provide examples of desired behavior in the prompt, especially for structured outputs.
Pattern 14: Retrieval-Augmented Generation (RAG)
Fetch relevant context from external knowledge base before generating response.
Pattern 15: Function Calling with Schema Validation
Use structured outputs with JSON Schema validation to ensure reliable tool calls.
Production Deployment Checklist
Before deploying agents to production:
✅ Observability
- Log all LLM calls with latency and cost
- Track tool usage patterns
- Monitor error rates by failure type
- Set up alerts for cost anomalies
✅ Cost Management
- Implement caching for repeated queries
- Use cheaper models for simple tasks
- Set per-user budget limits
- Cache embeddings and tool results
✅ Safety
- Content filtering on inputs and outputs
- Action whitelisting for tools
- PII detection and redaction
- Rate limiting per user
✅ Testing
- Regression test suite with expected outputs
- Load testing for concurrent users
- Chaos testing (API failures, timeouts)
- Red team testing for prompt injection
✅ User Experience
- Streaming for long responses
- Progress indicators during tool usage
- Graceful error messages
- Ability to cancel long-running tasks
Common Pitfalls to Avoid
❌ Over-Engineering Early Start simple (single agent, few tools), add complexity only when needed.
❌ Ignoring Costs LLM calls add up fast. Track costs per user, per task type. Cache aggressively.
❌ No Evaluation Framework You can’t improve what you don’t measure. Build eval harness from day one.
❌ Deterministic Expectations Agents will surprise you. Design for non-determinism with retries and validation.
❌ Unbounded Tool Access Every tool is a potential failure point and cost center. Start with minimal tools.
Conclusion
Agentic AI patterns transform how we build software, shifting from deterministic control flow to systems that reason, adapt, and use tools autonomously. The patterns outlined here emerge from production systems processing millions of agent requests, where reliability and cost efficiency determine success.
The key insight: successful agent systems aren’t about the latest model or framework—they’re about architectural discipline. Start simple with ReAct or Plan-and-Execute, add memory when context matters, layer in safety guardrails, and continuously evaluate. The most sophisticated pattern is useless without proper observability, cost tracking, and error handling.
As agent capabilities expand, these patterns will evolve, but the fundamentals remain: clear separation of concerns, graceful failure handling, and metrics-driven iteration. Build systems that work reliably today, architect them to adapt tomorrow.
Suggested Reading
- LangChain Documentation: Agent Types - Comprehensive guide to agent architectures
- Anthropic Research: Constitutional AI - Principle-based agent behavior
- OpenAI Function Calling Guide - Structured tool usage
- ReAct: Synergizing Reasoning and Acting in Language Models - Original ReAct paper
- LlamaIndex Documentation: Agents - Production agent patterns
- AWS Architecture: Building AI Agents - Infrastructure considerations