A Visual Guide for GenAI Builders

Workflows have fixed paths.
Agents choose their own.

A practical map of every pattern in Anthropic's Building Effective Agents - from a single tool call to a full ReAct loop - with the diagram and the Python for each one.

⚙ Augmented LLM 🔗 Workflows 🤖 Agent 📚 ReAct
The Taxonomy

Three layers, one building block

Read ten blog posts about "AI agents" and you'll see the word used to describe ten different things. Anthropic's taxonomy fixes that: every workflow and every agent is built from the same atomic unit - an augmented LLM call - composed differently.

🤖 Agent
An LLM in a loop, deciding which tools to call, in what order, and when to stop. The control flow is decided at runtime, by the model. Unbounded but capped with max_steps.
Model decides steps
🔗 Workflow
Multiple LLM calls wired together in a predefined structure: chains, routers, parallel branches, capped generate/evaluate loops. You write the control flow; the LLM just fills in each step.
You decide steps, at design time
⚙ Augmented LLM
One LLM call that can use tools, retrieval, or memory. The smallest unit in the stack - every workflow step and every agent step is one of these.
One round trip
Plain LLM
Text in, text out. A sealed box - no tools, no external state, no way to act on the world.
Foundation
💡
Mental shortcut: workflows have fixed code paths, agents have dynamic ones. Most production "AI products" today are workflows - they just get marketed as agents. That's not a problem: workflows are predictable, cheap, and debuggable. You pay the "agent tax" only when the task's shape genuinely depends on its input.
The Atomic Unit

The Augmented LLM

A plain LLM can't check today's weather, query your database, or reliably multiply two numbers - none of that lives in the weights. Cut a door into the box with tools, retrieval, or memory, and you get an augmented LLM.

one round trip
⚙ Tool calling, the primitive everything else is built on
Every workflow step and every agent step is one of these, repeated.
The tool-calling shape
  • 1. Describe - you give the model a JSON schema for each tool (name, description, parameters)
  • 2. Decide - the model either answers directly, or returns tool_calls asking to invoke one
  • 3. Execute - you run the tool in plain Python and get a result
  • 4. Resolve - you feed the result back as a tool message; the model writes the final answer
The three classic augmentations
  • Tools - request a function call, get a result back (calculators, APIs, code execution)
  • Retrieval - fetch external documents (RAG over a vector store)
  • Memory - read and write persistent state across turns
Why this matters Burn this sequence into memory - describe → decide → execute → resolve. Every "agent framework" you'll ever use is a wrapper around this exact loop, just dressed up with extra abstractions.
User question"What is (1289×47)−312?"
LLM calldecides: needs a tool
Execute toolyour Python code runs
LLM callwrites final answer
one user turn → at most one tool round → one final answer
augmented_llm.py - a calculator tool, end to end
import json import cohere co = cohere.ClientV2(api_key=api_key) MODEL = "command-a-03-2025" # ── 1. Describe: the schema the model sees ───────────────────────────────── tools = [{ "type": "function", "function": { "name": "calculator", "description": "Evaluates a math expression and returns the result.", "parameters": { "type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"], }, }, }] def calculator(expression: str) -> dict: # NEVER use eval() on untrusted input in real code - demo only. return {"result": eval(expression, {"__builtins__": {}}, {})} TOOL_REGISTRY = {"calculator": calculator} # ── 2. Decide: first call, model chooses ──────────────────────────────────── messages = [{"role": "user", "content": "What is (1289 * 47) - 312?"}] resp = co.chat(model=MODEL, messages=messages, tools=tools) if resp.message.tool_calls: messages.append({ "role": "assistant", "tool_calls": resp.message.tool_calls, "tool_plan": resp.message.tool_plan, }) # ── 3. Execute: run the tool yourself ─────────────────────────────────── for tc in resp.message.tool_calls: args = json.loads(tc.function.arguments) result = TOOL_REGISTRY[tc.function.name](**args) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result), }) # ── 4. Resolve: second call, model writes the final answer ────────────── resp = co.chat(model=MODEL, messages=messages, tools=tools) print(resp.message.content[0].text) # → "(1289 * 47) - 312 = 60271"
Predefined Control Flow

Four workflow patterns

Real tasks rarely fit in one round trip. A workflow composes several augmented LLM calls in a structure you design upfront. Pick the pattern that matches your input/output shape.

