Chapter 11
15 min read
Section 65 of 175

Agent Design Decisions

Building Your First Agent

Introduction

You've learned the theory—ReAct loops, tool use, memory systems, and planning strategies. Now it's time to build. But before writing code, you need to make key design decisions that will shape your agent's capabilities, limitations, and overall success.

This chapter guides you through building your first complete agent, starting with the critical design decisions that differentiate a well-architected agent from a fragile prototype.

Core Principle: The best agents are built from clear requirements, not accumulated features. Define what your agent must do before deciding how it will do it.

When to Build an Agent

Not every AI application needs to be an agent. Before investing in agent architecture, consider whether simpler approaches might suffice:

ApproachBest ForExample
Single LLM callOne-shot tasks with clear inputsSummarizing a document
Prompt chainMulti-step but predictable workflowsGenerate → Review → Refine
RAG pipelineKnowledge retrieval with generationAnswering questions from docs
AgentDynamic, multi-step, tool-using tasksResearch and write a report

Signs You Need an Agent

  • Dynamic tool selection: The tools needed depend on intermediate results
  • Iterative refinement: Outputs need review and improvement loops
  • Multi-step reasoning: The task requires planning and execution
  • Error recovery: The system must adapt when things fail
  • Stateful interaction: Context must persist across multiple exchanges

Signs You Don't Need an Agent

  • Fixed workflow: Steps are always the same
  • Single tool: Only one capability is ever needed
  • No iteration: First output is always acceptable
  • Latency-critical: Response time must be under 1 second
🐍python
1# Decision helper
2def should_use_agent(task_description: str) -> dict:
3    """Analyze whether a task needs an agent."""
4
5    characteristics = {
6        "requires_multiple_tools": False,
7        "dynamic_tool_selection": False,
8        "needs_iteration": False,
9        "requires_planning": False,
10        "stateful": False,
11        "error_recovery_needed": False,
12    }
13
14    # Analyze task...
15    # (In practice, you'd use LLM or heuristics here)
16
17    score = sum(characteristics.values())
18
19    return {
20        "characteristics": characteristics,
21        "agent_score": score,
22        "recommendation": "agent" if score >= 3 else "simpler_approach",
23        "reasoning": f"{score}/6 agent characteristics detected"
24    }

Architecture Choices

Once you've decided to build an agent, you need to choose its architecture. Here are the main patterns:

1. Simple ReAct Agent

A straightforward thought-action-observation loop. Best for focused tasks with clear tool boundaries.

🐍python
1class SimpleReActAgent:
2    """Basic ReAct agent for focused tasks."""
3
4    def __init__(self, tools: list, model: str):
5        self.tools = {t.name: t for t in tools}
6        self.model = model
7        self.max_steps = 10
8
9    async def run(self, task: str) -> str:
10        history = []
11
12        for step in range(self.max_steps):
13            # Think and act
14            response = await self.step(task, history)
15
16            if response.get("final_answer"):
17                return response["final_answer"]
18
19            # Execute action
20            tool_name = response["action"]
21            tool_input = response["action_input"]
22            result = await self.tools[tool_name].execute(tool_input)
23
24            history.append({
25                "thought": response["thought"],
26                "action": tool_name,
27                "observation": result
28            })
29
30        return "Max steps reached without answer"

2. Planning Agent

Creates a plan before execution, then follows it with monitoring. Best for complex, multi-phase tasks.

🐍python
1class PlanningAgent:
2    """Agent that plans before executing."""
3
4    def __init__(self, tools: list, model: str):
5        self.tools = {t.name: t for t in tools}
6        self.model = model
7        self.planner = TaskPlanner(model)
8        self.executor = TaskExecutor(tools)
9
10    async def run(self, task: str) -> str:
11        # Phase 1: Plan
12        plan = await self.planner.create_plan(task)
13
14        # Phase 2: Execute with monitoring
15        results = []
16        for step in plan.steps:
17            result = await self.executor.execute(step)
18            results.append(result)
19
20            # Check if replanning needed
21            if not result.success:
22                plan = await self.planner.replan(task, results)
23
24        # Phase 3: Synthesize
25        return await self.synthesize(task, results)

3. Hierarchical Agent

A supervisor agent delegates to specialized sub-agents. Best for broad domains requiring multiple expertise areas.

