Skip to content

Single Agent Architecture: The Foundation

Overview

Before scaling to multiple agents, you need a solid single-agent architecture. This is the foundation all other patterns build upon.

A well-designed single agent is simpler to debug, easier to test, and more predictable than complex multi-agent systems.


Single Agent Anatomy

- ┌──────────────────────────────────────────────────────┐
    - SINGLE AGENT ARCHITECTURE                    │
  - ┤
│                                                      │
    - ┌─────────────────────────────────────────────────┐ │
        - INPUT INTERFACE                                 │ │
        - (User query, API request, task)                │ │
      - ┬──────────────────────────────┘ │
│                     │                                │
    - ┌──────────────────▼──────────────────────────────┐ │
        - PERCEPTION LAYER                               │ │
        - • Parse input                                  │ │
        - • Check constraints                           │ │
        - • Access memory/context                       │ │
      - ┬──────────────────────────────┘ │
│                     │                                │
    - ┌──────────────────▼──────────────────────────────┐ │
        - LLM REASONING LAYER                            │ │
        - • Understand goal                              │ │
        - • Decide action                                │ │
        - • Consider alternatives                        │ │
      - ┬──────────────────────────────┘ │
│                     │                                │
    - ┌──────────────────▼──────────────────────────────┐ │
        - PLANNING LAYER (Optional)                      │ │
        - • Decompose complex goals                      │ │
        - • Create execution plan                        │ │
        - • Check feasibility                            │ │
      - ┬──────────────────────────────┘ │
│                     │                                │
    - ┌──────────────────▼──────────────────────────────┐ │
        - TOOL INTERFACE LAYER                           │ │
        - • Select appropriate tool                      │ │
        - • Call tool with args                          │ │
        - • Handle errors                                │ │
      - ┬──────────────────────────────┘ │
│                     │                                │
    - ┌──────────────────▼──────────────────────────────┐ │
        - EXTERNAL SYSTEMS                               │ │
        - • APIs, databases, search, files               │ │
        - • Real-world interaction                       │ │
      - ┬──────────────────────────────┘ │
│                     │                                │
    - ┌──────────────────▼──────────────────────────────┐ │
        - REFLECTION LAYER                               │ │
        - • Check if goal achieved                       │ │
        - • Evaluate result quality                      │ │
        - • Decide to loop or stop                       │ │
      - ┬──────────────────────────────┘ │
│                     │                                │
    - ┌──────────────────▼──────────────────────────────┐ │
        - OUTPUT INTERFACE                               │ │
        - (Response to user, result, action taken)       │ │
      - ┘ │
│                                                      │
  - ┘

Design Principles for Single Agents

1. Modularity

Each layer is independent:

class ModularAgent:
    def __init__(self):
        self.perception = PerceptionModule()
        self.reasoner = ReasoningModule()
        self.planner = PlanningModule()
        self.executor = ExecutionModule()
        self.reflector = ReflectionModule()

    def run(self, task):
        perceived = self.perception.process(task)
        decision = self.reasoner.reason(perceived)
        plan = self.planner.plan(decision)
        result = self.executor.execute(plan)
        reflection = self.reflector.reflect(result)
        return reflection.output

Benefits: - Each layer testable independently - Easy to debug - Can upgrade one layer without others - Clear responsibility

2. State Management

Agent maintains clear state:

class AgentState:
    goal: str
    context: List[str]
    completed_steps: List[str]
    current_findings: Dict
    iteration_count: int

    def is_valid(self):
        """Check state invariants"""
        return (
            self.goal is not None and
            self.iteration_count < MAX_ITERATIONS
        )

Benefits: - Explicit (not hidden in LLM) - Debuggable (see exact state) - Persistable (can checkpoint) - Testable (assert state properties)

3. Error Handling

Graceful degradation:

def robust_execution(task):
    try:
        # Primary approach
        return execute_with_best_method(task)
    except PrimaryError:
        try:
            # Secondary approach
            return execute_with_fallback(task)
        except SecondaryError:
            # Graceful degradation
            return partial_solution(task)

Benefits: - System doesn't crash - Partial results better than none - Users see what worked - Observability of failures

4. Observability

Log everything:

class ObservableAgent:
    def run(self, task):
        self.log_event("start", task=task)

        perceived = self.perceive(task)
        self.log_event("perceived", state=perceived)

        decision = self.reason(perceived)
        self.log_event("decision", action=decision)

        result = self.execute(decision)
        self.log_event("executed", result=result)

        reflection = self.reflect(result)
        self.log_event("reflected", output=reflection)

        return reflection