simplest workflow
🔗 Prompt Chaining
Sequential LLM calls where each step's output becomes the next step's input.
Why split into steps
  • One giant prompt doing 5 things underperforms 5 small prompts doing 1 thing each
  • Each step's instructions are short and unambiguous - more attention per task
  • Debuggable: a bad step 2 output means you only fix step 2's prompt
  • Swappable: replace any step with a different model, or even a regex
Production upgrade
  • Validation gates - add a cheap check between steps ("is this the right shape?") and abort early if not
  • Catches problems close to where they happen, instead of three steps downstream
When to use The task naturally decomposes into a fixed sequence of sub-tasks where step N always needs step N−1's output. Marketing copy generation, multi-stage document summarization, structured data extraction → formatting → validation.
Product description
LLM Aextract top 3 features
LLM Bwrite a tagline
LLM Cwrite Instagram post
input → [LLM A] → [LLM B] → [LLM C] → final output
prompt_chaining.py - marketing copy in 3 steps
def llm(prompt: str, system: str = "") -> str: msgs = ([{"role": "system", "content": system}] if system else []) msgs.append({"role": "user", "content": prompt}) return co.chat(model=MODEL, messages=msgs).message.content[0].text.strip() product = ( "NoiseNest is a $129 portable white-noise machine with 30 ambient sounds, " "a 40-hour battery, an app-free design, and a child-safe lock." ) # ── Step 1: extract features ──────────────────────────────────────────── features = llm( f"Extract the 3 most marketable features from this product:\n\n{product}\n\n" "Return a short bulleted list." ) # ── Step 2: tagline - uses features from step 1 ───────────────────────── tagline = llm(f"Write a single punchy tagline (max 10 words) for:\n\n{features}") # ── Step 3: social post - uses tagline from step 2 ────────────────────── post = llm(f"Expand this tagline into a 2-sentence Instagram caption:\n\nTagline: {tagline}")
classify, then dispatch
🔀 Routing
A small classifier decides what kind of input arrived, then dispatches to a specialist prompt.
Why bother routing
  • One generic prompt handling billing + technical + sales is a compromise - specialized prompts are sharper
  • Routing buys specialization without making the user pick a category
  • You can tune (and test) each branch independently
Two production tips
  • Use a small/cheap model for the classifier - classification is easier than generation, don't pay flagship prices for it
  • Always have a safe default. Classifiers occasionally return "BILLING" or "billing-related" - your dispatch code must not crash on those
When to use Distinct categories of input that genuinely benefit from different tone, instructions, or even different models/tools - customer support triage, content moderation tiers, query complexity routing (cheap model vs. flagship model).
User message
Classifiercheap model
billing specialist prompt
technical specialist prompt
general specialist prompt (safe default)
routing.py - customer-support triage
ROUTES = { "billing": "You are a billing specialist. Be precise about amounts and refund policies.", "technical": "You are a senior support engineer. Diagnose issues step by step. Be concise.", "general": "You are a friendly first-line support rep. Answer warmly and briefly.", } def route(user_msg: str) -> str: """Tiny classifier - returns one of: billing, technical, general.""" label = llm( f"Classify into exactly one of: billing, technical, general.\n\n" f"Message: {user_msg!r}\n\nReturn ONLY the label, lowercase." ).lower().strip() # Defensive default - never trust the classifier blindly. return label if label in ROUTES else "general" def handle(user_msg: str) -> str: label = route(user_msg) return llm(user_msg, system=ROUTES[label]) handle("Why was I charged twice this month?") # → routed to "billing", precise tone, refund-policy aware
independent calls, run together
⇶ Parallelization
When LLM calls are independent, run them concurrently instead of sequentially - then synthesize.
Two flavors
  • Sectioning - split a task into different subtasks, each handled by a specialist, then merge. (security / performance / style code review)
  • Voting - run the same prompt N times and aggregate (majority vote, pick best). Useful when reliability beats cost.
Two distinct wins
  • Latency - wall-clock time ≈ one call, not three. Use ThreadPoolExecutor since API calls are I/O-bound
  • Quality - each specialist has one job; the aggregator gets a richer, less confused signal than one model trying to do everything