🐍python
1class HierarchicalAgent:
2    """Supervisor with specialized sub-agents."""
3
4    def __init__(self, sub_agents: dict[str, Agent]):
5        self.sub_agents = sub_agents
6        self.supervisor = SupervisorLLM()
7
8    async def run(self, task: str) -> str:
9        # Supervisor decides delegation
10        delegation = await self.supervisor.analyze(task)
11
12        results = {}
13        for subtask in delegation.subtasks:
14            agent = self.sub_agents[subtask.agent_type]
15            results[subtask.id] = await agent.run(subtask.description)
16
17        # Supervisor synthesizes
18        return await self.supervisor.synthesize(task, results)

Choosing an Architecture

ArchitectureComplexityBest For
Simple ReActLowSingle-domain tasks, quick iterations
Planning AgentMediumMulti-step tasks, need for oversight
HierarchicalHighMulti-domain tasks, specialized expertise
Start with the simplest architecture that could work. You can always add complexity later, but removing it is harder.

Model Selection

The model powering your agent significantly impacts its capabilities and cost. Here's how to choose:

Model Capabilities Matrix

CapabilityClaude HaikuClaude SonnetClaude Opus
SpeedFastestBalancedSlower
CostLowestModerateHighest
ReasoningGoodExcellentBest
Tool useGoodExcellentExcellent
Long contextGoodExcellentExcellent
Complex planningLimitedGoodExcellent

Model Selection Strategy

🐍python
1from enum import Enum
2
3class ModelTier(Enum):
4    FAST = "claude-3-5-haiku-20241022"    # Quick, cheap operations
5    BALANCED = "claude-sonnet-4-20250514"  # Most agent tasks
6    POWERFUL = "claude-opus-4-20250514"    # Complex reasoning
7
8class ModelSelector:
9    """Select appropriate model based on task."""
10
11    def select(self, task_type: str, complexity: str) -> str:
12        """Choose model based on task characteristics."""
13
14        # Simple lookups, formatting -> Fast model
15        if task_type in ["lookup", "format", "simple_qa"]:
16            return ModelTier.FAST.value
17
18        # Most agent tasks -> Balanced model
19        if complexity in ["low", "medium"]:
20            return ModelTier.BALANCED.value
21
22        # Complex reasoning, planning -> Powerful model
23        if complexity == "high" or task_type in ["planning", "analysis"]:
24            return ModelTier.POWERFUL.value
25
26        return ModelTier.BALANCED.value  # Default

Multi-Model Agents

Advanced agents use different models for different operations:

🐍python
1class MultiModelAgent:
2    """Agent using different models for different tasks."""
3
4    def __init__(self):
5        self.models = {
6            "planning": "claude-opus-4-20250514",      # Complex planning
7            "execution": "claude-sonnet-4-20250514",   # Tool use, reasoning
8            "summarization": "claude-3-5-haiku-20241022",  # Quick summaries
9        }
10        self.clients = {name: Anthropic() for name in self.models}
11
12    async def plan(self, task: str) -> Plan:
13        """Use powerful model for planning."""
14        client = self.clients["planning"]
15        # ... planning logic
16
17    async def execute_step(self, step: Step) -> Result:
18        """Use balanced model for execution."""
19        client = self.clients["execution"]
20        # ... execution logic
21
22    async def summarize(self, results: list) -> str:
23        """Use fast model for summarization."""
24        client = self.clients["summarization"]
25        # ... summarization logic
Model capabilities evolve rapidly. What requires a powerful model today might be achievable with a faster model tomorrow. Design your agent to make model selection configurable.

Tool Strategy

Tools are your agent's interface with the world. Poor tool design is a common cause of agent failure.

Tool Design Principles

  1. Single responsibility: Each tool does one thing well
  2. Clear interfaces: Unambiguous inputs and outputs
  3. Informative errors: Failures explain what went wrong
  4. Reasonable defaults: Optional parameters have sensible defaults
  5. Composable: Tools can be combined for complex operations
🐍python
1from dataclasses import dataclass
2from typing import Any
3
4@dataclass
5class ToolDefinition:
6    """Well-defined tool interface."""
7    name: str
8    description: str
9    parameters: dict[str, dict]  # JSON Schema
10    required_params: list[str]
11    examples: list[dict]  # Example calls
12
13# Good tool definition
14search_tool = ToolDefinition(
15    name="web_search",
16    description="Search the web for information. Returns top results with snippets.",
17    parameters={
18        "query": {
19            "type": "string",
20            "description": "Search query (be specific for better results)"
21        },
22        "num_results": {
23            "type": "integer",
24            "description": "Number of results to return",
25            "default": 5,
26            "minimum": 1,
27            "maximum": 20
28        }
29    },
30    required_params=["query"],
31    examples=[
32        {"query": "Python async programming tutorial", "num_results": 3},
33        {"query": "latest React 19 features"}
34    ]
35)
36
37# Bad tool definition (avoid)
38bad_tool = ToolDefinition(
39    name="search",  # Too generic
40    description="Searches things",  # Vague
41    parameters={"q": {"type": "string"}},  # Cryptic param name
42    required_params=["q"],
43    examples=[]  # No examples
44)