Benefits: - Full audit trail - Debugging support - Performance analysis - User transparency


Common Single Agent Patterns

Pattern 1: Simple Tool-Using Agent

class SimpleToolAgent:
    def __init__(self, tools: Dict):
        self.llm = LLM()
        self.tools = tools
        self.memory = []

    def run(self, goal: str) -> str:
        context = f"Goal: {goal}\n"

        while True:
            # Decide what to do
            response = self.llm.generate(
                prompt=context,
                available_tools=list(self.tools.keys())
            )

            if response.type == "final_answer":
                return response.content

            # Use tool
            tool_result = self.tools[response.tool](
                **response.args
            )
            context += f"\n{response.tool} result: {tool_result}"
            self.memory.append(tool_result)

Use When: Task requires simple tool calls
Complexity: Low
Reliability: High


Pattern 2: Planning-Based Agent

class PlanningAgent:
    def run(self, goal: str) -> str:
        # Step 1: Create plan
        plan = self.llm.create_plan(goal)
        # plan = ["Step 1: ...", "Step 2: ...", "Step 3: ..."]

        # Step 2: Execute each step
        results = []
        for i, step in enumerate(plan):
            self.llm_context = f"Executing step {i+1}/{len(plan)}: {step}"
            result = self.execute_step(step)
            results.append(result)

        # Step 3: Compile results
        final = self.llm.compile(goal, results)
        return final

Use When: Complex multi-step tasks
Complexity: Medium
Reliability: High


Pattern 3: Reflection-Enhanced Agent

class ReflectionAgent:
    def run(self, goal: str) -> str:
        # Generate
        output = self.generate(goal)

        # Reflect
        critique = self.llm.critique(output)

        if critique.quality_score < 0.8:
            # Improve
            output = self.improve(output, critique)

        return output

Use When: Quality is paramount
Complexity: Medium
Reliability: Very High


Pattern 4: Stateful Agent with Memory

class StatefulAgent:
    def __init__(self):
        self.memory = Memory()
        self.state = AgentState()

    def run(self, goal: str) -> str:
        self.state.goal = goal

        while self.state.iteration_count < MAX_ITERATIONS:
            # Perceive with memory
            context = self._build_context()

            # Reason
            decision = self.llm.decide(context)

            # Act
            result = self.execute(decision)

            # Update state and memory
            self.state.completed_steps.append(str(decision))
            self.memory.add(result)
            self.state.iteration_count += 1

            # Reflect
            if self.goal_achieved():
                break

        return self.state.result

    def _build_context(self):
        """Build context including memory"""
        recent = self.memory.recall(self.state.goal)
        return {
            "goal": self.state.goal,
            "completed": self.state.completed_steps,
            "memory": recent
        }

Use When: Multi-turn, learning needed
Complexity: High
Reliability: High


Architecture Decision Points

Decision 1: Loop vs Single Pass

Single Pass (RAG-style):
  Input → Process → Output
  Latency: <1s
  Cost: Low
  Quality: Medium
  Use: Simple Q&A

Loop-Based (Agentic):
  Input → Loop → Output
  Latency: 5-60s
  Cost: Higher
  Quality: High
  Use: Complex tasks

Decision 2: Planning vs Reactive

Reactive:
  See situation → Act → See result → Adapt

  Agent: "I see we need data. Let me search."
  Flexible, adapts to surprises
  Harder to explain

Planning-Based:
  Analyze goal → Create plan → Execute steps

  Agent: "Goal requires 3 steps. Here's the plan:"
  Predictable, easier to follow
  Less flexible

Decision 3: Tool-Rich vs Tool-Light

Tool-Rich:
  ✓ Can do many things
  ✓ Powerful
  ✗ More complex to manage
  ✗ Higher error surface

Tool-Light:
  ✓ Simple, focused
  ✓ Easy to debug
  ✗ Limited capabilities
  ✗ Can't do everything

Implementation Checklist