When to use Subtasks that don't depend on each other's output. Don't forget the aggregator step - parallel branches still need synthesis into one final answer.
Code snippet
Security
reviewer
Performance
reviewer
Style
reviewer
Aggregatorconsolidated, prioritized list
3 reviewers run concurrently (~2s total, not ~6s) → 1 synthesis call
parallelization.py - sectioning: parallel code review
from concurrent.futures import ThreadPoolExecutor REVIEWERS = { "security": "You are a security reviewer. ONLY flag security issues. Be terse.", "performance": "You are a performance reviewer. ONLY flag performance issues. Be terse.", "style": "You are a code-style reviewer. ONLY flag style issues. Be terse.", } def review(name_system): name, system = name_system return name, llm(f"Review this code:\n\n```python\n{code_snippet}\n```", system) # ── Run all three reviewers concurrently - I/O-bound, threads are enough ─── with ThreadPoolExecutor(max_workers=3) as pool: reviews = dict(pool.map(review, REVIEWERS.items())) # ── Aggregator: one more call that synthesizes the parallel branches ────── merged = "\n\n".join(f"## {n}\n{t}" for n, t in reviews.items()) summary = llm( "Consolidate these parallel reviews into one prioritized list " "(highest severity first). Don't repeat overlapping findings.\n\n" + merged )
a capped loop, still a workflow
🔁 Evaluator–Optimizer
Two LLMs in disagreement: a generator drafts, an evaluator grades against a rubric, repeat until approved or capped.
Why two roles beat one
  • LLMs are usually better at judging text than at producing text that satisfies every constraint in one shot
  • The generator focuses on writing; the evaluator focuses on the rubric - neither does both jobs at once
  • Shines on tasks with a clear quality signal that's hard to encode in a single prompt
Why it's still a workflow, not an agent
  • The loop shape is fixed: always generate → evaluate. The model can't skip evaluation or call a new tool
  • The stopping criterion is yours: evaluator says approved, or you hit MAX_ROUNDS. The model never chooses to stop
When to use Translation quality, JSON validity, tone matching, code passing tests - anything where grading is easier than one-shot generation.
Brief
Generatordraft
Evaluatorapproved?
Output
not approved → feedback fed into next draft · capped at MAX_ROUNDS = 3
evaluator_optimizer.py - copywriter + critic
def generate(brief: str, feedback: str = "") -> str: prompt = f"Write a 2-sentence product description for: {brief}" if feedback: prompt += f"\n\nRevise to address this feedback:\n{feedback}" return llm(prompt, system="You are a copywriter. Avoid clichés.") def evaluate(text: str) -> dict: raw = llm( f"Evaluate this copy: {text!r}\n\n" "Criteria: no clichés; contains a concrete number; exactly 2 sentences.\n" 'Respond ONLY as JSON: {"approved": bool, "feedback": "..."}' ) return json.loads(raw.strip("`")) # ── The loop: fixed shape, hard cap - this is the part that's NOT agentic ── feedback, MAX_ROUNDS = "", 3 for i in range(1, MAX_ROUNDS + 1): draft = generate(brief, feedback) verdict = evaluate(draft) if verdict["approved"]: break feedback = verdict["feedback"] else: # for/else: only runs if we never broke out - i.e. hit MAX_ROUNDS unapproved print("⚠️ hit max rounds without approval - ship last draft or escalate")
Dynamic Control Flow

The Agent

Every workflow above had a shape you designed at coding time. An agent hands that decision to the model: "look at the situation, decide what to do next, do it, observe, repeat until done."

unbounded, capped with max_steps
🤖 The Agent Loop
An LLM in a loop, picking which tool to call - or deciding to stop - at every step.
What makes it an agent, not a workflow
  • You don't know upfront how many calls it'll make, in what order, or with what arguments
  • The model plans its own sequence - handles problems whose shape depends on the input
  • Powerful, but costs and failure modes become open-ended
The loop, in ~20 lines
  • Send messages + tools to the LLM
  • No tool_calls in the response → done, return the answer
  • Otherwise execute every tool call, append results, loop
  • Cap iterations with max_steps - a confused model can't burn money forever
