DEV Community

6 Agentic AI Design Patterns: Which one to pick when

Disclaimer: Self written post, not AI generated.

We all are now in the age of Agentic AI and agents. Every topic, every discussion, every application is somehow concluding with only one conclusion, let's make an AI agent.

When we decide to build an AI agent, then mostly the planning starts with a wrong question, "LangGraph or CrewAI?"

The actual question that should be asked that determines whether your app is debuggable or maintainable or not is this:

Who decides what happens next, Your Code or the model?

Every Agentic AI Design Pattern will give an answer to that question.

Here are 6 most popular and efficient Design Patterns along with some classification so that its easy to remember.

Agentic AI Design Patterns Classification

As mentioned in the chart above, there are majorly two categories, Deterministic and Autonomous

Deterministic Workflows
  • Call a model one or more times, but execution path lives in the source code
  • You can read and understand the flow from the code
  • Rerunning the same input takes the same route
Autonomous Workflows
  • LLM decide the execution path
  • Model decides the tool calling, status of work and who should handle next
  • Solve problems whose steps you could not know in advance
  • Same input may not take same route
  • Reproducibility becomes a challenge

Neither category is more advanced than the other, but choosing an autonomous workflow when a deterministic workflow would fit, then its the most expensive mistake, not just in terms of tokenomics but you pay with non-determinism which you didnt need.

Code Examples

Rules for the examples

  • Every example is complete and runnable in under 45 lines
  • Every pattern example share a small helper llm.py.
  • The helper returns actual output if anthropic api key is provided else it would return a dummy/mocked output.
  • Claude Opus 5 was used to generate the code example, but with specific instructions, specification, and actual code review scrutiny. Minor manual changes were also made in the code examples.

Common Helper: llm.py

This is the whole abstraction, that accepts a prompt and returns a string output.
The stub argument is the dummy/mocked text that gets returned when the API key is not set. So, every example can run offline. The intention here is to learn the pattern and not to create a working app. To call the actual LLM, export ANTHROPIC_API_KEY and no other code changes needed.

# llm.py
import os

MODEL = os.getenv("ANTHROPIC_MODEL", "claude-opus-5")
API_KEY = os.getenv("ANTHROPIC_API_KEY", None)


def ask(prompt: str, system: str | None = None, stub: str = "") -> str:
    """One prompt in, one string out."""
    if not API_KEY:
        return stub

    # Not a fan of inline imports, but keeping it here so that
    # the examples can also run offline without installing the package.
    import anthropic
    anthropic_client = anthropic.Anthropic(api_key=API_KEY)

    kwargs = {
        "model": MODEL,
        "max_tokens": 4096,
        "messages": [{"role": "user", "content": prompt}],
    }
    if system:
        kwargs["system"] = system

    reply = anthropic_client.messages.create(**kwargs)
    return "".join(b.text for b in reply.content if b.type == "text").strip()
Enter fullscreen mode Exit fullscreen mode

Part 1: Deterministic Workflow: Code controls the execution


1. Planning Design Pattern

  • Ask the model for a plan
  • Execute the plan in a loop
# pattern_1_planning.py

import json
from llm import ask

GOAL = "Ship a REST API for expense reports"


def make_plan(goal):
    reply = ask(
        f'Break this into 3 ordered steps. JSON only: {{"steps": [str]}}'
        f'\n\nGoal: {goal}',
        system="You are a staff engineer.",
        # Stub is Dummy output when no api key is provided
        stub='{"steps": ["design the data model", "write the endpoints", "add tests"]}'
    )
    return json.loads(reply)["steps"]


def build(step, done):
    return ask(
        f"Already done: {done or 'nothing'}\nNow do exactly this step: {step}",
        system="You are an engineer. Do one step. Output the artifact only.",
        stub=f"<artifact for: {step}>"
    )


steps = make_plan(GOAL)          # ONE call decides the shape of all the work
print("PLAN:", steps)