Core Components:
  ✓ LLM selection (model, API, cost)
  ✓ Tool definition (what can agent do)
  ✓ Tool calling (function signature specification)
  ✓ Error handling (what if tool fails?)
  ✓ State management (what's tracked?)

Optional Enhancements:
  ✓ Memory system (remember past?)
  ✓ Reflection (self-critique?)
  ✓ Planning (decompose first?)
  ✓ Routing (classify before acting?)

Production Requirements:
  ✓ Observability (log everything)
  ✓ Error recovery (graceful degradation)
  ✓ Testing framework (how verify?)
  ✓ Monitoring (track quality)
  ✓ User interface (how humans interact?)

Scaling from Single Agent

When does single agent stop working?

Single Agent Sufficient:
  ✓ < 5 tools
  ✓ < 30 second latency acceptable
  ✓ 1-2 task types
  ✓ < 10,000 users
  ✓ Quality < 95%

Consider Multi-Agent:
  ✗ > 5 specialized tasks
  ✗ Need different specialists
  ✗ Parallel work beneficial
  ✗ > 10,000 concurrent users
  ✗ Quality > 95% needed

Single Agent Best Practices

1. Start Simple

Week 1: LLM + 1-2 tools + error handling
Week 2: Add memory if needed
Week 3: Add planning or reflection if quality issues

2. Test Thoroughly

def test_agent():
    assert agent.run("simple query") is not None
    assert agent.run("complex task") succeeds
    assert agent.handles_errors_gracefully()
    assert agent.respects_constraints()
    assert agent.logs_everything()

3. Monitor Continuously

Key Metrics:
- Success rate (% goals achieved)
- Quality score (accuracy)
- Latency (time to result)
- Cost per call
- Error rate
- User satisfaction

4. Iterate Based on Feedback

Feedback → Metric Analysis → Identify Issue
                                    ↓
- ┌──────────────────┴──────────────────┐
                   ↓                                      ↓
            Quality Issue?                      Speed Issue?
                   │                                      │
            Add Reflection/CoT              Remove loop/Reflection
            Use better model                Simplify approach
            Improve prompts                  Use RAG instead

Example: Production-Grade Single Agent

class ProductionSingleAgent:
    def __init__(self, config: AgentConfig):
        self.config = config
        self.llm = LLM(model=config.model)
        self.tools = self._init_tools(config.tools)
        self.memory = Memory(backend=config.memory_backend)
        self.logger = setup_logging()

    def run(self, task: Task) -> Result:
        """Main agent loop"""
        self.logger.info(f"Starting task: {task.id}")

        state = AgentState(goal=task.goal)

        try:
            while state.iteration_count < self.config.max_iterations:
                # Perceive
                context = self._build_context(state)
                self.logger.debug(f"Context size: {len(context)}")

                # Reason
                decision = self.llm.decide(
                    context=context,
                    tools=list(self.tools.keys())
                )
                self.logger.info(f"Decided: {decision.action}")

                # Act
                try:
                    result = self.tools[decision.action](
                        **decision.args
                    )
                    self.logger.info(f"Action succeeded: {decision.action}")
                except ToolError as e:
                    self.logger.warning(f"Tool error: {e}")
                    result = self._handle_tool_error(e, state)

                # Update state
                state.completed_steps.append(decision)
                state.last_result = result
                state.iteration_count += 1

                # Store in memory
                self.memory.add(result)

                # Reflect
                if self._goal_achieved(state):
                    self.logger.info("Goal achieved")
                    break

            return self._format_result(state)

        except Exception as e:
            self.logger.error(f"Agent failed: {e}")
            return Result(success=False, error=str(e))

    def _build_context(self, state: AgentState) -> str:
        """Build prompt context"""
        recent_memory = self.memory.recall(state.goal, k=5)
        return f"""
            Goal: {state.goal}
            Completed: {state.completed_steps}
            Memory: {recent_memory}
            Iteration: {state.iteration_count}/{self.config.max_iterations}
        """

    def _handle_tool_error(self, error, state):
        """Recover from tool errors"""
        self.logger.info(f"Recovering from: {error}")

        if error.recoverable:
            return {"fallback": "Using alternative approach"}
        else:
            state.has_error = True
            return {"error": str(error)}

    def _goal_achieved(self, state: AgentState) -> bool:
        """Check if goal is achieved"""
        if state.last_result is None:
            return False

        confidence = self.llm.assess_goal_achievement(
            goal=state.goal,
            result=state.last_result
        )

        return confidence > self.config.success_threshold

    def _format_result(self, state: AgentState) -> Result:
        """Format final result"""
        return Result(
            success=not state.has_error,
            output=state.last_result,
            steps=len(state.completed_steps),
            memory=self.memory.get_summary()
        )

Key Takeaways

  1. Single agent is the foundation - Master this before multi-agent
  2. Modularity enables testing - Each layer independent
  3. State matters - Make it explicit, persistent
  4. Error handling is critical - Systems will fail
  5. Observability essential - Log everything
  6. Start simple, add as needed - Don't over-engineer
  7. Know when to scale - Multi-agent for specific problems

Next Steps


Last Updated: August 9, 2026