Tool Categories

CategoryExamplesConsiderations
Information retrievalWeb search, database query, API callRate limits, caching, error handling
Content creationWrite file, generate image, send emailConfirmation for destructive actions
ComputationCalculator, code execution, data analysisSandboxing, timeouts, resource limits
External servicesSlack, GitHub, calendarAuthentication, permissions, quotas

Tool Count Guidelines

  • 3-5 tools: Optimal for focused agents
  • 6-10 tools: Manageable with good descriptions
  • 10+ tools: Consider tool categorization or hierarchical selection
🐍python
1class ToolRegistry:
2    """Manage agent tools with categories."""
3
4    def __init__(self):
5        self.tools: dict[str, Tool] = {}
6        self.categories: dict[str, list[str]] = {}
7
8    def register(self, tool: Tool, category: str) -> None:
9        """Register a tool in a category."""
10        self.tools[tool.name] = tool
11        if category not in self.categories:
12            self.categories[category] = []
13        self.categories[category].append(tool.name)
14
15    def get_tools_for_task(self, task: str) -> list[Tool]:
16        """Get relevant tools for a task (could use LLM selection)."""
17        # For now, return all tools
18        # In production, filter based on task analysis
19        return list(self.tools.values())
20
21    def get_tool_descriptions(self) -> str:
22        """Format tool descriptions for prompt."""
23        lines = []
24        for category, tool_names in self.categories.items():
25            lines.append(f"\n## {category}")
26            for name in tool_names:
27                tool = self.tools[name]
28                lines.append(f"- {tool.name}: {tool.description}")
29        return "\n".join(lines)

Design Checklist

Before building, work through this checklist:

Requirements

  • What specific tasks must the agent accomplish?
  • What are the success criteria for each task?
  • What are the failure modes and how should they be handled?
  • What are the latency and cost constraints?

Architecture

  • Which architecture pattern fits the requirements?
  • What is the maximum number of steps allowed?
  • How will the agent know when it's done?
  • What state needs to persist between steps?

Tools

  • What tools are essential vs. nice-to-have?
  • Are tool descriptions clear and unambiguous?
  • What happens when tools fail?
  • Are there any dangerous tools that need confirmation?

Model

  • What model capabilities are required?
  • Should different operations use different models?
  • What is the token budget per task?
  • How will context length be managed?

Safety

  • What actions should require human confirmation?
  • What are the rate limits and resource bounds?
  • How will sensitive data be handled?
  • What logging and auditing is required?
🐍python
1@dataclass
2class AgentDesign:
3    """Document agent design decisions."""
4
5    # Requirements
6    name: str
7    purpose: str
8    success_criteria: list[str]
9    failure_handling: str
10
11    # Architecture
12    architecture: str  # "react", "planning", "hierarchical"
13    max_steps: int
14    termination_conditions: list[str]
15
16    # Tools
17    tools: list[ToolDefinition]
18    dangerous_tools: list[str]  # Require confirmation
19
20    # Model
21    primary_model: str
22    model_overrides: dict[str, str]  # operation -> model
23
24    # Safety
25    require_confirmation: list[str]  # Action types
26    rate_limits: dict[str, int]
27    max_tokens_per_task: int
28
29    def validate(self) -> list[str]:
30        """Validate design completeness."""
31        issues = []
32
33        if not self.success_criteria:
34            issues.append("No success criteria defined")
35
36        if not self.tools:
37            issues.append("No tools defined")
38
39        if self.max_steps < 1:
40            issues.append("Invalid max_steps")
41
42        for dangerous in self.dangerous_tools:
43            if dangerous not in [t.name for t in self.tools]:
44                issues.append(f"Dangerous tool {dangerous} not in tool list")
45
46        return issues

Summary

Good agent design starts with clear decisions about what you're building and why. We covered:

  • When to build an agent: Agents are for dynamic, multi-step, tool-using tasks—not every AI application
  • Architecture choices: Simple ReAct, Planning, or Hierarchical based on complexity
  • Model selection: Match model capabilities to task requirements, consider multi-model strategies
  • Tool strategy: Well-designed tools with clear interfaces are essential
  • Design checklist: Work through requirements, architecture, tools, model, and safety decisions upfront

In the next section, we'll implement the core agent loop—the beating heart of any agent system.