done = []
for i, step in enumerate(steps, 1):   # this loop is Python. The model has no say.
    print(f"\n[{i}/{len(steps)}] {step}\n    {build(step, done)}")
    done.append(step)
Enter fullscreen mode Exit fullscreen mode

Output

PLAN: ['design the data model', 'write the endpoints', 'add tests']

[1/3] design the data model
    <artifact for: design the data model>

[2/3] write the endpoints
    <artifact for: write the endpoints>

[3/3] add tests
    <artifact for: add tests>
Enter fullscreen mode Exit fullscreen mode

In this pattern, the plan is made first, it can be in the form of a spec as well. Each step is a separate call with a small brief, which keeps the context small. Every failure is localised, means if the step three produces garbage then you re-run only step three.

Actual implementations adds a dependency graph and a checkpoint after each step. Checkpoints or gates can be

  • Does the artifact exists?
  • Do the tests pass?
  • Does the application compile and run?

A failed checkpoint retries one step rather than restarting the run and running all steps.

When to use this pattern
  • You already know the scope of work
  • Example
    • a weekly or periodic report always has same sections
    • a scaffold would always need a database model before an API
When it would hurt
  • If the success of the next step genuinely depends on what the last step found
  • A fixed plan cannot react, so it will confidently execute the stale plan

2. Voting/Consensus Design Pattern

  • Ask several times independently
  • Let the code decide what the answer means
# pattern_2_voting_consensus.py

from collections import Counter
from concurrent.futures import ThreadPoolExecutor

from llm import ask

CLAUSE = "Vendor's liability is unlimited. The contract auto-renews for 24 months."

# Stub decisions, one per lens/role, used when no API key is set.
LENSES = {
    "liability": "BLOCK",
    "privacy": "ACCEPT",
    "finance": "NEGOTIATE",
    "commercial": "NEGOTIATE",
    "regulatory": "ACCEPT",
}  # dummy output if no anthropic api key is provided.


def review(lens):
    return ask(
        f"Clause: {CLAUSE}\n\nReply with one word: BLOCK, NEGOTIATE or ACCEPT.",
        system=f"You review contracts through one lens only: {lens}.",
        stub=LENSES[lens],
    ).upper()


with ThreadPoolExecutor() as pool:  # independent calls, multiprocessing can be used as well
    votes = list(pool.map(review, LENSES))

tally = Counter(votes)
for lens, vote in zip(LENSES, votes):
    print(f"{lens:<12} {vote}")

top, count = tally.most_common(1)[0]
decision = top if count >= 3 else "ESCALATE"  # the policy is code, not a prompt
print(f"\ntally={dict(tally)} -> {decision}")
Enter fullscreen mode Exit fullscreen mode

Output

liability    BLOCK
privacy      ACCEPT
finance      NEGOTIATE
commercial   NEGOTIATE
regulatory   ACCEPT

tally={'BLOCK': 1, 'ACCEPT': 2, 'NEGOTIATE': 2} -> ESCALATE
Enter fullscreen mode Exit fullscreen mode

This design pattern depends on two important points. Its easy to get both these points wrong.

  1. The reviewers must not see each other's response.
    The moment the reviewers can see each other's response, their errors would correlate and that would lead to same opinion from all the reviewers.

  2. Disagreement is a result, not a problem
    A single call gives the confidence, but five calls gives a distribution, like 3:2, or 2:2:1 or any other combination. Let human in the loop or any other agent or even a simple python code can evaluate the distributed output. Escalating to human or other agent is the feature. Most importantly, trying to break the tie throws the only information that is produced by the pattern.

Additionally, there can be policies, for example, a "BLOCK" from a "Liability Reviewer" might veto regardless of the count, and this rule will belong to the code.

When to use this pattern
  • When being wrong is expensive and correctness can be checked by judgement or rules in the code.
  • Example
    • Compliance review
    • Medical triage
When it would hurt
  • When cost matters more than the confidence
  • The example above is 5x the price of one call, every time.

