Agent Design Patterns

The vendor-neutral patterns underneath every agent framework

Introduction

LangGraph and Google ADK give you the machinery for building agents. This lesson covers the patterns that machinery implements: ReAct, reflection, planning, routing, evaluator-optimizer loops, and the memory tiers that hold it all together. These patterns are framework-agnostic, you can build every one of them with nothing but the raw model API, and understanding them is what lets you choose the right structure for a task instead of reaching for a framework by reflex.

1. What Is an Agent?

LLM + tools + loop + memory

2. ReAct

Reason, act, observe, repeat

3. Reflection

Draft, critique, revise

4. Planner-Executor

Decompose, then execute

5. Routing

Classify, then dispatch

6. Agent Memory

Short, working, long-term

7. Multi-Agent

Supervisor and network topologies

8. When NOT to

Anti-patterns and simpler options

1. What Is an Agent?

An agent is an LLM placed inside a loop, given tools it can call and some memory to track progress, and left to decide its own next step until a goal is met. That last part is the distinction that matters:

Chain

A fixed sequence: A then B then C. You decide the steps in advance. Predictable, no autonomy.

Workflow

Branches and loops you wire explicitly (the LangGraph model). The paths are fixed; which path runs is dynamic.

Agent

The model chooses its own next action at each step. Maximum flexibility, less predictability, harder to test.

The four ingredients: every agent is some combination of an LLM (the reasoning engine), tools (how it affects the world), a control loop (what keeps it going), and memory (what it remembers across steps). The patterns below are different ways of arranging those four ingredients.

2. ReAct: Reason + Act

ReAct is the foundational agent pattern and the one built into every tool-calling API. The model alternates between reasoning ("I need the release docs") and acting (calling a tool), then observes the result and reasons again. The loop continues until the model decides it has enough to answer.

The ReAct Loop

UserAgentTooltaskThought: I need dataAction: search_docs(q)Observation: resultsThought: enough to answerFinal answer

Figure 1: Thought and action interleave until the model stops requesting tools

Here is the entire pattern with the raw Anthropic SDK, no framework. The loop is driven by one signal: stop_reason == "tool_use". While the model keeps asking for tools, you run them and feed results back; when it stops, you have the answer.

import anthropic

client = anthropic.Anthropic()

# --- Tool definition (what the model is allowed to call) ---
tools = [{
    "name": "search_docs",
    "description": "Search internal documentation for a query.",
    "input_schema": {
        "type": "object",
        "properties": {"query": {"type": "string"}},
        "required": ["query"],
    },
}]

def search_docs(query: str) -> str:
    # Real implementation would hit a vector store or search API
    return f"[results for '{query}': the deploy step runs 'make release']"

# --- The ReAct loop: reason -> act -> observe -> repeat ---
messages = [{"role": "user", "content": "How do I cut a release?"}]

while True:
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )
    messages.append({"role": "assistant", "content": response.content})

    # No tool requested -> the model has its final answer
    if response.stop_reason != "tool_use":
        answer = "".join(b.text for b in response.content if b.type == "text")
        print(answer)
        break

    # Execute every tool call the model asked for, feed results back
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = search_docs(**block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })
    messages.append({"role": "user", "content": tool_results})

3. Reflection: Self-Critique

Models produce better output when asked to critique their own first attempt. The reflection pattern separates generation from evaluation: draft an answer, critique it against the goal, then revise, looping until the critique is satisfied or a round limit is hit.

Reflection Loop

Draftgenerate first attemptCritiquelist concrete flawsRevisefix the flawsAcceptcritique = APPROVEDgoodflaws foundre-check

Figure 2: Generation and evaluation are separate calls, so the critic is not defending its own draft

def reflect_and_improve(client, task: str, max_rounds: int = 2) -> str:
    """Generate a draft, then critique and revise it until approved."""

    draft = client.messages.create(
        model="claude-sonnet-5", max_tokens=1024,
        messages=[{"role": "user", "content": task}],
    ).content[0].text

    for _ in range(max_rounds):
        critique = client.messages.create(
            model="claude-sonnet-5", max_tokens=512,
            messages=[{"role": "user", "content": (
                "Critique the draft against the task. List concrete, "
                "actionable flaws only. If it is genuinely good, reply "
                f"exactly 'APPROVED'.\n\nTask: {task}\n\nDraft:\n{draft}"
            )}],
        ).content[0].text

        if "APPROVED" in critique:
            break

        draft = client.messages.create(
            model="claude-sonnet-5", max_tokens=1024,
            messages=[{"role": "user", "content": (
                "Revise the draft to fix every point in the critique. "
                f"Return only the improved draft.\n\nCritique:\n{critique}"
                f"\n\nDraft:\n{draft}"
            )}],
        ).content[0].text

    return draft
Cost note: reflection multiplies your token spend by the number of rounds. Reserve it for high-value output (code, analysis, customer-facing copy), not every response, and always cap the rounds so a stubborn critic cannot loop forever.

4. Planner-Executor

For multi-step goals, asking a model to do everything in one shot invites it to lose the thread. The planner-executor pattern splits the work: a planner decomposes the goal into an explicit list of steps, then an executor carries out each step in turn, often calling tools. Because the plan is written down, you can inspect it, edit it, or ask for human approval before any action is taken.