When to use Tasks whose steps genuinely depend on the input in a way you can't predict at design time - open-ended research, multi-fact lookups with unknown order, anything where the "right" sequence changes per request.
User questionneeds 3 different tools
LLM ⇄ toolsloop, model-directed
Final answer
model decides: call another tool, or stop · capped at max_steps = 8
agent.py - the core loop every agent framework wraps
def run_agent(user_question: str, max_steps: int = 8) -> str: messages = [ {"role": "system", "content": "Use tools to gather facts and do arithmetic. " "Stop calling tools once you have enough information."}, {"role": "user", "content": user_question}, ] for step in range(1, max_steps + 1): resp = co.chat(model=MODEL, messages=messages, tools=tools) if not resp.message.tool_calls: return resp.message.content[0].text # model chose to stop messages.append({ "role": "assistant", "tool_calls": resp.message.tool_calls, "tool_plan": resp.message.tool_plan, # the model's "Thought" }) for tc in resp.message.tool_calls: # the model's "Action(s)" args = json.loads(tc.function.arguments) result = TOOL_REGISTRY[tc.function.name](**args) messages.append({ # the "Observation" "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result), }) return "⚠️ Hit max steps without producing a final answer."
⚠️
The max_steps cap is load-bearing infrastructure, not a debug-only knob. There is no other safety net - a confused agent will happily call lookup with random keys forever if you let it. Other guardrails real agents need: a dollar/token budget, tool-call argument validation, step-by-step logging, and human-in-the-loop for destructive actions.
Yao et al., 2022 · arXiv:2210.03629
📚 This pattern has a name: ReAct
Reasoning + Acting - the most common agent pattern in the wild, and the one you just built above.
The three ingredients, in a loop
  • Thought - the model reasons about what to do next
  • Action - the model picks a tool and its arguments
  • Observation - the tool's result is fed back to the model

…then it loops until the model decides it has enough information to answer. That's the whole pattern.

2022 paper vs. modern APIs
  • The original paper had the model emit Thought/Action/Observation as structured text that you parsed by hand
  • Modern function-calling - Cohere's tool_plan + tool_calls, OpenAI's reasoning + tool_calls, Anthropic's thinking + tool_use - bakes the same loop directly into the API
  • No more text parsing. Same shape underneath.
ReAct step Where it lives in the code What it looks like in a trace
Thought resp.message.tool_plan "I need to look up the price per ticket…"
Action resp.message.tool_calls lookup({'key': 'attendees'})
Observation {"role": "tool", "content": …} = {'value': 47, ...}
🎯
One line to take with you: ReAct is the pattern. Function calling is the implementation of that pattern in modern LLM APIs. Plan-and-Execute (plan all steps upfront, then run them), Reflexion (add a self-critique pass after each action), and multi-agent setups are all variations on the ReAct loop, not replacements for it.
Side by Side

The whole guide, on one page

Same underlying primitive - the augmented LLM call - composed three different ways.

Augmented LLM Workflow Agent
Calls per request
1 (+1 per tool round) Fixed N Unbounded (capped)
Control flow
Trivial Hardcoded by you Decided by the model
Predictability
Very high High Low
Cost ceiling
Tight Tight Open-ended
Debuggability
Easy Step-by-step Trace-required
Best for
One-shot tasks with a clear tool need Tasks you can decompose upfront Tasks whose shape depends on the input
A rule of thumb to take with you: start with the simplest thing that works. If a single augmented LLM call solves the task, ship it. If not, design a workflow. Only reach for an agent when the steps themselves depend on the input in a way you can't predict.
Decision Guide

Pick your pattern
in three questions

Answer each question in order. By question 3 you know exactly what to build.

Does the task fit in one round trip?
Yes, with maybe one tool call → Augmented LLM - describe, decide, execute, resolve. Ship it.
No, it needs several distinct steps → keep going to question 2
Can you predict the sequence of steps at design time?
Fixed sequence, output of one feeds the next → Prompt Chaining
First step depends on input category → Routing (classify, then dispatch)
Steps are independent of each other → Parallelization (sectioning or voting)
Quality is easy to grade but hard to one-shot → Evaluator–Optimizer (capped retry loop)
No - the right sequence changes per input and you can't predict it upfront → go to question 3
Are you ready to pay the agent tax?
Yes - build a ReAct agent: LLM in a loop, model picks tools, hard max_steps cap, plus a token/dollar budget and full step logging
Not yet - most "agentic" products in the wild are actually well-designed workflows. Re-check question 2 before reaching for the loop.
🎯
Set max_steps=1 on any agent and re-run it. Watch the answer degrade - that's proof the loop is doing real work, and a good way to sanity-check that you actually needed an agent and not a workflow.