Part 2: Autonomous Workflow: LLM controls the execution


1. ReAct Design Pattern

  • Its a loop, Reason + Action = ReAct
  • Model thinks -> Picks a tool -> Analyse the output -> Think again
# pattern_3_react.py
# ReAct. The model picks the next tool call every turn

import re

from llm import ask

STOCK = {"SKU-4417": 6}

TOOLS = {
    "check_stock": lambda sku: f"{sku}: {STOCK.get(sku, 0)} units on hand",
    "reorder": lambda sku, qty: f"created a PO for {qty} units of {sku}",
}

SYSTEM = """Work in a loop. Each turn emit exactly one line, either:
Action: tool_name(arg, ...)
Done: <your answer>

Tools: check_stock(sku), reorder(sku, qty)"""

# dummy output if no anthropic api key is provided.
STUBS = [
    "Action: check_stock(SKU-4417)",
    "Action: reorder(SKU-4417, 120)",
    "Done: Only 6 units were left before a promo, so I ordered 120 more.",
]

transcript = "Task: SKU-4417 may stock out before the weekend promo. Fix it."

for step in range(6):  # the step cap belongs in code, not the prompt
    stub = STUBS[step] if step < len(STUBS) else "Done: out of steps"
    reply = ask(transcript, system=SYSTEM, stub=stub)
    print(reply)

    if reply.startswith("Done:"):
        break

    name, raw = re.match(r"Action:\s*(\w+)\((.*)\)", reply).groups()
    result = TOOLS[name](*[a.strip() for a in raw.split(",") if a.strip()])
    print(f"Observation: {result}\n")
    transcript += f"\n{reply}\nObservation: {result}"
Enter fullscreen mode Exit fullscreen mode

Output

Action: check_stock(SKU-4417)
Observation: SKU-4417: 6 units on hand

Action: reorder(SKU-4417, 120)
Observation: created a PO for 120 units of SKU-4417

Done: Only 6 units were left before a promo, so I ordered 120 more.
Enter fullscreen mode Exit fullscreen mode

Important thing to notice here is that the loop decides very little, it parses an action, runs it, appends the result and then repeats.

Nowhere in the code it is mentioned that "check stock before reordering". The order was an "Action" based on the "Reason" from the first observation.

That is the entire value proposition here, the path is discovered, not declared.

The Step Cap: A confused or hallucinating agent might loop until you will notice your bill.

When to use this pattern
  • When the path cannot be known in advance.
  • Example
    • Debugging or Incident Response
    • Live data investigation, like ticker prices
When it would hurt
  • When the "Path" is known or "knowable"
  • Then you are paying per model call to rediscover a sequence that could be written down once

2. Reflection Design Pattern

  • First call produced the work
  • Second call with a different job attacks it
# pattern_4_reflection.py
# A critic decides when the work is good enough

from llm import ask

BRIEF = "Meta description for a rechargeable trail headlamp. Max 155 characters."
LIMIT = 155

# dummy output if no anthropic api key is provided.
DRAFTS = [
    "Discover the best-in-class TrailLight 2, a revolutionary rechargeable trail "
    "headlamp built for serious night runners, with a huge 40-hour burn time and "
    "an ultra-bright beam. Buy yours today.",
    "TrailLight 2: a rechargeable trail headlamp with 40-hour burn time and a "
    "400-lumen beam for night runs. See sizes and pricing.",
]
CRITIQUES = ["FAIL: 'best-in-class' is filler and the CTA is generic.", "PASS"]


def lint(text):  # never pay a model to count characters
    return [f"{len(text)} characters, limit is {LIMIT}"] if len(text) > LIMIT else []


