Agents vs Skills vs Rules: Understanding AI System Layers
Building effective AI systems requires understanding three distinct layers that work together: Agents (the thinkers), Skills (the doers), and Rules (the guardrails). Confusing these layers leads to brittle, hard-to-maintain systems.
The Three-Layer Architecture
┌─────────────────────────────────────┐
│ AGENTS │ ← The Thinkers
│ • Planning & Decision Making │
│ • Coordination & Orchestration │
│ • Context Management │
├─────────────────────────────────────┤
│ SKILLS │ ← The Doers
│ • Capabilities & Actions │
│ • API Integrations │
│ • Data Processing │
├─────────────────────────────────────┤
│ RULES │ ← The Guardrails
│ • Safety & Security │
│ • Business Logic │
│ • Compliance & Governance │
└─────────────────────────────────────┘
Each layer has a specific responsibility and shouldn’t overlap with others.
Agents: The Orchestration Layer
What agents do:
- Receive user requests and understand intent
- Plan multi-step workflows
- Decide which skills to use and when
- Handle errors and retry logic
- Manage conversation context
- Coordinate multiple skills together
What agents DON’T do:
- Implement actual business logic (that’s skills)
- Enforce security policies (that’s rules)
- Directly access external APIs (through skills only)
Agent Example
class EmailAgent:
def __init__(self):
self.skills = {
"search_emails": EmailSearchSkill(),
"compose_email": ComposeSkill(),
"send_email": SendSkill()
}
self.rules = EmailRules()
async def handle_request(self, user_input):
# Agent thinking/planning
if "find emails from" in user_input:
return await self.skills["search_emails"].search(user_input)
elif "send email to" in user_input:
# Check rules before acting
if self.rules.can_send_to(user_input.recipient):
return await self.skills["send_email"].send(user_input)
else:
return "Cannot send: Security policy violation"
Skills: The Capability Layer
What skills do:
- Implement specific business capabilities
- Interface with external systems (APIs, databases)
- Process and transform data
- Execute domain-specific logic
- Return structured results
What skills DON’T do:
- Make decisions about which actions to take
- Handle user interaction directly
- Enforce business rules (they can check rules, but not define them)
Skill Example
class SendEmailSkill:
def __init__(self):
self.email_client = EmailClient()
self.rules = EmailRules() # Can reference rules
async def send(self, to, subject, body):
# Skill implementation
try:
# Reference rules for validation
if not self.rules.is_valid_email(to):
return {"success": False, "error": "Invalid recipient"}
result = await self.email_client.send(to, subject, body)
return {"success": True, "message_id": result.id}
except Exception as e:
return {"success": False, "error": str(e)}
Rules: The Governance Layer
What rules do:
- Define security policies and constraints
- Enforce business logic and compliance
- Validate inputs and outputs
- Set rate limits and quotas
- Audit and log actions
- Handle authorization and permissions
What rules DON’T do:
- Implement any business logic
- Make decisions about workflow
- Directly interact with external systems
Rules Example
class EmailRules:
def __init__(self):
self.blocked_domains = ["spam.com", "malicious.net"]
self.rate_limits = {"hourly": 100, "daily": 1000}
self.usage_log = []
def can_send_to(self, recipient):
# Rule: Check blocked domains
domain = recipient.split("@")[1]
if domain in self.blocked_domains:
return False
# Rule: Check rate limits
if self.check_rate_limit():
return False
# Rule: Log for audit
self.log_action("send_attempt", recipient)
return True
def is_valid_email(self, email):
# Rule: Email format validation
return "@" in email and "." in email.split("@")[1]
Why Separation Matters
1. Reusability
# Skills can be reused across agents
email_skill = SendEmailSkill()
customer_agent = CustomerServiceAgent(email_skill)
marketing_agent = MarketingAgent(email_skill)
2. Testability
# Each layer can be tested independently
def test_skill():
skill = SendEmailSkill()
result = skill.send("[email protected]", "Subject", "Body")
assert result["success"] == True
def test_rules():
rules = EmailRules()
assert rules.can_send_to("[email protected]") == True
assert rules.can_send_to("[email protected]") == False
3. Maintainability
- Bug in email sending? Fix the skill, not the agent
- New security policy? Update rules, not skills
- Change workflow logic? Modify agent, not skills
4. Security
- Skills can’t bypass security (rules enforce it)
- Agents can’t directly access external systems
- Clear audit trail through rule layer
Common Anti-Patterns
❌ Mixing Responsibilities
# BAD: Agent implementing business logic
class BadAgent:
async def send_email(self, to, subject):
# Agent shouldn't implement email logic
if "spam" in subject.lower():
return "Cannot send spam" # This is a rule!
# Agent shouldn't call APIs directly
smtp = SMTPClient()
return smtp.send(to, subject) # This is a skill!
✅ Proper Separation
# GOOD: Clear separation of concerns
class GoodAgent:
def __init__(self):
self.skills = {"email": SendEmailSkill()}
self.rules = EmailRules()
async def handle_request(self, request):
if self.rules.validate(request):
return await self.skills["email"].send(request)
else:
return "Request blocked by policy"
Interaction Patterns
Pattern 1: Agent → Skill → Rules
Agent decides to send email
↓
Skill implements email sending
↓
Skill checks rules before sending
↓
Rules validate and allow/deny
Pattern 2: Agent → Rules → Skill
Agent wants to send email
↓
Agent checks rules first
↓
Rules validate the request
↓
If allowed, agent calls skill
Pattern 3: Skill → Rules (Internal Validation)
Skill receives request from agent
↓
Skill internally validates against rules
↓
Skill executes or rejects based on rules
Real-World Example: Document Processing Agent
# Agent Layer
class DocumentAgent:
def __init__(self):
self.skills = {
"ocr": OCRSkill(),
"summarize": SummarySkill(),
"classify": ClassificationSkill(),
"store": StorageSkill()
}
self.rules = DocumentRules()
async def process_document(self, file_path):
# Agent: Plan the workflow
if not self.rules.can_process(file_path):
return "File rejected by policy"
# Agent: Coordinate skills
text = await self.skills["ocr"].extract(file_path)
summary = await self.skills["summarize"].create(text)
category = await self.skills["classify"].determine(text)
# Agent: Store results
result = await self.skills["store"].save({
"summary": summary,
"category": category,
"original": file_path
})
return result
# Skill Layer
class OCRSkill:
async def extract(self, file_path):
# Skill: Implement OCR logic
ocr_service = OCRService()
return await ocr_service.extract_text(file_path)
# Rules Layer
class DocumentRules:
def __init__(self):
self.allowed_formats = ["pdf", "docx", "jpg"]
self.max_size_mb = 50
def can_process(self, file_path):
# Rules: Validate file
if not self.is_allowed_format(file_path):
return False
if not self.is_under_size_limit(file_path):
return False
return True
Implementation Best Practices
1. Clear Interfaces
# Define interfaces between layers
class AgentInterface:
async def process(self, request: Request) -> Response:
pass
class SkillInterface:
async def execute(self, params: dict) -> Result:
pass
class RulesInterface:
def validate(self, action: Action) -> bool:
pass
2. Dependency Injection
# Inject dependencies, don't create them
class Agent:
def __init__(self, skills: dict, rules: RulesInterface):
self.skills = skills
self.rules = rules
3. Configuration-Driven Rules
# rules.yaml
email:
blocked_domains: ["spam.com"]
rate_limits:
hourly: 100
daily: 1000
allowed_senders: ["@company.com"]
4. Observability
# Log at each layer
class Skill:
async def execute(self, params):
logger.info(f"Skill executing: {self.__class__.__name__}")
result = await self._do_work(params)
logger.info(f"Skill completed: {result}")
return result
Testing Strategy
Unit Tests (Per Layer)
# Test agent logic
def test_agent_planning():
agent = Agent(mock_skills, mock_rules)
plan = agent.plan_workflow("send email to user")
assert plan.steps == ["validate", "compose", "send"]
# Test skill implementation
def test_skill_execution():
skill = EmailSkill()
result = skill.send("[email protected]", "Subject", "Body")
assert result.success == True
# Test rule validation
def test_rule_validation():
rules = EmailRules()
assert rules.can_send_to("[email protected]") == True
assert rules.can_send_to("[email protected]") == False
Integration Tests (Cross-Layer)
def test_agent_to_skill_integration():
# Test agent correctly calls skill
agent = Agent(real_skills, mock_rules)
result = agent.handle_request("send email to [email protected]")
assert "message_id" in result
Evolution of the Architecture
As your system grows, you might add:
Additional Layers
- Monitoring Layer: Track performance, errors, usage
- Caching Layer: Improve performance with smart caching
- Security Layer: Advanced threat detection and response
Advanced Patterns
- Skill Composition: Combine multiple skills into meta-skills
- Rule Hierarchies: Global, team, and individual rule sets
- Agent Networks: Multiple agents coordinating across systems
Key Takeaway: The separation of agents, skills, and rules isn’t just architecture — it’s about creating maintainable, secure, and scalable AI systems. Each layer has a clear responsibility, and respecting those boundaries is key to success.
Next: Learn about common architecture patterns for implementing these layers effectively.