The 6 AI Agent Design Patterns Every Engineer Should Know (And When Not to Use Them) Anthropic's guidance on building effective agents distinguishes workflows, where LLMs and tools follow predefined code paths, from agents, where the LLM dynamically directs its own process and tool usage, and advises developers to start with a single agent using strong tools before scaling to sub-agents. Instrumented ReAct-style agents across 200 tasks wasted 90.8 percent of retries on unrecoverable errors such as hallucinated tool names, while GPT-4 class models hallucinated tool-call arguments at roughly a 28 percent rate, indicating tool design drives reliability more than model choice. Anthropic reported spending more time on tool descriptions and schemas than on the main system prompt. If you have ever watched an agent burn five dollars of API credits trying to fix its own mistake, you already know the feeling. The demo was flawless. Three agents, smooth handoffs, polished output. Then you deployed it, and within a week your carefully architected multi-agent system was hallucinating facts no single agent would have produced alone, looping on the same failed tool call, and quietly spending at triple the rate you projected. This is the love-hate relationship developers have with sub-agents and multi-agent systems. They are powerful in theory. In production, they often introduce heavy coordination overhead, unpredictable cascading errors, and token bills that make your finance team nervous. The industry consensus that has emerged is blunt: start with a single agent using great tools, and only scale to sub-agents when you genuinely hit a wall. But before you can decide when to scale, you need a shared vocabulary. When engineers talk about “agent design,” they are really talking about a small set of named, reusable architectural patterns. Each one defines how an LLM thinks, acts, and collaborates. Each one has a characteristic failure mode. And the difference between a system that survives production and one that becomes archaeology at 3am is almost always pattern selection, not model choice. An agent design pattern is a named, reusable control structure that defines how an LLM reasons, acts, delegates, and recovers from failure. The pattern is the skeleton. You can put the same model into six different skeletons and get six wildly different levels of reliability, latency, and cost. Anthropic’s widely cited guidance on building effective agents draws the distinction cleanly: a workflow is a system where LLMs and tools are orchestrated through predefined code paths, while an agent is a system where the LLM dynamically directs its own process and tool usage. Most production systems labeled “agents” are actually workflows, and Anthropic’s advice is blunt: if you can solve it with a deterministic pipeline, do that. Tool use is the building block beneath all of these patterns. The patterns are the buildings. ReAct decides when to call a tool inside a loop. A planner decides the sequence of tool calls up front. A critic adds a second pass over earlier output. An orchestrator treats entire worker agents as tools. When you compare patterns, you are really comparing how each one orchestrates the same fundamental action: a tool call. Here is the map of the six core patterns: Let us walk through each one, and more importantly, each one’s failure mode. Tool use is the simplest pattern and the one every other pattern is built from. The agent receives a request, decides whether it needs external capability, and calls an API, database, or code interpreter with structured arguments. The counterintuitive lesson from teams shipping agents at scale is that tool design determines reliability far more than model choice . Anthropic has stated they spent more time on tool descriptions and schemas than on the main system prompt. When researchers instrumented ReAct-style agents with 200 tasks, they found 90.8 percent of retries were wasted on unrecoverable errors like hallucinated tool names, and GPT-4 class models hallucinated tool-call arguments at roughly a 28 percent rate. The tools were not failing. The contract between the model and the tools was. The fix is rarely a better model. It is deterministic structure: If your agent is unreliable, audit your tool schemas before you touch anything else. This one lever moves the needle more than any framework migration you will ever do. """Tool Use: free-form LLM tool calls vs strict schemas + code-driven routing.The key production lesson: tool design determines reliability far more thanmodel choice. Instrumented ReAct-style agents wasted 90.8% of retries onunrecoverable errors like hallucinated tool names, and GPT-4 class modelshallucinate tool-call arguments at roughly a 28% rate.The BAD version routes tools through free-form LLM strings and acceptsunguarded arguments. The GOOD version:1. Validates arguments with strict Pydantic schemas - structured error feedback the model can correct from.2. Routes tools in CODE a dict lookup , not in the prompt - eliminates tool-name hallucinations and drops step variance dramatically.3. Classifies errors as retryable vs non-retryable - saves retry budget.Run: python tool use schemas.py"""from pydantic import BaseModel, Field, ValidationError --------------------------------------------------------------------------- Tool registry ---------------------------------------------------------------------------class SearchArgs BaseModel : query: str = Field min length=3 limit: int = Field default=5, ge=1, le=20 class CalcArgs BaseModel : expression: str = Field pattern=r"^ \d\s+\- / . +$" numbers/operators onlydef search tool args: SearchArgs - str: return f" results for: {args.query} top {args.limit} "def calculator tool args: CalcArgs - str: return str eval args.expression, {" builtins ": {}} sandboxed toy GOOD: tool registry with BOTH a code router and its schema.TOOL REGISTRY = { "search": search tool, SearchArgs , "calculator": calculator tool, CalcArgs ,} --------------------------------------------------------------------------- BAD: the LLM picks the tool name and raw args as free text. ---------------------------------------------------------------------------def bad tool call llm output: str : """No schema, no routing table, no error taxonomy. Every defect the model produces flows straight into the tool.""" llm output might be "calclator: 2+2" <- hallucinated tool name name, , raw args = llm output.partition ":" for known in TOOL REGISTRY: if name.strip .lower .startswith known :4 : fuzzy guess name = known break else: raise RuntimeError f"unknown tool '{name}' - naive retry burns budget" return TOOL REGISTRY name 0 raw args raw string into the tool --------------------------------------------------------------------------- GOOD: strict validation + deterministic routing + error taxonomy. ---------------------------------------------------------------------------class ToolError Exception : def init self, message: str, retryable: bool : super . init message self.retryable = retryable retry timeouts, never retry bad namesdef safe call name: str, raw args: dict - str: 1. Deterministic routing: dict lookup, not LLM string matching. if name not in TOOL REGISTRY: 2. Non-retryable: the model must fix its output, retrying the same hallucinated name wastes budget 90.8% of observed retries . raise ToolError f"tool '{name}' does not exist. valid: {list TOOL REGISTRY }", retryable=False fn, schema = TOOL REGISTRY name try: 3. Strict schema: catches malformed args BEFORE execution and returns field-level feedback the model can actually correct from. args = schema.model validate raw args except ValidationError as e: errors = "; ".join f"{'.'.join map str, x 'loc' }: {x 'msg' }" for x in e.errors raise ToolError f"bad arguments: {errors}", retryable=False return fn args def agent turn llm tool name: str, llm args: dict, retry budget: int = 2 - str: """One agent turn with a retry loop that only spends budget on retryable errors timeouts, rate limits -- never on the model's own malformed output.""" for attempt in range 1, retry budget + 2 : try: return safe call llm tool name, llm args except ToolError as e: if not e.retryable: Feed the error back to the model for self-correction, retrying blindly would be throwing money at a typo. return f"ERROR sent to model for correction : {e}" print f" attempt {attempt} retryable failure: {e}" return "gave up: transient failure after retry budget" --------------------------------------------------------------------------- Demo ---------------------------------------------------------------------------if name == " main ": print "=== BAD: free-form string routing ===" try: bad tool call "calclator: 2+2" hallucinated tool name except Exception as e: print f" {type e . name }: {e}" print "\n=== GOOD: strict schemas + dict routing + error taxonomy ===" print f" valid call - {agent turn 'calculator', {'expression': ' 18 4 / 3'} }" print f" hallucinated name - {agent turn 'web search', {'query': 'ai agents'} }" print f" malformed args - {agent turn 'search', {'query': 'ab', 'limit': 99} }" Numbers to remember: - 90.8% of retries in instrumented ReAct agents were wasted on unrecoverable errors mostly hallucinated tool names . A retry/retryable split, like the one above, fixes the waste. - Deterministic routing + schema validation achieves ~3x lower step variance and near-zero wasted retries vs naive ReAct setups. The snippet contrasts the two contracts. The bad version routes tools through free-form LLM strings and fuzzy-matches hallucinated tool names before dumping raw, unvalidated arguments straight into the tool. The good version routes through a dictionary lookup, validates every argument against a strict Pydantic schema, splits errors into retryable versus non-retryable, and only spends retry budget on transient failures. Read the demo output with the 90.8 percent wasted-retry statistic in mind: most of that waste is avoidable with these three lines of structure. ReAct, introduced by Yao et al. in 2022, is the pattern most people mean when they say “agent.” The model interleaves reasoning with action: it thinks about what to do, takes one action, observes the result, thinks again, and repeats until the goal is met. ReAct is the simplest pattern that still earns the word agent, and for tasks under roughly eight steps, it is usually the right call. Its strength is flexibility. On dynamic, open-ended problems like support triage, multi-hop research, or troubleshooting flows, the next question genuinely depends on the last answer, and no plan written in advance can anticipate every branch. The visible Thought-Action-Observation trail also makes the reasoning grounded and auditable in a way that latent chain-of-thought is not. Its weakness is drift. Because every step depends on the previous one, a small early mistake compounds. By step twelve, the agent may be confidently solving the wrong problem. The scratchpad grows, earlier sub-goals scroll out of effective context, and latency climbs with every sequential model call. The math here deserves its own attention, because it explains most agent production incidents. Per-step reliability does not add across steps. It multiplies. An agent that is 95 percent reliable per step is only about 60 percent reliable across ten steps, because 0.95 to the tenth power is roughly 0.60. At a 1 percent per-step error rate, a 20-step workflow accumulates into a 63 percent failure probability. Production analyses of multi-agent systems found something even worse: when agents share state, errors amplify rather than average out. A single agent with a 5 percent error rate becomes a 52 percent system failure rate with 10 agents sharing state, and real production measurements where agents also retry and corrupt state push the amplification factor higher still. This is why the fix for a stalling ReAct agent is almost never a bigger model. It is architecture that bounds steps, truncates observations, and validates intermediate results. Every ReAct loop you ship should have a hard iteration limit, a per-task token budget, and observation truncation. An unconstrained agent loop is a denial-of-service vulnerability against your own cloud bill. """ReAct Agent Loop: naive vs production-grade.The BAD version has no step limit, feeds raw unbounded observations back intocontext, and trusts the model to stop on its own. The GOOD version capsiterations, truncates observations, logs every step, and exits with astructured result.Why the guardrails matter: per-step reliability MULTIPLIES across steps.A 95% reliable step is only ~60% reliable across 10 steps 0.95 10 = 0.60 .Guardrails do not make the model smarter; they stop small errors fromcompounding into a runaway loop.Run: python react agent loop.py"""from dataclasses import dataclass, fieldfrom typing import Callable --------------------------------------------------------------------------- Fake tool environment so the example is fully runnable without API keys. In a real system, execute tool would call an LLM + actual tools. ---------------------------------------------------------------------------def fake tool call name: str, args: dict - str: """Pretend tool executor. Returns a large JSON-like payload for search.""" if name == "search": Simulates a tool that returns a 40KB JSON blob. return '{"results": ' + '{"title": "item", "snippet": "' + "x" 900 + '"},' 40 + ' }' if name == "calculator": return str eval args "expression" toy only, never do this in prod return f"Unknown tool: {name}"@dataclassclass AgentResult: answer: str steps used: int hit step limit: bool trace: list = field default factory=list --------------------------------------------------------------------------- BAD: the naive loop most tutorials show you. ---------------------------------------------------------------------------def naive react loop goal: str, max llm calls: int = 10 000 - str: """ Problems: 1. No real step limit max llm calls=10000 is not a limit, it is a wish . 2. Raw observation 40KB of JSON is appended to context every turn. By step 10 the context is 400KB and every subsequent call costs 5-15x more than expected long-context bloating . 3. No logging: when it fails, you are doing archaeology. """ history = for in range max llm calls : response = f"llm history={history}, goal={goal} " placeholder LLM call observation = fake tool call "search", {} unbounded payload history.append observation <-- context bloat lives here return "hopefully done" no structured exit --------------------------------------------------------------------------- GOOD: the production-grade loop. ---------------------------------------------------------------------------MAX STEPS = 8 hard iteration capMAX OBS CHARS = 500 truncate observations before they hit contextdef truncate observation obs: str, max chars: int = MAX OBS CHARS - str: """Keep the head and tail of an observation, drop the middle bloat.""" if len obs <= max chars: return obs return f"{obs :max chars // 2 } ... truncated {len obs - max chars} chars ... {obs -max chars // 2: }"def production react loop goal: str, execute tool: Callable = fake tool call - AgentResult: trace = for step in range 1, MAX STEPS + 1 : 1. THINK: in a real system, call the LLM with goal, truncated trace . thought = f"step {step}: decide next action for: {goal}" 2. ACT: call a tool. Route tools in code when you can, not via free-form LLM strings this removes tool-name hallucinations . raw obs = execute tool "search", {} 3. OBSERVE: truncate BEFORE it enters context. This single line prevents the 5-15x long-context cost blowup. observation = truncate observation raw obs trace.append {"step": step, "thought": thought, "observation": observation} print f" step {step:02d} obs len={len observation } truncated " 4. CHECK EXIT: a real loop asks the model "is the goal met?" with structured output. Here we simulate a done-condition at step 4. if step = 4: return AgentResult answer="structured answer with citations", steps used=step, hit step limit=False, trace=trace, 5. HARD EXIT: if we get here the agent is stuck. Fail LOUDLY with a structured error instead of silently looping forever. print "WARNING: hit MAX STEPS, escalating to human/fallback" return AgentResult answer="escalated", steps used=MAX STEPS, hit step limit=True, trace=trace if name == " main ": The compounding math that justifies every guardrail above: for p in 0.99, 0.95, 0.90 : for n in 5, 10, 20 : print f"step success={p:.2f}, steps={n:2d} - task success={p n:.1%}" print result = production react loop "Research the top 3 competitors" print f"\nDone in {result.steps used} steps. Hit limit: {result.hit step limit}" The snippet above shows the contrast between a naive ReAct loop and a production-grade one. The bad version has no step limit, feeds raw 40KB JSON observations back into context, and trusts the model to stop on its own. The good version caps iterations, truncates observations, logs every step, and exits with a structured result. Read it with the multiplication math in mind: the guardrails exist because 0.95 is not good enough at step ten. Reflection adds a critic step. After the agent produces a draft, a second pass, either the same model with a different prompt or a separate evaluator, scores the draft against the goal and either approves it or sends it back with corrections. The Reflexion work by Shinn et al. formalized this and reported strong gains on reasoning and coding benchmarks, lifting GPT-4’s HumanEval pass@1 from 80 percent to 91 percent. The production reality is sharper than the benchmark numbers suggest: reflection only works when the critique is grounded in something external . A code agent should reflect against a real test run. An extraction agent should reflect against a JSON schema validator. A research agent should reflect against retrieved sources. When the critic is just the model judging its own output, it can confidently approve a wrong answer. Practitioners have a name for this: reflection theater. It adds cost without adding accuracy. There is also an economic filter. Reflection reliably earns its cost on high-stakes, low-volume outputs: a legal summary, a financial reconciliation, code that must pass a test suite. It rarely pays off on cheap, high-volume calls where a wrong answer is easy to retry. And every reflection pass multiplies latency and tokens, so a task that took one call now takes three or four. Two hard rules prevent reflection from becoming a cost center. First, always wire the critic to a real validator when one exists, because a verifiable signal beats model self-judgment every time. Second, cap the loop at two to three iterations with a token budget per task. Teams have found agents stuck revising the same broken output, burning dollars of spend on a problem a human would have fixed in one pass. """Reflection Loop: grounded in a real validator, with a hard iteration cap.The BAD version asks the model to judge its own output "does this lookright?" . That is reflection theater: it adds cost without adding accuracy,because a model that confidently produced wrong output will confidentlyapprove it.The GOOD version:1. Grounds the critique in an EXTERNAL validator Pydantic schema here; in production it could be a test suite, a SQL query, or a compiler .2. Passes the specific validation errors back to the generator, so each revision fixes a known defect instead of vibes.3. Caps the loop at 2-3 iterations with a token budget. Uncapped reflection loops have been observed burning dollars revising the same broken output.Run: python reflection loop.py"""from pydantic import BaseModel, Field, ValidationError --------------------------------------------------------------------------- The structured output we need from the agent. ---------------------------------------------------------------------------class CompetitorReport BaseModel : name: str = Field min length=2 employees: int = Field gt=0 strengths: list str = Field min length=1 funding musd: float = Field ge=0 --------------------------------------------------------------------------- BAD: self-judged reflection. The critic is the same model with no oracle. ---------------------------------------------------------------------------def bad reflection draft: str - str: """ The critique has no ground truth to check against, so it can only produce an opinion. If the draft hallucinated an employee count of "twelve thousand", the critic has no way to catch it. """ critique = f"llm critique draft={draft} " opinion, not verification return critique revision follows an opinion - reflection theater --------------------------------------------------------------------------- GOOD: validator-grounded reflection with a hard cap. ---------------------------------------------------------------------------MAX REFLECTIONS = 3 production rule: cap the loop, budget the tokensdef generate draft prompt: str, feedback: str | None - str: """ Placeholder for a real LLM call. First pass gets the task; revision passes get the SPECIFIC validation errors, not a vague "try better". """ if feedback is None: Simulate a first draft that fails validation missing field, negative number -- a very common LLM failure mode. return '{"name": "Acme", "employees": -50, "strengths": }' Simulate a corrected second pass after seeing real error messages. return '{"name": "Acme Corp", "employees": 1200, ' '"strengths": "distribution" , "funding musd": 85.0}' def reflect with validator max iterations: int = MAX REFLECTIONS : feedback = None for attempt in range 1, max iterations + 1 : draft = generate draft "Write a competitor report", feedback try: THE CRITIC IS THE SCHEMA, not another model opinion. report = CompetitorReport.model validate json draft print f" attempt {attempt} PASSED validation" return report except ValidationError as e: Grounded feedback: the exact defects, machine-readable. errors = f"{'.'.join str loc for loc in err 'loc' }: {err 'msg' }" for err in e.errors feedback = "; ".join errors print f" attempt {attempt} FAILED - {feedback}" Hard exit: escalate rather than loop forever. print f"FAILED after {max iterations} attempts, escalating to human review" return Noneif name == " main ": print "=== BAD: self-judged critique no oracle ===" bad reflection '{"name": "Acme", "employees": "twelve thousand"}' print " - critic approved based on vibes. Nothing was verified.\n" print "=== GOOD: validator-grounded critique ===" result = reflect with validator print f"\nFinal report: {result.model dump json indent=2 }" Key numbers to remember: - Reflexion Shinn et al., 2023 lifted GPT-4 HumanEval pass@1 from 80% to 91% -- but ONLY because code has a real oracle tests . - 0.95 reliable self-critique on top of a 0.80 generator does not give you 0.76 reliability with a boost; without a validator it gives you 2-4x the token cost and the same wrong answer, approved confidently. This snippet implements reflection the way production systems should for this type pf agent: the critic evaluates against a Pydantic schema validator rather than its own opinion, the loop has a hard maximum iteration count, and each revision carries the specific validation errors forward. Notice how much of the code is guardrails rather than generation. That ratio is typical of reliable agent systems. Planning splits the agent into two roles. A planner reads the goal once and produces an ordered, complete plan. An executor then works through the plan, ideally with a smaller, cheaper model. The plan is a commitment device: it survives context scrolling because it lives outside the per-step reasoning, and a human can review it before any irreversible action runs. This pattern shines when the task shape is predictable: a monthly financial close, a data migration, a multi-section report, a 20-step code change. Writing the plan once and executing it is cheaper than reasoning step by step, because the expensive reasoning tokens are spent only at the planning stage. On long-horizon tasks, keeping the executor’s context bounded to the current step plus plan context rather than the full accumulated scratchpad cuts context consumption by 40 to 60 percent compared to flat ReAct. The failure mode is plan staleness. The plan is generated at the start based on the planner’s world model at that moment. If a step returns something unexpected, an export fails, a record does not exist, an external data source updates, a naive executor barrels ahead executing a dead plan. The fix is a hybrid that most reliable production agents converge on: plan the skeleton, react within each step, and add a cheap replan trigger that fires when an observation contradicts the plan’s assumptions . The honest framing is that planning and ReAct are not rivals. Above roughly ten steps, the planning overhead almost always pays for itself. Below that, it is wasted tokens on a three-step task. """Plan-and-Execute: dead plans vs plans with a replan trigger.The BAD version generates a plan and executes it no matter what happens.This is the pattern's signature failure mode, plan staleness: the plan isbuilt on the planner's world model at T=0, and if reality diverges a stepfails, data updates, an assumption breaks , a naive executor barrels aheadexecuting a dead plan.The GOOD version:1. Generates the plan ONCE cheap, compact, survives context scrolling .2. Uses a cheap executor plan tokens are spent once, not per step .3. Compares each step's EXPECTED output against the ACTUAL observation. On a mismatch, it fires a REPLAN signal and regenerates the remaining plan from current state, not from scratch.Run: python plan and execute.py"""from dataclasses import dataclass --------------------------------------------------------------------------- Placeholder LLM functions the logic, not the model, is the point here . ---------------------------------------------------------------------------def fake planner goal: str, state: dict | None = None - list dict : """ In production this is one LLM call returning a compact structured plan 50-150 tokens . Keeping the plan OUT of per-step context cuts context consumption 40-60% vs flat ReAct on long-horizon tasks. """ if state is None: initial plan for: quarterly report with live data return {"id": 1, "description": "fetch Q3 revenue data", "expect": "numeric dict"}, {"id": 2, "description": "compute growth vs Q2", "expect": "percentage"}, {"id": 3, "description": "draft report section", "expect": "text"}, REPLAN: regenerate only the remaining steps from current state. return {"id": 4, "description": "re-fetch revenue data source changed ", "expect": "numeric dict"}, {"id": 5, "description": "compute growth revised", "expect": "percentage"}, {"id": 6, "description": "draft report section", "expect": "text"}, def fake execution env step: dict - str: """Simulated environment: the data source CHANGES mid-run.""" if step "id" == 2 and not REPLANNED 0 : return "ERROR: revenue API schema changed, numeric dict expected" reality diverges return f"ok - {step 'expect' }"REPLANNED = False tracks whether the demo already replanned --------------------------------------------------------------------------- BAD: fires and forgets. Executes a dead plan to the end. ---------------------------------------------------------------------------def bad execute plan: list dict - list str : results = for step in plan: no expectation check at all results.append fake execution env step ERROR result is just appended like any other output - downstream steps build on a broken precondition and the compound error rolls on to step 20. return results --------------------------------------------------------------------------- GOOD: executor + expectation gate + replan trigger. ---------------------------------------------------------------------------def expected met observation: str, step: dict - bool: """Cheap check: does the observation match the step's expectation? In production this is a schema/type assert or a tiny classifier call, NOT a full frontier-model reflection.""" return not observation.startswith "ERROR" def plan and execute goal: str - dict: trace = state = None plan = fake planner goal PLAN ONCE while plan: work through the plan step = plan.pop 0 obs = fake execution env step EXECUTE current step trace.append f"step {step 'id' }: {obs :60 }" if expected met obs, step : continue healthy, carry on MISMATCH - REPLAN from current state, not from scratch. print f" replan step {step 'id' } expectation '{step 'expect' }' unmet" REPLANNED 0 = True plan = fake planner goal, state={ step, "observation": obs, "trace": trace} return {"result": "quarterly report drafted", "trace": trace} --------------------------------------------------------------------------- When to use this pattern vs plain ReAct: steps <= 5 and reversible actions - ReAct planning is waste 6-20 steps, structure known in advance - Plan-and-Execute this plan likely to face divergent reality - add the replan trigger ---------------------------------------------------------------------------if name == " main ": print "=== BAD: executes a dead plan straight off a cliff ===" for line in bad execute fake planner "quarterly report" : print f" {line}" print " - rest of the plan ran on broken assumptions.\n" print "=== GOOD: replan trigger rescues the run ===" result = plan and execute "quarterly business report" for line in result "trace" : print f" {line}" print f"\n final: {result 'result' }" The snippet shows both worlds. The bad version executes a dead plan straight to the end: an error observation is appended like any other output, and downstream steps build on a broken precondition. The good version checks every step’s expected output against the actual observation, fires a REPLAN signal on mismatch, and regenerates the remaining plan from current state rather than from scratch. Notice that the replan check is a cheap, deterministic comparison, not another expensive model call. A sequential workflow is an assembly line: the output of one agent or stage becomes the validated input of the next. Content generation flows into an SEO check, which flows into social formatting. Retrieval flows into analysis, which flows into report writing. This pattern barely gets conference talks, but production engineers keep landing on it, and for good reason. Sequential chaining with validation eliminates, by design, the three failure modes that plague every multi-agent system. There are no infinite loops because the chain is strictly sequential. There is no collaborative hallucination because each stage operates on validated data rather than agent-to-agent conversation. There is minimal coordination overhead because the routing is a function call, not an LLM classification. The critical word is validated . The pattern works because an explicit gate sits between stages: a schema check, a test suite, a deterministic assertion. Unvalidated chaining is just a pipeline that confidently propagates garbage. Validated chaining is how most real business logic gets built on LLMs: boring, predictable, testable, and cheap to debug, because when stage three fails you know exactly which stage to inspect. When you compare framework benchmarks, this is also where the cost differences get dramatic. Token efficiency varies by two to three times across popular frameworks for the same workflow, because some pass full conversation context between stages while others pass compact state deltas. At a million requests per month, that difference is real money. A deterministic chain where you control exactly what context each stage receives is the cheapest architecture you can build. """Sequential Workflow: shared mutable state vs validated immutable payloads.A sequential workflow is an assembly line: the output of one stage becomesthe VALIDATED input of the next. It is the pattern that quietly wins inproduction because it eliminates, by design, the three failure modes thatplague multi-agent systems:1. No infinite loops -- the chain is strictly sequential.2. No collaborative hallucination -- each stage operates on validated data, not agent-to-agent conversation.3. Minimal coordination overhead -- routing is a function call, not an LLM classification.The critical word is VALIDATED. Unvalidated chaining is a pipeline thatconfidently propagates garbage. Every handoff needs a gate.Run: python sequential workflow.py"""from dataclasses import dataclassfrom typing import Callable --------------------------------------------------------------------------- BAD: one shared mutable dict that every stage reads and writes. ---------------------------------------------------------------------------SHARED STATE = {"topic": "agent patterns", "tone": "professional", "draft": None, "seo": None}def bad stage generate state: dict - dict: state "draft" = f"Draft about {state 'topic' }" writes shared state return statedef bad stage seo state: dict - dict: Reads 'draft'... which might not exist yet if stages run out of order, or might contain another stage's half-written value. No validation. state "seo" = f"SEO ok for: {state.get 'draft', 'MISSING' }" return state Failure modes: hidden coupling, out-of-order execution, no gate between stages, and one bad write silently corrupts everything downstream. --------------------------------------------------------------------------- GOOD: immutable validated payloads passed explicitly between stages. ---------------------------------------------------------------------------@dataclass frozen=True class GeneratedDraft: topic: str body: str@dataclass frozen=True class SeoResult: draft: GeneratedDraft keywords: list str def validate cond: bool, msg: str - None: """The gate. Fail LOUDLY at the boundary, never propagate garbage.""" if not cond: raise ValueError f"Validation gate failed: {msg}" def stage generate topic: str - GeneratedDraft: body = f"A structured article about {topic}" placeholder LLM call GATE 1: assert the output contract before anything downstream sees it. validate len body 10, "draft too short" return GeneratedDraft topic=topic, body=body def stage seo draft: GeneratedDraft - SeoResult: The type system IS part of the gate: this function cannot be called with an unvalidated string. It only accepts a GeneratedDraft. keywords = draft.topic, "ai agents" placeholder LLM call validate len keywords 0, "no keywords extracted" return SeoResult draft=draft, keywords=keywords def stage format seo: SeoResult - str: GATE 3: final output check placeholder: assert publishable shape . validate seo.draft.topic in str seo.keywords , "topic lost in formatting" return f"PUBLISHED: {seo.draft.body} keywords: {', '.join seo.keywords } "def validated pipeline topic: str - str: """ The routing between stages is plain Python. No LLM decisions, no negotiation, no shared state. When stage 2 fails you know EXACTLY which stage to inspect -- debugging is a stack trace, not archaeology. """ draft = stage generate topic stage 1 - gate 1 seo = stage seo draft stage 2 - gate 2 return stage format seo stage 3 - gate 3 --------------------------------------------------------------------------- Why not multi-agent here? Decision table: --------------------------------------------------------------------------- | Question | This task | |---------------------------------------------|----------------| | Steps listable in advance? | Yes | | Each stage's input checkable with code? | Yes | | Domains need different tools per agent? | No | | Would one prompt be overwhelmed? | No | = Sequential workflow with gates. Not a supervisor.if name == " main ": print "=== BAD: shared mutable state, no gates ===" Imagine these run out of order a retry, a parallel scheduler : out of order = dict SHARED STATE bad stage seo out of order runs BEFORE generate print f" seo result: {out of order 'seo' }" print " - silently accepted a missing draft as None. No error raised.\n" print "=== GOOD: sequential stages with validation gates ===" final = validated pipeline "ai agent design patterns" print f" {final}" When a gate fails, it fails LOUDLY with a named stage, e.g.: try: stage generate "x" too short - gate 1 rejects it except ValueError as e: print f"\n gate demo {e}" The snippet builds a three-stage content pipeline where each stage is a function call and each handoff passes through an explicit validation gate. The bad version shares a mutable state dictionary across stages and lets any stage read anything. The good version passes immutable, validated payloads and fails loudly at the first gate. This is deliberately unglamorous code. That is the point. Now the pattern everyone wants to talk about. An orchestrator or supervisor agent receives a goal, decomposes it, and delegates subtasks to specialized workers, each with its own role, tools, and context. LangGraph’s supervisor pattern, CrewAI’s hierarchical process, and OpenAI’s triage-and-handoff model all implement this shape. The genuine advantages are real: focused workers with tight context windows are easier to make correct than one agent juggling everything, subtasks with different toolsets can run in parallel, and per-role audit attribution satisfies compliance requirements. But the production evidence is overwhelmingly one-sided. The failures are predictable and well documented: Here is the decision rule, distilled from what teams actually ship: One team’s experience captures the pattern of failure and correction: they built a multi-agent research pipeline, watched it fail under production traffic, then collapsed it back to a single agent with a longer context window and a plan-and-execute loop. Same task, half the cost, better output. The lesson they kept relearning, and that the wider community keeps confirming, is that most applications do not need agents that collaborate. They need agents that each do one thing well, connected by deterministic code. If you do reach multi-agent, three rules are non-negotiable: no two agents may have a mutual dependency where each can delegate to the other, at least one agent must have access to real data rather than other agents’ outputs, and every agent carries an explicit iteration cap. """Supervisor / Multi-Agent: mutual delegation chaos vs a disciplined hub.Multi-agent systems amplify errors instead of reducing them when builtnaively: 10 agents each 95% accurate, sharing state, produce a system thatfails ~40% of requests 0.95 10 = 0.60, and state contamination makes itworse . Documented runaway scenarios burn $50-500 per incident.The BAD version lets agents delegate to each other freely - infinitehandoff loops A asks B, B asks A back, no exit criteria . The GOOD versionenforces the three non-negotiable rules:1. NO MUTUAL DELEGATION: if A can delegate to B, B can never delegate back to A. All routing flows through one hub supervisor .2. GROUND TRUTH CHECKPOINT: at least one worker reads real data, so the system cannot do "collaborative hallucination".3. HARD CAPS: every loop has max iterations and a token budget. An unconstrained agent is a denial-of-service attack on your own bill.Run: python supervisor pattern.py"""from dataclasses import dataclass, field --------------------------------------------------------------------------- BAD: peer-to-peer mesh where agents can call each other. ---------------------------------------------------------------------------def bad multi agent : """Agent A and Agent B have overlapping scope and no exit criteria.""" log = turn, speaker, question = 0, "A", "who validates the data?" while turn < 5 and speaker = "DONE": NO hard cap range 5 = demo mercy if speaker == "A": log.append f"A asks B: {turn}" speaker = "B" A can talk to B... else: log.append f"B asks A back: {turn}" speaker = "A" ...and B can delegate back - LOOP turn += 1 return log --------------------------------------------------------------------------- GOOD: supervisor hub-and-spoke. ---------------------------------------------------------------------------@dataclassclass Worker: name: str tools: list@dataclassclass SupervisorRun: answer: str | None = None handoffs: list = field default factory=list cost units: int = 0 token budget accounting hit cap: bool = FalseMAX HANDOFFS = 6 hard cap 1: bounded handoffsMAX COST UNITS = 100 hard cap 2: bounded spendWORKERS = { "research": Worker "research", tools= "web search", "docs" , "database": Worker "database", tools= "sql" , GROUND TRUTH worker "writer": Worker "writer", tools= "none" , pure text synthesis}def supervisor route task: str - str: """ Routing decision lives in CODE a tiny classifier/ruleset here , not in a free-form LLM conversation. Deterministic routing is debuggable: when something goes wrong there is exactly one node to inspect. A production supervisor may classify with an LLM, but the TOPOLOGY guarantees every handoff passes through this single point. """ if "lookup" in task: return "database" facts come from real data, never from peers if "summarize" in task: return "writer" return "research"def ground truth lookup query: str - str: return "acme corp: 1,200 employees, $85M raised" real data sourcedef worker execute worker name: str, task: str - str: w = WORKERS worker name if "sql" in w.tools: return ground truth lookup task if worker name == "writer": return f"summary grounded in: {task}" return f"research notes on: {task}"def supervisor run tasks: list str - SupervisorRun: run = SupervisorRun for task in tasks: THE RULE: workers never talk to each other. The supervisor is the only node that can initiate a handoff decisively breaking loops . for handoff in range MAX HANDOFFS : route = supervisor route task run.cost units += 10 every hop costs tokens if run.cost units MAX COST UNITS: budget enforcement run.hit cap = True run.answer = "aborted: token budget exceeded" return run result = worker execute route, task run.handoffs.append f"{task} - {route}: {result :50 }" Supervisor validates the worker output against the plan gate. if result and "ERROR" not in result: run.answer = result if "summary" in result or "acme" in result else run.answer break progress - next task else: Exited by handoff cap: escalate rather than loop forever. run.hit cap = True run.answer = "escalated to human handoff cap " return runif name == " main ": print "=== BAD: mutual delegation mesh topology ===" for line in bad multi agent : print f" {line}" print " - A and B ping-pong forever. Only a rate limit ends it.\n" print "=== GOOD: supervisor hub with caps and ground truth ===" run = supervisor run "lookup new competitor", "summarize findings" for line in run.handoffs: print f" {line}" print f" cost: {run.cost units}/{MAX COST UNITS} units, cap hit: {run.hit cap}" Measure coordination overhead in YOUR system: coordination tokens routing + summarizing between agents ----------------------------------------------------------- work tokens actual task execution If coordination 40% of spend, a single agent with better tools would be cheaper AND more reliable. That threshold is the exit sign. The snippet contrasts both topologies. The bad version is a peer-to-peer mesh where Agent A and Agent B have overlapping scope and can delegate to each other, so they ping-pong forever until a rate limit intervenes. The good version is a hub-and-spoke supervisor: routing flows only through one hub in code, a dedicated ground-truth worker reads real data so no chain of agents can hallucinate consensus, and the run carries hard caps on both handoffs and token budget. The closing code returns a coordination-overhead ratio you should measure in your own system, because if coordination exceeds 40 percent of your spend, a single agent with better tools would be cheaper and more reliable. Everything above compresses into a single progressive ladder that the industry has converged on: The discipline is to add complexity only when the simpler pattern’s failure mode has been observed, not because the sophisticated architecture looks impressive in a demo. Every layer you add buys capability and spends latency, cost, and debuggability. Production systems optimize for predictability, and a 95 percent per-step success rate is a promise architecture makes, not a model. Ask yourself the question that separates systems that survive from systems that get rebuilt: does this design decision make the system easier to reason about six months from now? If the answer is no, you are adding complexity faster than you are adding capability. In production, complexity always collects its debt. The agent pattern landscape in 2026 is not a menu of equally valid choices. It is a ladder, and the evidence from teams running these systems at scale points in one direction: the simplest pattern that handles the task is almost always the correct one. The engineers getting the best results are not the ones with the most agents. They are the ones with the best tool schemas, the tightest validation gates, and the strongest restraint about when to add a second agent. Here are several key takeaways from this article: Thank you for reading this article I hope you found it helpful. If you have any questions or feedback, please feel free to reach out to me. ArtificialIntelligence AIEngineering AIAgents LLM SoftwareArchitecture MachineLearning TechTrends The 6 AI Agent Design Patterns Every Engineer Should Know And When Not to Use Them https://pub.towardsai.net/the-6-ai-agent-design-patterns-every-engineer-should-know-and-when-not-to-use-them-3720ac3b5b90 was originally published in Towards AI https://pub.towardsai.net on Medium, where people are continuing the conversation by highlighting and responding to this story.