issues = []
for round_no in range(3):  # the ceiling is code, the critic decides
    i = min(round_no, len(DRAFTS) - 1)

    draft = ask(
        f"{BRIEF}\nFix these issues: {issues or 'none'}",
        system="Write the description only.",
        stub=DRAFTS[i],
    )
    verdict = ask(
        f"Brief: {BRIEF}\nDraft: {draft}\n\nReply PASS, or FAIL: <reason>.",
        system="You are a hostile reviewer. Find what is wrong.",
        stub=CRITIQUES[i],
    )

    issues = lint(draft) + ([verdict] if verdict.startswith("FAIL") else [])
    print(f"\nround {round_no + 1} ({len(draft)} chars): {draft}")
    print(f"  -> {issues or 'APPROVED'}")

    if not issues:
        break
Enter fullscreen mode Exit fullscreen mode

Output

round 1 (190 chars): Discover the best-in-class TrailLight 2, a revolutionary rechargeable trail headlamp built for serious night runners, with a huge 40-hour burn time and an ultra-bright beam. Buy yours today.
  -> ['190 characters, limit is 155', "FAIL: 'best-in-class' is filler and the CTA is generic."]

round 2 (126 chars): TrailLight 2: a rechargeable trail headlamp with 40-hour burn time and a 400-lumen beam for night runs. See sizes and pricing.
  -> APPROVED
Enter fullscreen mode Exit fullscreen mode

The loop length is not fixed. The loops runs until the "critic" is satisfied or the ceiling is hit. In the above example, ceiling is the loop limit itself. This makes it an autonomous pattern rather than a two step workflow.

Anything a regex can do, it can be put in code, and doesn't require a costly llm call. Example like character counts, banned words or phrases, required keywords, schema validation etc.

lint() method checks for the length violation with full reliability. These small things doesn't require llm. Use the model only for the judgement like whether the call to action is specific, whether a claim is quantified and so on.

Give the "critic" a different job. Find what is wrong with the judgement output. Do not use critic to just redo the same thing that is being done by the judgement call.

When to use this pattern
  • When the first draft quality is insufficient
  • Or failure modes are hard to specify up front
  • Example
    • Code review
    • Vulnerability patching
When it would hurt
  • If the judgement is deterministic
  • Example, if a test suite can tell whether the work is correct, then run the tests, do not ask model for opinion.

3. Collaboration Design Pattern

  • Specialist agents hand over the work to each other
  • No manager or supervisor
# pattern_5_collaboration_pattern.py

import json

from llm import ask

PEERS = {
    "copywriter": "You write short social copy.",
    "art_director": "You turn copy into an image direction.",
    "compliance": "You check claims and reject unsupported ones.",
}

# dummy output if no anthropic api key is provided.
STUBS = [
    '{"work": "Headline: One cup. Every refill.", "to": "art_director"}',
    '{"work": "Overhead shot of one cup, faint coffee rings, morning light.",'
    ' "to": "compliance"}',
    '{"work": "REJECTED: plastic-free is unsupported, the lid gasket is silicone.",'
    ' "to": "copywriter"}',
    '{"work": "Headline: Your 4th coffee of the day, same cup.", "to": "art_director"}',
    '{"work": "2x2 grid: the same cup at four times of one workday.",'
    ' "to": "compliance"}',
    '{"work": "APPROVED. Every claim is substantiated.", "to": "done"}',
]

thread, peer = [], "copywriter"

for handoff in range(8):  # the cycle guard belongs in code
    reply = json.loads(
        ask(
            "Campaign: spring launch for a refillable coffee cup.\n"
            f"Thread so far: {thread or 'nothing yet'}\n\n"
            f"Do your part, then pick who goes next ({', '.join(PEERS)}, or done).\n"
            'JSON only: {"work": str, "to": str}',
            system=PEERS[peer],
            stub=STUBS[min(handoff, len(STUBS) - 1)],
        )
    )
    print(f"{handoff + 1}. {peer:<13} {reply['work']}\n   -> {reply['to']}")
    thread.append(f"{peer}: {reply['work']}")

    if reply["to"] == "done":
        break
    peer = reply["to"]  # note: a peer can hand work BACKWARDS
