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 (also called the evaluator-optimizer loop), planning, routing, 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.
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
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. Two details in this code are what separate a demo from something you would run: the loop counts its steps rather than spinning on while True, and it collects every tool call from a turn before replying, because the model can request several at once. The mechanics of tool schemas and parallel calls are covered properly in Lesson 16.
# react_loop.py - a bounded ReAct loop on the raw Anthropic SDK
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": "What command cuts a release?"}]
MAX_STEPS = 6 # a loop without a budget is an anti-pattern (section 8)
for step in range(1, MAX_STEPS + 1):
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(f"step {step}: answering")
print(answer)
break
# Execute every tool call the model asked for, feed the results back
# in a single user message: the model may request several at once.
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"step {step}: {block.name}({block.input['query']!r})")
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})
else:
raise RuntimeError(f"agent did not finish within {MAX_STEPS} steps")Expected Output:
step 1: search_docs('cut a release command')
step 1: search_docs('release process')
step 2: answering
The command that cuts a release is:
```
make release
```
This is the command run during the deploy step to cut a release, according to the internal documentation.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. You will also see this called the evaluator-optimizer loop: the critic is the evaluator, the reviser is the optimizer, and the two names describe the same structure.
Reflection Loop
Figure 2: Generation and evaluation are separate calls, so the critic is not defending its own draft
# reflection.py - draft, critique, revise, with the rounds capped
import anthropic
def text_of(message) -> str:
"""Pull the text out of a response.
content is a list of typed blocks, and on a thinking model the first one
is a ThinkingBlock, so select by type rather than by index.
"""
return next(b.text for b in message.content if b.type == "text")
def reflect_and_improve(client, task: str, max_rounds: int = 2) -> str:
"""Generate a draft, then critique and revise it until approved."""
draft = text_of(client.messages.create(
model="claude-sonnet-5", max_tokens=1024,
messages=[{"role": "user", "content": task}],
))
for round_no in range(1, max_rounds + 1):
critique = text_of(client.messages.create(
model="claude-sonnet-5", max_tokens=1024,
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}"
)}],
))
if "APPROVED" in critique:
print(f"round {round_no}: approved")
break
print(f"round {round_no}: revising")
draft = text_of(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}"
)}],
))
return draft
if __name__ == "__main__":
client = anthropic.Anthropic()
print(reflect_and_improve(client, "Write a one-sentence release note for a "
"--dry-run flag that prints planned changes "
"without applying them."))Expected Output:
round 1: revising round 2: approved Added a `--dry-run` flag to preview planned changes without applying them.
max_tokens too small for a thinking model truncates the critique mid-sentence, and the loop cannot tell that from real feedback. Budgeting those tokens is Lesson 18.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
Figure 3: The plan is an explicit, inspectable artifact between goal and execution
What makes the plan inspectable is that it comes back as a list, not as prose you would have to re-parse. Asking for a typed object with structured outputs is what turns "the plan" from a paragraph into steps[2], something you can print, reorder, drop a step from, or hold for approval. Note the two models: planning is the part worth paying for, so it runs on Sonnet, while each step is executed by Haiku.
# planner_executor.py - Sonnet decomposes the goal, Haiku executes each step
import anthropic
from pydantic import BaseModel
client = anthropic.Anthropic()
class Plan(BaseModel):
steps: list[str]
def make_plan(goal: str) -> list[str]:
"""Planner: decompose the goal into ordered steps, typed rather than prose."""
response = client.messages.parse(
model="claude-sonnet-5",
max_tokens=600,
system=("Decompose the goal into 3 to 5 ordered steps. Each step is one "
"short sentence naming a single concrete action."),
messages=[{"role": "user", "content": goal}],
output_format=Plan,
)
return response.parsed_output.steps
def execute(goal: str, step: str, done: list[str]) -> str:
"""Executor: carry out one step, with the goal and prior results in view."""
so_far = "\n".join(f"- {d}" for d in done) or "(nothing yet)"
reply = client.messages.create(
model="claude-haiku-4-5", # execution is the cheap half of the pattern
max_tokens=200,
system=("Carry out exactly the step you are given. Reply with one sentence "
"of at most 15 words, no headings and no lists."),
messages=[{"role": "user", "content": (
f"Goal: {goal}\n\nDone so far:\n{so_far}\n\nYour step: {step}"
)}],
)
return next(b.text for b in reply.content if b.type == "text")
if __name__ == "__main__":
goal = "Prepare a release checklist for a small Python CLI tool."
steps = make_plan(goal)
# The plan is data, not prose. This is where you would print it for review,
# edit the list, or require a sign-off before anything actually runs.
for i, step in enumerate(steps, 1):
print(f"plan {i}. {step}")
results: list[str] = []
for i, step in enumerate(steps, 1):
results.append(execute(goal, step, results))
print(f"done {i}. {results[-1]}")Expected Output:
plan 1. Update version number and changelog in the project files plan 2. Run the full test suite and fix any failing tests plan 3. Update documentation and README with new features or changes plan 4. Build the package and verify it installs correctly in a clean environment plan 5. Publish the release to PyPI and tag the release in version control done 1. Update the version number in setup.py and add release notes to CHANGELOG.md. done 2. Run pytest or your test command to verify all tests pass before proceeding to release. done 3. Review and update README.md to describe new features and update docs with current usage examples. done 4. Create a virtual environment, build your package with `python -m build`, and install it to verify functionality. done 5. Push built package to PyPI using `twine upload dist/*` and create a git tag with `git tag v<version>`.
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: Lesson 18 measures the gap between the cheapest and most expensive model on one task.
Routing / Handoff
Figure 4: A cheap model routes; specialized handlers do the expensive work only when needed
# router.py - a Haiku classifier in front of three specialized handlers
import anthropic
# Each handler is a mini-agent of its own: different model, prompt, and
# tools. Stubbed here so the example runs end to end.
def handle_billing(query: str) -> str:
return "-> billing agent"
def handle_technical(query: str) -> str:
return "-> technical agent"
def handle_sales(query: str) -> str:
return "-> sales agent"
def handle_general(query: str) -> str:
return "-> general agent"
def route(client, query: str) -> str:
"""Classify the query once with a cheap model, then dispatch."""
reply = client.messages.create(
model="claude-haiku-4-5", # 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}],
)
label = next(b.text for b in reply.content if b.type == "text").strip().upper()
handlers = {
"BILLING": handle_billing, # the fallback catches anything the
"TECHNICAL": handle_technical, # classifier returns that is not one
"SALES": handle_sales, # of the three expected labels
}
handler = handlers.get(label, handle_general)
return f"{label:10} {handler(query)}"
if __name__ == "__main__":
client = anthropic.Anthropic()
for q in ["My card was charged twice this month.",
"The webhook returns 502 after I rotate the key.",
"Do you offer volume discounts for 50 seats?"]:
print(route(client, q))Expected Output:
BILLING -> billing agent TECHNICAL -> technical agent SALES -> sales agent
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
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)
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. The capstone project builds the same fan-out/fan-in shape, though there the branch is decided in code rather than by a model, which makes it a workflow by the definitions in section 1. Both are worth knowing: a hardcoded fan-out is cheaper and far easier to test, so promote the decision to a model only when the set of workers genuinely varies.
Supervisor Topology
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:
- 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
- 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
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. - Every loop gets a budget - count the steps and fail loudly at the cap. A stub tool returning one constant string still drew two searches before the model would answer.
- 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.