Planner-Executor

GoalPlannerdecompose into stepsStep 1Step 2Step 3Resultplan

Figure 3: The plan is an explicit, inspectable artifact between goal and execution

You already built a version of this in the Google ADK lesson: the Blogger's outline-then-write pipeline is a planner (outline) feeding an executor (writer). The pattern generalizes far beyond content: research assistants, migration tools, and coding agents all plan first, then execute.

5. Routing: Classify, Then Dispatch

Not every request deserves your most powerful, most expensive path. The routing pattern uses a fast classifier (often a small model) to label the request, then dispatches to a handler tuned for that category, each with its own prompt, tools, and even model. It is the single cheapest way to cut cost and latency in a production system.

Routing / Handoff

User QueryRoutercheap classifierBilling AgentTechnical AgentSales AgentBILLINGTECHNICALSALES

Figure 4: A cheap model routes; specialized handlers do the expensive work only when needed

def route(client, query: str) -> str:
    """Classify the query once with a cheap model, then dispatch."""

    label = client.messages.create(
        model="claude-haiku-4-5-20251001",  # fast + cheap for classification
        max_tokens=10,
        system=(
            "Classify the user query into exactly one label: "
            "BILLING, TECHNICAL, or SALES. Reply with only the label."
        ),
        messages=[{"role": "user", "content": query}],
    ).content[0].text.strip().upper()

    handlers = {
        "BILLING": handle_billing,      # each handler can use a
        "TECHNICAL": handle_technical,  # different model, prompt,
        "SALES": handle_sales,          # and tool set
    }
    handler = handlers.get(label, handle_general)
    return handler(query)

6. Agent Memory

An agent's intelligence is bounded by what it can remember. Three tiers of memory work together, and knowing which is which prevents the two classic failures: overflowing the context window, and forgetting things that mattered.

The Three Memory Tiers

Agentcurrent reasoning stepShort-Termcontext windowWorkingscratchpad / stateLong-Termvector DB / SQLretrieve relevant slice

Figure 5: Short-term is finite and fast, working memory tracks progress, long-term is queried on demand

Short-Term

The context window: this request's messages. Fast, but finite and expensive, everything competes for the same space.

messages = [system, *recent_turns, user_msg]

Working

An explicit scratchpad the agent reads and writes across steps, the plan, intermediate results, and progress flags.

state = {"plan": [...], "step": 3}

Long-Term

An external store (vector DB, SQL) queried on demand. Survives across sessions; only the relevant slice is pulled into context.

vector_store.search(query, k=4)

Compaction: when short-term memory fills up, do not let the oldest messages silently fall out of the window. Instead, keep the system prompt and the most recent few turns, and replace the middle with a summary, so the agent retains the gist of what happened without paying for every token of history.

7. Multi-Agent Topologies

When one agent's job gets too broad, split it. Multiple focused agents, each with a narrow tool set and prompt, are easier to reason about and debug than one agent trying to do everything. The most common arrangement is the supervisor: an orchestrator that delegates to workers and aggregates their results (exactly the fan-out/fan-in the capstone project uses).

Supervisor Topology

Supervisordelegates + aggregatesResearcherWriterFact-Checkerdelegate

Figure 6: A supervisor delegates to specialized workers and merges their output (solid = delegate, dashed = report back)

Supervisor

One orchestrator, many workers. Easiest to control and observe. Default choice.

Network

Agents hand off to each other peer-to-peer. Flexible but harder to trace and bound.

Hierarchical

Supervisors of supervisors. For large systems; adds coordination overhead.

8. When NOT to Build an Agent

Agents are powerful and frequently the wrong tool. Every loop is a chance to burn tokens, take a wrong action, or run forever. Reach for the simplest structure that solves the problem:

Anti-patterns
  • Using an agent for a task with a fixed, known sequence (that is a chain)
  • No stopping condition or step budget, so a confused agent loops indefinitely
  • Giving one agent dozens of tools instead of splitting or routing
  • No observability, you cannot see the thought/action trace when it misbehaves
Prefer the simpler option when...
  • A single prompt answers it, use a plain call
  • The steps never change, use a chain or workflow
  • You only need branching, use conditional edges (LangGraph)
  • Autonomy is genuinely required, then, and only then, use an agent
This mirrors the lesson from LangChain & LangGraph: always add exactly as much structure as the problem demands, and no more. The patterns here are tools to reach for when the problem earns them.

Key Takeaways

  • An agent is an LLM in a loop - with tools and memory, deciding its own next step. Chains and workflows are the less-autonomous alternatives.
  • ReAct is the base pattern - reason, act, observe, repeat; it is exactly what a tool-calling API does when you loop on stop_reason.
  • Reflection separates generating from judging - a critic pass catches flaws the drafter missed. Cap the rounds and reserve it for high-value output.
  • Planner-executor makes the plan explicit - so you can inspect, edit, or gate it before any action runs.
  • Routing is the cheapest optimization - a small classifier sends each request to the right-sized handler.
  • Memory has three tiers - short-term (context), working (scratchpad), long-term (external store). Compact before you overflow.
  • Default to a supervisor - for multi-agent work; it is the easiest topology to observe and control.
  • Most tasks are not agents - use the simplest structure that works; earn the loop before you add it.