Enter fullscreen mode Exit fullscreen mode

Output

1. copywriter    Headline: One cup. Every refill.
   -> art_director

2. art_director  Overhead shot of one cup, faint coffee rings, morning light.
   -> compliance

3. compliance    REJECTED: plastic-free is unsupported, the lid gasket is silicone.
   -> copywriter

4. copywriter    Headline: Your 4th coffee of the day, same cup.
   -> art_director

5. art_director  2x2 grid: the same cup at four times of one workday.
   -> compliance

6. compliance    APPROVED. Every claim is substantiated.
   -> done
Enter fullscreen mode Exit fullscreen mode

In the above example, 6 handovers or collaborations have happened.
But, the third handover is the one where this pattern proves itself.

What happened at handover 3
When compliance got its turn, it read the thread and image generated in previous two handovers, and then sent it back to the copywriter who already had its turn.

If we look at first three handovers in isolation: copywriter -> art_director -> compliance, then that is indistinguishable from a hard coded pipeline.

If compliance had approved at handover 3, then the run would have ended and there would be no evidence that we are looking something other than a three sequential stages.

The backward handoff in this case is what makes this pattern like a mesh and not a fixed pipeline. Changing headline also invalidates the image direction, so at 4th handover art_director is called again. One rejection propagated through two peers.

Compliance sent work backwards, and nothing scheduled that rework loop, a peer chose it in mid run. That neither requires a fixed pipeline nor a manager/supervisor.

The cost of that freedom is the cycles. It can also end up in an infinite cycle loop where compliance and copywriter would handover each other indefinitely. So a guardrail is needed, and a guardrail need not be something fancy, it can be as simple as a ceiling number in the code or a per peer visit/handover cap.

When to use this pattern
  • When the work is a chain of specialist
  • And each handoff needs the output of previous specialist
  • Example
    • Creative pipeline
    • Multi stage routing
When it would hurt
  • When a single accountable answer is needed
  • Nobody in the mesh of specialist owns the outcome

4. Supervisor Design Pattern

  • One manager or supervisor
  • Workers that only see their assignment
# pattern_6_supervisor.py

import json

from llm import ask

TICKET = (
    "Since the SSO cutover we were charged twice in March, and our security team "
    "sees logins from IP ranges nobody recognises. Renewal is in three weeks."
)

WORKERS = {
    "billing": "You own invoices and refunds.",
    "platform": "You own SSO and provisioning.",
}

BOSS = "You triage enterprise support tickets."

# dummy output if no anthropic api key is provided.
PLAN_STUB = (
    '{"assign": [{"worker": "billing", "task": "Is the duplicate charge real?"},'
    '{"worker": "platform", "task": "Did the SSO cutover break anything?"}]}'
)
FINAL_STUB = (
    '{"answer": "One misconfigured cutover created a duplicate org record. That '
    'single fault caused both the double invoice and the unfamiliar logins, which '
    'came from our own provisioning range."}'
)
REPORTS = {
    "billing": "Two charges four days apart. A duplicate org entity was billed twice.",
    "platform": "SCIM still pointed at the old directory and created a second org.",
}

plan = json.loads(
    ask(
        f"Ticket: {TICKET}\n\nAssign work to {list(WORKERS)}.\n"
        'JSON only: {"assign": [{"worker": str, "task": str}]}',
        system=BOSS,
        stub=PLAN_STUB,
    )
)

reports = {}
for job in plan["assign"]:
    reports[job["worker"]] = ask(
        f"Ticket: {TICKET}\n\nYour task: {job['task']}",   # ENTIRE context
        system=WORKERS[job["worker"]],
        stub=REPORTS[job["worker"]],
    )
    print(f"{job['worker']:<10} {reports[job['worker']]}")

answer = json.loads(
    ask(
        f"Ticket: {TICKET}\nWorker reports: {reports}\n\n"
        'Write the answer to the customer. JSON only: {"answer": str}',
        system=BOSS,
        stub=FINAL_STUB,
    )
)["answer"]

print(f"\nRESOLUTION: {answer}")
Enter fullscreen mode Exit fullscreen mode

Output

billing    Two charges four days apart. A duplicate org entity was billed twice.

platform   SCIM still pointed at the old directory and created a second org.

RESOLUTION: One misconfigured cutover created a duplicate org record. That single fault caused both the double invoice and the unfamiliar logins, which came from our own provisioning range.

Enter fullscreen mode Exit fullscreen mode

The line thats doing the heavy lifting is the single f-string thats building the worker prompt. That f-string is the isolation boundary, the ticket plus the worker's own task and nothing else. No sibling reports, no manager reasoning, no history.

Every worker's context remains small and workers can run in parallel because there is no dependency on each other. A wrong guess by one worker cannot become the premise of other worker.

The price is that all the cross cutting and decisions happens in the supervisor. If two workers genuinely need each others raw output, then the collaboration is better.

When to use this pattern
  • When the work or request splits into independent specialists and someone has to be accountable for the combined answer
  • And each handoff needs the output of previous specialist
  • Example
    • Understanding code base, multi analyst research
    • something like "search these six subsystems and tell me the answer
When it would hurt
  • When the workers or subtasks are dependent on each other

Read/Understand them in pair

These patterns teach more against each other than alone.

Planning vs ReAct

  • Same job shape but opposite control flow
  • Planning fixes the order, whereas ReAct discovers the order on the go.

Choosing wrongly is either paying for non-determinism you didn't need or executing a stale plan

Voting vs Reflection

  • Both use multiple calls but for opposite reasons
  • Voting keeps the reviewers separate. Disagreement is actually a measurement.
  • Reflection deliberately shows the output to critic because the goal is improving not measuring confidence.

Collaboration vs Supervisor

  • The difference is in the context
  • Collaboration shares the thread and allows backward handoffs
  • Supervisor isolate each worker so that one worker's guess cannot impact other worker

Choosing the pattern

If this is true Use
You can write the steps down before you start Planning
Being wrong is expensive, and judgement decides correctness Voting
The next step depends on what the last step found ReAct
First drafts are not good enough and the flaws are hard to pre-specify Reflection
Specialists must pass real work between them, with rework Collaboration
Independent specialist questions, one accountable answer Supervisor
None of the above clearly applies A single model call. Seriously, KISS.

The last row is not a joke. Before reaching for any of this pattern, check four things:

  1. is the task genuinely multi step and hard to fully specify
  2. does the outcome justify the cost and latency
  3. is the model actually needed for doing it
  4. can errors be caught and can the app recover from it

A single "no" to any of the four points above means change your design.

These four criterias are from the Anthropic's own guidance on when to build an agent and when not to. Following is the screenshot from the mentioned source and this one paragraph itself is more than enough to tell where most of the developers are going wrong.

https://www.anthropic.com/engineering/building-effective-agents

Anthropic reference


The part that nobody explicitly tells

Every pattern above needs guardrails, and in every case it belong in code rather than in a prompt. A prompt is a request, but the code is a constraint.

  1. Planning: Validate the plan first. Put a gate on each step's artifact.
  2. Voting: The veto rule, the majority threshold and the escalation path
  3. ReAct: the step cap, and approval gates on tools
  4. Reflection: Every deterministic check and the maximum round count
  5. Collaboration: The handover limit, the per peer visit limit, routing validation
  6. Supervisor: The round ceiling or limit, and the isolation boudary itself

Notice that how many of these are one line, for step in range(6) is a real guardrail. "Please don't loop forever" is not.

One last habit worth building. Do not ever trust an agent's own summary of its work. The agent that says "Fixed and Verified", is producing the text, and not the evidence. If the outcome is checkable then check it yourself, in the code, like tests are passing, file exists, row counts matching etc.

This single step catches more failures than any amount of prompt engineering.

Top comments (0)