Building Effective Agents
A guided walkthrough of Anthropic's official engineering guidance: what an agent actually is, the seven building-block patterns, and the rules for picking the simplest one that solves your problem.
Learning Objectives
- Distinguish workflows (LLMs on predefined code paths) from agents (LLMs that direct their own work) and explain why the distinction matters
- Recognize the foundational augmented LLM building block (retrieval + tools + memory) underneath every other pattern
- Identify the five workflow patterns — prompt chaining, routing, parallelization, orchestrator-workers, evaluator-optimizer — and pick the right one for a given task
- Decide when full agent autonomy is justified and when a simpler workflow is the better engineering choice
- Apply the agent-computer interface (ACI) principles when designing tools your agent will actually use well
Prerequisites: M00 (Course Overview), M05 (Function Calling), M12 (ReAct Loop) help, but this module is self-contained. | Level: Intermediate
Workflows vs. Agents — The Core Distinction
The first — and most important — thing the article does is define its terms. The word “agent” gets used loosely in the industry to mean anything from a chatbot to a fully autonomous robot. Anthropic narrows it down to one specific axis: who decides what step happens next?
Before this distinction: Imagine the difference between a vending machine and a personal shopper. The vending machine has a fixed flow — insert coin, press B4, item drops. Every step is predetermined by the machine's wiring. A personal shopper, by contrast, hears your goal (“I need a gift for my mom’s birthday”) and decides themselves which stores to visit, what to compare, when to call you for clarification, and when to stop.
The pain: Most teams call both systems “AI agents” the moment Claude is involved. That hides the actual engineering choice. A vending machine is cheap, predictable, and easy to debug, but rigid — new product lines mean new buttons. A personal shopper handles novelty for free, but you can’t predict their cost, latency, or exact path on any given errand.
The mapping: Anthropic’s vocabulary draws the line cleanly. Workflows are vending machines: LLMs and tools wired together by code paths you wrote. Agents are personal shoppers: LLMs that read the goal and dynamically pick their own next step using tools in a loop. Both are useful. They are not the same thing, and conflating them leads to over-engineered prototypes and under-engineered production systems.
The article splits the “agentic systems” umbrella into two precise categories:
Workflows are systems where LLMs and tools are orchestrated through predefined code pathsA workflow is code you wrote that calls one or more LLMs in a fixed sequence. The structure is static. The LLM fills in steps, but the path through those steps is hard-coded.. Your code calls the LLM once, takes its output, calls the LLM again, applies a rule, and so on. The structure of the program is fixed. You can read the source and predict every API call that will happen.
Agents are systems where LLMs dynamically direct their own processes and tool usageAn agent is an LLM running in a loop: it picks a tool, observes the result, picks the next tool, and decides on its own when to stop. Your code provides the tools and the loop, but not the order of operations.. You give the model a goal and a toolbox, and it decides which tool to call, in which order, and when to stop. The structure of the run is decided at runtime, by the model.
Why this split matters: every recommendation in the article hangs off it. Different patterns suit different points on this spectrum. Cost, debuggability, latency, and reliability all change as you move from workflow to agent.
One subtle but important note: the article doesn’t tell you to always pick one or the other. Most production systems combine both — a workflow at the top level that calls an agent for the genuinely open-ended sub-task, or an agent that internally invokes deterministic workflows as “tools.”
Picking the wrong side of this line is the most common cause of failed AI projects. Teams reach for “an agent” when their problem is actually a 3-step workflow — and end up with a system that costs 10× more, runs 5× slower, and is impossible to debug. Other teams build a rigid 50-step workflow when an agent could have handled the variety in 8 lines of code. Anthropic’s recommendation throughout the article is consistent: start simple, escalate complexity only when needed.
When (and When Not) to Use Agents
The article opens with a recommendation that’s easy to read and easy to skip past: find the simplest solution possible, and only increase complexity when needed. It then drives the point home with a sharper version: “This might mean not building agentic systems at all.” Coming from the company that sells the model, that’s a notable thing to print. Take it seriously.
The reasoning is grounded in tradeoffs. Agentic systems trade latency and cost for better task performance on open-ended problems. That trade is worth it for some workloads and a bad deal for others. The decision tree below summarizes the article’s heuristics:
| If your task is… | Then start with… | Why |
|---|---|---|
| A single LLM call solves it | Just an LLM call (no pattern at all) | Optimize the prompt with examples and retrieval before adding orchestration. |
| Decomposable into fixed sub-steps | Prompt chaining | Each step is small and validatable. You buy accuracy with a tiny bit of latency. |
| Distinct categories needing different handling | Routing | One specialized prompt per category beats one generalist prompt. |
| Multiple independent sub-tasks or perspectives | Parallelization | Speed (sectioning) or robustness via voting. |
| Sub-tasks unknowable in advance | Orchestrator-workers | Let the model decompose at runtime; structure the rest. |
| Output quality has clear evaluation criteria | Evaluator-optimizer | Iterative refinement when feedback is measurable. |
| Open-ended, # of steps unknowable, sandboxed | Full agent | Worth the cost when nothing simpler fits. |
“An agent is more powerful, so it’s always better.” — No. An agent solving a problem a routing workflow could solve is just an unreliable, expensive, slow version of the same answer. Power isn’t free.
“If we use a framework, we don’t need to think about this.” — The article warns that frameworks (Claude Agent SDK, LangChain, Vellum, Rivet) make it easy to start but often add abstractions that obscure prompts and make debugging painful. Even when you use one, understand the underlying pattern. Prefer LLM APIs directly until the abstraction earns its weight.
“Agents and chatbots are the same thing.” — A chatbot generates responses. An agent uses tools in a loop to take actions in the world. The difference is whether side effects happen mid-conversation.
A single LLM call: 1 API request, ~1-3 seconds, ~$0.003 (Sonnet). A 3-step prompt chain: 3 calls, ~3-9 seconds, ~$0.009. An agent that loops 10 times to finish a task: 10 calls, 30+ seconds, ~$0.030 — and unbounded if it doesn’t converge. The simplest pattern that works wins. Reach for the next pattern only when the simpler one demonstrably fails on your eval set.
The Augmented LLM — The Foundation Block
Every pattern in the article — chaining, routing, agents, all of them — is built out of one repeating piece: an LLM that has been augmented with three capabilities: retrieval, tools, and memory. Anthropic calls this the “augmented LLM,” and the framing matters: don’t think of the LLM as a chatbot, think of it as a reasoning engine with three plug-in slots.
Before augmentation: A bare LLM is like a brilliant new hire on day one. They’re smart and well-read but they don’t know your codebase, can’t open Jira, and forgot what you discussed yesterday. They produce confident-sounding answers from training data alone.
The pain: A bare LLM hallucinates because it has no way to look things up (no retrieval), no way to act on the world (no tools), and no continuity across sessions (no memory). Treating that bare LLM as your agent is the source of most of the failures the rest of the article exists to prevent.
The mapping: The augmented LLM is the same brilliant hire on day 30. They have access to the wiki (retrieval), can run scripts and update tickets (tools), and remember the past month of decisions (memory). That same brain becomes dramatically more useful once it’s plugged into the systems around it. Every pattern in this module is a way of arranging these augmented LLMs — how many you use, how they connect, who decides what.
The augmented LLM is an LLM call enriched with three capabilities the model can actively use:
- Retrieval: Pulling relevant context into the prompt at query time. Vector search, BM25, SQL queries, document loading — whatever brings the right knowledge into the context window. (Course modules M09 and M10 cover this.)
- Tools: Functions the model can invoke through the Messages API to take actions or fetch data the model cannot produce on its own — a database query, a web search, a file read, an API call. (Modules M05–M07.)
- Memory: Persisting state across turns and sessions so the model has continuity. Conversation history, scratchpads, vector-indexed notes. (Modules M08 and M11.)
The article calls out the Model Context ProtocolAn open protocol for connecting AI models to external tools and data sources in a standardized way. Lets one server expose tools to multiple AI clients (Claude, others) without bespoke integrations. Covered in M07. as one good way to wire third-party tools into the augmented LLM without bespoke per-tool code. The takeaway: tailor each capability to your specific use case — the right retrieval index, the right tool surface, the right memory representation — and ensure the LLM has a documented, easy-to-use interface to all three.
Most production agent failures aren’t reasoning failures — they’re augmentation failures. The retrieval pulled stale data. The tool returned an error the model didn’t know how to interpret. The memory truncated the wrong message. Every “Claude hallucinated” bug ticket is usually a “we never gave Claude what it needed” bug ticket. Get the augmented LLM right first, and the patterns above it become much easier to make work.
Pattern 1 — Prompt Chaining
Before chaining: Imagine asking a copywriter to “write a marketing email, translate it to French, and verify the translation” in one breath. They’ll do all three, but a fluent French speaker would still spot rough patches because the copywriter was juggling three jobs at once.
The pain: A single LLM call asked to do three things produces work that’s mediocre at all three. Worse, you can’t inspect the intermediate steps — if the French translation is wrong, you can’t tell if the English draft was the problem or the translation step itself.
The mapping: Prompt chaining is the assembly line. Step 1: write the email. Stop. Inspect. Step 2: translate. Stop. Inspect. Step 3: verify. Each call is focused, the output of one becomes the input of the next, and you can drop in programmatic gates between steps to validate or short-circuit. The article phrases it as: “trade a little latency for higher accuracy by making each LLM call easier.”
Prompt chaining decomposes a task into a sequence of LLM calls where each call processes the output of the previous one. Optional programmatic checks (the article calls them gates) sit between calls to validate that the previous step produced something usable before the next call fires.
When it’s the right tool: The task can be cleanly cut into fixed sub-steps, you know the order, and each sub-step is small enough that the model handles it reliably. Outline-then-write, draft-then-edit, extract-then-summarize, fetch-then-format are all canonical examples.
When it’s the wrong tool: The order of steps depends on the input, or some inputs need different sequences. That’s a routing or orchestrator-workers problem, not a chaining problem.
Code: Outline → Draft → Polish
This is the smallest interesting prompt chain. We split “write a short blog post on topic X” into three calls, with a length-check gate between the outline and the draft.
from anthropic import Anthropic, APIError
client = Anthropic()
MODEL = "claude-sonnet-4-6"
def call(system: str, user: str, max_tokens: int = 1024) -> str:
"""One Messages API call. Centralized so error handling stays in one place."""
try:
resp = client.messages.create(
model=MODEL,
max_tokens=max_tokens,
system=system,
messages=[{"role": "user", "content": user}],
)
return resp.content[0].text
except APIError as e:
# In production, log + retry with backoff. Here we surface the error
# so the chain halts visibly rather than producing silent garbage.
raise RuntimeError(f"LLM call failed: {e}") from e
def chain_blog_post(topic: str) -> str:
# Step 1: outline
outline = call(
system="You write tight, structured outlines.",
user=f"Write a 5-bullet outline for a 300-word blog post about: {topic}",
max_tokens=400,
)
# GATE: did we actually get a structured outline back?
if outline.count("\n") < 3:
raise ValueError(f"Outline too short to be useful:\n{outline}")
# Step 2: draft from the outline
draft = call(
system="You expand outlines into clear, engaging prose.",
user=f"Expand this outline into a 300-word draft. Use the outline order.\n\n{outline}",
max_tokens=900,
)
# Step 3: polish (tighten language, fix awkward phrasing)
polished = call(
system="You are an editor. Tighten prose, remove filler, fix awkward sentences.",
user=f"Polish this draft. Keep the meaning, improve flow.\n\n{draft}",
max_tokens=900,
)
return polished
if __name__ == "__main__":
print(chain_blog_post("why prompt chaining beats one giant prompt"))
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
async function call(system, user, maxTokens = 1024) {
try {
const resp = await client.messages.create({
model: MODEL,
max_tokens: maxTokens,
system,
messages: [{ role: "user", content: user }],
});
return resp.content[0].text;
} catch (err) {
// Halt visibly rather than continuing with bad input downstream.
throw new Error(`LLM call failed: ${err.message}`);
}
}
export async function chainBlogPost(topic) {
// Step 1: outline
const outline = await call(
"You write tight, structured outlines.",
`Write a 5-bullet outline for a 300-word blog post about: ${topic}`,
400,
);
// GATE: structural sanity check before we spend more tokens
if ((outline.match(/\n/g) || []).length < 3) {
throw new Error(`Outline too short to be useful:\n${outline}`);
}
// Step 2: draft
const draft = await call(
"You expand outlines into clear, engaging prose.",
`Expand this outline into a 300-word draft. Use the outline order.\n\n${outline}`,
900,
);
// Step 3: polish
const polished = await call(
"You are an editor. Tighten prose, remove filler, fix awkward sentences.",
`Polish this draft. Keep the meaning, improve flow.\n\n${draft}`,
900,
);
return polished;
}
const result = await chainBlogPost("why prompt chaining beats one giant prompt");
console.log(result);
You spent three API calls instead of one and added ~5 seconds of latency. In exchange, each step is small enough that the model handles it well, the gate kills bad runs early, and when the output is wrong you can pinpoint which step went wrong by inspecting intermediate text. That’s the prompt-chaining trade in one paragraph.
Pattern 2 — Routing
Before routing: A general-practitioner doctor handles every complaint — broken leg, weird rash, chest pain, sore throat. They’re competent at all of it but excellent at none, and they’re always looking up reference material on the way to the next room.
The pain: One LLM prompt that has to handle “billing question, technical bug report, refund request, and account deletion” ends up bloated — the prompt grows to cover every case, and the model’s attention is split. Performance is mediocre across the board.
The mapping: Routing is triage. A short, fast classifier reads the input and dispatches it to the right specialist prompt — one for billing, one for bugs, one for refunds. Each specialist prompt is short and sharply focused. The model handles its narrow case excellently, and you can swap in a stronger model only for the harder categories without paying that cost on every request.
Routing classifies the input and directs it to a specialized follow-up task. The classification can be done by an LLM call (when categories are fuzzy) or by a deterministic rule (when categories are obvious). Each downstream branch has its own prompt, possibly its own model, possibly its own tool set.
When it’s the right tool: Inputs fall into distinct categories that benefit from different handling, and a generalist prompt would underserve each category. Customer support intents, document types, query difficulties (route easy ones to Haiku, hard ones to Opus), language detection.
Watch for: Misclassification cascades. A bad classifier sends queries to the wrong specialist, who tries to answer anyway. Add a fallback branch for low-confidence classifications, and log misroutes for tuning.
Code: Customer-Support Router
A classifier picks one of three lanes — billing, technical, general — and dispatches to a specialized prompt with its own tone and constraints.
from anthropic import Anthropic, APIError
client = Anthropic()
MODEL_FAST = "claude-haiku-4-5-20251001" # cheap classifier
MODEL_SMART = "claude-sonnet-4-6" # main answerer
SPECIALISTS = {
"billing": (
"You are a billing specialist. Be precise about amounts, dates, and "
"policies. Never invent fees. If unsure, say so and escalate."
),
"technical": (
"You are a technical support engineer. Ask for logs/versions when needed. "
"Suggest one fix at a time. Never guess at internals you don't know."
),
"general": (
"You are a friendly customer support agent. Answer in 2-3 sentences. "
"Defer to specialists when the question becomes billing or technical."
),
}
def classify(message: str) -> str:
"""Cheap, fast classifier. Returns one of the keys in SPECIALISTS."""
resp = client.messages.create(
model=MODEL_FAST,
max_tokens=10,
system=(
"Classify the user message into exactly one label: "
"'billing', 'technical', or 'general'. "
"Reply with only the label, no punctuation."
),
messages=[{"role": "user", "content": message}],
)
label = resp.content[0].text.strip().lower()
return label if label in SPECIALISTS else "general" # safe fallback
def respond(message: str) -> str:
lane = classify(message)
try:
resp = client.messages.create(
model=MODEL_SMART,
max_tokens=500,
system=SPECIALISTS[lane],
messages=[{"role": "user", "content": message}],
)
return f"[lane={lane}] {resp.content[0].text}"
except APIError as e:
raise RuntimeError(f"Specialist call failed for lane={lane}: {e}") from e
if __name__ == "__main__":
print(respond("My credit card was charged twice for last month's plan."))
print(respond("Login button does nothing on Firefox 124."))
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const MODEL_FAST = "claude-haiku-4-5-20251001";
const MODEL_SMART = "claude-sonnet-4-6";
const SPECIALISTS = {
billing:
"You are a billing specialist. Be precise about amounts, dates, and policies. Never invent fees. If unsure, say so and escalate.",
technical:
"You are a technical support engineer. Ask for logs/versions when needed. Suggest one fix at a time. Never guess at internals you don't know.",
general:
"You are a friendly customer support agent. Answer in 2-3 sentences. Defer to specialists when the question becomes billing or technical.",
};
async function classify(message) {
const resp = await client.messages.create({
model: MODEL_FAST,
max_tokens: 10,
system:
"Classify the user message into exactly one label: 'billing', 'technical', or 'general'. Reply with only the label, no punctuation.",
messages: [{ role: "user", content: message }],
});
const label = resp.content[0].text.trim().toLowerCase();
return SPECIALISTS[label] ? label : "general"; // safe fallback
}
export async function respond(message) {
const lane = await classify(message);
try {
const resp = await client.messages.create({
model: MODEL_SMART,
max_tokens: 500,
system: SPECIALISTS[lane],
messages: [{ role: "user", content: message }],
});
return `[lane=${lane}] ${resp.content[0].text}`;
} catch (err) {
throw new Error(`Specialist call failed for lane=${lane}: ${err.message}`);
}
}
console.log(await respond("My credit card was charged twice for last month's plan."));
console.log(await respond("Login button does nothing on Firefox 124."));
Notice the cost optimization: the classifier uses Haiku (cheap, fast) and the specialist uses Sonnet. You only pay for the smarter model on the work that needs it.
Pattern 3 — Parallelization
Before parallelization: A single proofreader reads a manuscript end-to-end checking grammar, fact accuracy, and tone all at once. By page 80 they’re tired, mixing up the passes, and missing things.
The pain: Doing several independent jobs serially in one LLM call wastes wall-clock time and dilutes attention. And for safety-critical checks, a single pass gives you a single point of failure — if the model misses a problem, no one catches it.
The mapping: Parallelization splits the work two different ways. Sectioning: hand different chapters to different proofreaders so they finish in parallel — useful when sub-tasks are independent. Voting: hand the same chapter to three proofreaders and combine their findings — useful when one careful pass isn’t reliable enough and you want consensus or coverage.
Parallelization runs multiple LLM calls simultaneously and aggregates their outputs. The article calls out two flavors:
Sectioning: The task splits into independent sub-tasks that can be done at the same time. Translate a long document by sending each chapter to a different call. Validate three different aspects of one input (toxicity, on-topic, factual) in parallel and combine the verdicts.
Voting: The same task is run multiple times with varied prompts or temperature, and the results are combined — majority vote, union of findings, or LLM-aggregated synthesis. Used when one shot isn’t robust enough: code-vulnerability scans (catch as many issues as possible), output safety classifiers (flag if any pass flags it), or content evaluation where multiple perspectives improve quality.
Watch for: Cost. N calls cost N× one call. Use parallelization when speed or robustness justifies that cost — not as a default.
Code: Voting-Style Safety Triple-Check
A user-generated post is checked for three risks in parallel: toxicity, off-topic, and contains personal info. Any flag fails the post.
import asyncio
from anthropic import AsyncAnthropic, APIError
client = AsyncAnthropic()
MODEL = "claude-haiku-4-5-20251001"
CHECKS = {
"toxicity": "Is this post toxic, hateful, or harassing? Reply only YES or NO.",
"offtopic": "The forum is about home gardening. Is this post off-topic? Reply only YES or NO.",
"pii": "Does this post contain personal info (full names, phone, email, address)? Reply only YES or NO.",
}
async def check_one(name: str, prompt: str, post: str) -> tuple[str, bool]:
try:
resp = await client.messages.create(
model=MODEL,
max_tokens=4,
system=prompt,
messages=[{"role": "user", "content": post}],
)
verdict = resp.content[0].text.strip().upper().startswith("YES")
return name, verdict
except APIError:
# Fail closed: treat unreachable check as a flag, not as a pass.
return name, True
async def screen_post(post: str) -> dict:
results = await asyncio.gather(
*(check_one(n, p, post) for n, p in CHECKS.items())
)
flags = {name: flagged for name, flagged in results}
return {"approved": not any(flags.values()), "flags": flags}
if __name__ == "__main__":
sample = "Loving my new tomato variety this year, way better than last season's!"
print(asyncio.run(screen_post(sample)))
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const MODEL = "claude-haiku-4-5-20251001";
const CHECKS = {
toxicity: "Is this post toxic, hateful, or harassing? Reply only YES or NO.",
offtopic: "The forum is about home gardening. Is this post off-topic? Reply only YES or NO.",
pii: "Does this post contain personal info (full names, phone, email, address)? Reply only YES or NO.",
};
async function checkOne(name, prompt, post) {
try {
const resp = await client.messages.create({
model: MODEL,
max_tokens: 4,
system: prompt,
messages: [{ role: "user", content: post }],
});
const flagged = resp.content[0].text.trim().toUpperCase().startsWith("YES");
return [name, flagged];
} catch {
// Fail closed: a missing check counts as a flag, not a pass.
return [name, true];
}
}
export async function screenPost(post) {
const entries = await Promise.all(
Object.entries(CHECKS).map(([n, p]) => checkOne(n, p, post))
);
const flags = Object.fromEntries(entries);
return { approved: !Object.values(flags).some(Boolean), flags };
}
const sample = "Loving my new tomato variety this year, way better than last season's!";
console.log(await screenPost(sample));
Three calls fire concurrently, so the wall-clock time is roughly one call — about 3× faster than running them sequentially. The fail-closed default in the error handler is deliberate: if a safety check can’t run, treat the post as flagged rather than silently approve it.
Pattern 4 — Orchestrator-Workers
Before orchestrator-workers: A senior editor receives a vague brief: “produce a comprehensive market briefing on emerging fintech in Southeast Asia.” They can’t pre-define the chapters — they don’t know yet whether the briefing needs three country deep-dives or seven, or whether regulation deserves its own section.
The pain: Routing won’t work because the categories aren’t known up front. Prompt chaining won’t work because the steps depend on what we find. The decomposition itself is part of the problem.
The mapping: The senior editor reads the brief, decides on the chapters, hands each chapter to a specialist writer, then weaves the results into a single briefing. That’s orchestrator-workers: a central LLM dynamically decomposes the work, dispatches sub-tasks to worker LLMs, and synthesizes their outputs. The shape of the run is decided at runtime, not at code time.
Orchestrator-workers uses one LLM as a dispatcher: it reads the input, generates a list of sub-tasks dynamically, calls worker LLMs (in parallel where possible) to solve each one, and synthesizes a final answer from the results.
The key difference from parallelization is that the sub-tasks aren’t pre-defined — the orchestrator decides them at runtime based on the input. The key difference from a full agent is that the structure is still bounded: orchestrator decomposes, workers solve, orchestrator synthesizes, done. There’s no open-ended loop.
When it’s the right tool: Complex tasks where you can’t predict the sub-tasks in advance — multi-file code edits, research summaries from many sources, analysis pipelines that depend on what the data looks like.
This pattern shows up in this course as the supervisor-worker architecture in M14: Multi-Agent Systems, and is the foundation of the Anthropic Agent SDK’s subagent feature.
The exam favors the orchestrator-workers (a.k.a. coordinator + subagents) pattern over flat or peer-to-peer multi-agent designs. Memorize the four properties it gives you: single coordination point, context isolation between workers, structured result aggregation, auditable decision flow.
Pattern 5 — Evaluator-Optimizer
Before evaluator-optimizer: A novelist sends each chapter to a publisher and gets a single “looks good” or “please revise.” Without specific feedback, every revision is a guess at what the publisher actually wants.
The pain: Many tasks aren’t one-shot. Translation, code review, technical writing, summarization — these benefit from a critique pass and a revision. Asking the original LLM to “make it better” just produces a different version with the same blind spots.
The mapping: Evaluator-optimizer uses two LLM roles in a loop. A generator produces a candidate output. An evaluator — a separate prompt with a different focus — critiques it against explicit criteria. The generator revises using the critique. Loop until the evaluator approves or a max-iterations cap kicks in.
Evaluator-optimizer is a generator + critic loop. Generator produces, evaluator scores against criteria, generator revises with the critique. The loop terminates when the evaluator’s score crosses a threshold or a max-iterations cap is reached.
When it’s the right tool: The article gives a specific test — use this when (1) you can articulate clear evaluation criteria the model can apply, and (2) iterative refinement provides measurably better outputs. Translation quality, literary editing, code that must pass tests, documents that must satisfy a checklist.
When it’s the wrong tool: If “good” is fuzzy and subjective, the evaluator will produce noisy critiques that the generator chases unproductively, burning calls without converging. Use only when criteria are concrete.
The cousin pattern in this course is reflexion / critique loops in M17: Output Guardrails and the eval-driven development covered in M18.
Code: Code Reviewer + Author Loop
Generator writes a function. Evaluator checks it for correctness, edge cases, and naming. Up to three revision rounds.
from anthropic import Anthropic, APIError
client = Anthropic()
MODEL = "claude-sonnet-4-6"
MAX_ROUNDS = 3
def llm(system: str, user: str, max_tokens: int = 800) -> str:
try:
r = client.messages.create(
model=MODEL, max_tokens=max_tokens, system=system,
messages=[{"role": "user", "content": user}],
)
return r.content[0].text
except APIError as e:
raise RuntimeError(f"LLM error: {e}") from e
def evaluate(code: str, spec: str) -> tuple[bool, str]:
"""Evaluator returns (approved, critique). 'APPROVED' on the first line means done."""
review = llm(
system=(
"You are a strict code reviewer. Check correctness, edge cases, "
"naming, and style against the spec. Reply with the literal first "
"line 'APPROVED' if and only if the code fully meets the spec; "
"otherwise list specific, actionable issues as a bulleted list."
),
user=f"SPEC:\n{spec}\n\nCODE:\n{code}",
max_tokens=400,
)
return review.strip().upper().startswith("APPROVED"), review
def write_with_review(spec: str) -> str:
code = llm("You write clean, well-named Python.", spec)
for round_num in range(1, MAX_ROUNDS + 1):
approved, critique = evaluate(code, spec)
if approved:
return code
# Revise using the specific critique
code = llm(
system="You are revising your own code to address reviewer feedback.",
user=f"SPEC:\n{spec}\n\nYOUR CODE:\n{code}\n\nREVIEWER FEEDBACK:\n{critique}\n\nRevise the code.",
)
# Hit the cap; return best effort with a warning
return f"# WARNING: not approved after {MAX_ROUNDS} rounds\n{code}"
if __name__ == "__main__":
spec = "Write a Python function `median(nums)` that returns the median of a non-empty list of numbers. Handle even and odd lengths."
print(write_with_review(spec))
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
const MAX_ROUNDS = 3;
async function llm(system, user, maxTokens = 800) {
try {
const r = await client.messages.create({
model: MODEL, max_tokens: maxTokens, system,
messages: [{ role: "user", content: user }],
});
return r.content[0].text;
} catch (err) {
throw new Error(`LLM error: ${err.message}`);
}
}
async function evaluate(code, spec) {
const review = await llm(
"You are a strict code reviewer. Check correctness, edge cases, naming, and style against the spec. Reply with the literal first line 'APPROVED' if and only if the code fully meets the spec; otherwise list specific, actionable issues as a bulleted list.",
`SPEC:\n${spec}\n\nCODE:\n${code}`,
400,
);
return [review.trim().toUpperCase().startsWith("APPROVED"), review];
}
export async function writeWithReview(spec) {
let code = await llm("You write clean, well-named JavaScript.", spec);
for (let round = 1; round <= MAX_ROUNDS; round++) {
const [approved, critique] = await evaluate(code, spec);
if (approved) return code;
code = await llm(
"You are revising your own code to address reviewer feedback.",
`SPEC:\n${spec}\n\nYOUR CODE:\n${code}\n\nREVIEWER FEEDBACK:\n${critique}\n\nRevise the code.`,
);
}
return `// WARNING: not approved after ${MAX_ROUNDS} rounds\n${code}`;
}
const spec = "Write a JavaScript function median(nums) that returns the median of a non-empty array of numbers. Handle even and odd lengths.";
console.log(await writeWithReview(spec));
“The evaluator should be the same prompt as the generator.” — No. They need different framings. The generator’s prompt says “produce X.” The evaluator’s prompt says “find what’s wrong with X according to these specific criteria.” Reusing the same prompt collapses the loop — the “evaluator” just produces another generation, not a critique.
Pattern 6 — Agents
Before full agents: Every workflow above has a code-defined shape. The path through the system is fixed by the code you wrote. Steps may differ in content, but the structure of the run — how many calls, in what order, with what tools — is decided at write-time.
The pain: Some tasks resist that. “Reproduce this bug, find the root cause, write a fix, run the tests, open a PR.” You can’t pre-decide how many test-runs, how many file edits, or what investigation order will be needed — that depends entirely on what the bug turns out to be.
The mapping: A full agent is the LLM running its own loop. You hand it a goal and a toolbox, and it picks tools, observes results, and decides on its own when the work is done. Path length is unbounded. Cost and latency are unbounded. In exchange, the agent can handle problems whose shape couldn’t have been pre-determined.
An agent is an LLM in a tool-calling loop: receive task → pick tool → observe result → decide next step (more tools, or stop) → loop. It’s the architecture M12 (ReAct) and M13 (Planning & Decomposition) teach in detail.
When it’s the right tool: Open-ended problems where the number of steps is unknowable in advance, AND you can run in a trusted/sandboxed environment, AND you have evals to catch regressions. The article’s canonical examples: coding agents that solve real GitHub issues by iterating against tests, and computer-use agents that navigate UIs.
The cost is real. The article warns about three things specifically: higher costs (loops mean N× tokens), compounding errors (one wrong step contaminates everything that follows), and need for guardrails (sandboxed execution, max-step limits, human-in-the-loop checkpoints for high-stakes actions).
Code: Minimal Agent Loop
This is the smallest correct agent loop — the article’s recommendation distilled to its essentials. The model picks tools, the harness executes them, the loop continues until the model produces a non-tool response. Note the step cap: a real-world agent must always have one.
from anthropic import Anthropic, APIError
client = Anthropic()
MODEL = "claude-sonnet-4-6"
MAX_STEPS = 10 # hard cap so a confused agent can't loop forever
# 1. Define tools the model can call.
TOOLS = [
{
"name": "get_weather",
"description": "Get current weather for a city. Returns short text.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
{
"name": "calculator",
"description": "Evaluate a basic arithmetic expression like '12 * (3 + 4)'.",
"input_schema": {
"type": "object",
"properties": {"expr": {"type": "string"}},
"required": ["expr"],
},
},
]
def execute_tool(name: str, args: dict) -> str:
"""The harness executes the tool and returns a string result."""
if name == "get_weather":
# Stub. In real life: call your weather API.
return f"{args['city']}: 18C, partly cloudy."
if name == "calculator":
# Tightly-scoped eval: numbers and operators only.
allowed = set("0123456789+-*/(). ")
if not set(args["expr"]) <= allowed:
return "ERROR: disallowed characters"
try:
return str(eval(args["expr"], {"__builtins__": {}}, {})) # nosec
except Exception as e:
return f"ERROR: {e}"
return f"ERROR: unknown tool {name}"
def run_agent(goal: str) -> str:
messages = [{"role": "user", "content": goal}]
for step in range(MAX_STEPS):
try:
resp = client.messages.create(
model=MODEL, max_tokens=1024, tools=TOOLS, messages=messages,
)
except APIError as e:
return f"ABORTED: {e}"
# Stop condition: model is done calling tools.
if resp.stop_reason != "tool_use":
return next(
(b.text for b in resp.content if hasattr(b, "text")),
"(no text response)",
)
# Otherwise: append the model's tool calls and run them.
messages.append({"role": "assistant", "content": resp.content})
tool_results = []
for block in resp.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
return f"ABORTED: hit MAX_STEPS={MAX_STEPS} without a final answer"
if __name__ == "__main__":
print(run_agent("What's the weather in Tokyo, and what's 17% of the temperature?"))
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-6";
const MAX_STEPS = 10;
const TOOLS = [
{
name: "get_weather",
description: "Get current weather for a city. Returns short text.",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
{
name: "calculator",
description: "Evaluate a basic arithmetic expression like '12 * (3 + 4)'.",
input_schema: {
type: "object",
properties: { expr: { type: "string" } },
required: ["expr"],
},
},
];
function executeTool(name, args) {
if (name === "get_weather") {
return `${args.city}: 18C, partly cloudy.`;
}
if (name === "calculator") {
if (!/^[0-9+\-*/(). ]+$/.test(args.expr)) return "ERROR: disallowed characters";
try { return String(Function(`"use strict"; return (${args.expr});`)()); }
catch (e) { return `ERROR: ${e.message}`; }
}
return `ERROR: unknown tool ${name}`;
}
export async function runAgent(goal) {
const messages = [{ role: "user", content: goal }];
for (let step = 0; step < MAX_STEPS; step++) {
let resp;
try {
resp = await client.messages.create({
model: MODEL, max_tokens: 1024, tools: TOOLS, messages,
});
} catch (e) {
return `ABORTED: ${e.message}`;
}
if (resp.stop_reason !== "tool_use") {
const text = resp.content.find((b) => b.type === "text");
return text ? text.text : "(no text response)";
}
messages.push({ role: "assistant", content: resp.content });
const toolResults = [];
for (const block of resp.content) {
if (block.type === "tool_use") {
const result = executeTool(block.name, block.input);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: result,
});
}
}
messages.push({ role: "user", content: toolResults });
}
return `ABORTED: hit MAX_STEPS=${MAX_STEPS} without a final answer`;
}
console.log(await runAgent("What's the weather in Tokyo, and what's 17% of the temperature?"));
The article is explicit: agents need extensive testing in sandboxed environments and appropriate guardrails before being trusted with autonomy. In the example above, the calculator eval is locked to numeric characters — without that, you have a remote-code-execution hole. Production agents add: max-steps caps, max-cost caps, an allowlist of tools per task, human-in-the-loop confirmation for irreversible actions (database writes, emails, deploys), and pervasive logging. (See M16 & M17.)
Combining Patterns — Real Systems Mix Them
The article makes one final point that’s easy to miss because it’s phrased gently: “These building blocks aren’t prescriptive. They’re common patterns that developers can shape and combine to fit different use cases.” In other words: don’t pick exactly one. Real production systems compose them.
Three useful compositions:
- Routing → (specialized chain or agent): A top-level router classifies the request, then dispatches to either a deterministic chain (cheap, predictable cases) or a full agent (rare, open-ended cases). Most customer-support stacks look like this.
- Agent with workflow tools: Inside an agent, individual “tools” can themselves be deterministic workflows. The agent calls a tool that internally does prompt-chaining to summarize a document — the agent doesn’t see that complexity.
- Orchestrator-workers with evaluator-optimizer per worker: A research orchestrator spawns subagents, and each subagent runs a generator-critic loop on its draft before returning. M14 + M17 patterns combined.
The mistake teams make is treating the patterns as competing options — “are we doing routing or are we doing agents?” — when in reality a real system is usually a tree: a workflow at the top, agents at the leaves where the open-ended work lives, and inside each agent, more workflows. The patterns are a vocabulary for talking about those tree levels, not a single choice for the whole system.
Tool Engineering — The Agent-Computer Interface (ACI)
Late in the article, Anthropic devotes a section to a topic that often gets shortchanged: how you design the tools the agent uses. They coin the term Agent-Computer Interface (ACI) — the agent’s analog of HCI. The takeaway: invest as much effort in the ACI as you’d invest in a UI for a human.
The Agent-Computer Interface (ACI)The set of tool definitions, names, parameter schemas, descriptions, and expected return formats that an agent works with. Good ACI design dramatically improves agent reliability and is often the highest-leverage intervention. is the surface the model sees: tool names, descriptions, parameter schemas, return formats, error messages. The article reports that on internal coding-agent work, tool-design effort “often required more effort than overall prompt development.” Translation: better tools beat better prompts, and most teams under-invest here.
The ACI checklist (from the article and surrounding Anthropic guidance)
- Give the model enough tokens to think before acting. Don’t cap output so tightly that the agent skips reasoning. Allow space for a brief plan before each tool call.
- Keep formats close to what the model sees naturally on the open web. JSON, markdown, plain English — not bespoke binary formats or custom DSLs. Models are dramatically better at common formats they’ve seen during training.
- Eliminate formatting overhead. Don’t require the model to count whitespace, escape exotic characters, or maintain rigid line numbers. Every char of overhead is one more thing to get wrong.
- Test extensively with example inputs. Run the tools against realistic inputs and read the model’s tool calls. You’ll spot ambiguities the schema didn’t catch.
- Apply poka-yokeJapanese for “mistake-proofing.” A design principle from manufacturing: shape the tool so common mistakes are physically/structurally impossible to make. design. Shape the tool so common errors are impossible. The article’s classic example: their coding agent kept generating relative file paths that broke when the working directory shifted, so they redesigned the tool to require absolute paths only — the bug class disappeared.
- Document the tool the way you’d document it for a junior engineer. What does it do, what are the inputs, what does it return, what errors can it produce, what should the caller do about each error? The model reads the description and acts on it.
If your agent is misbehaving, the first instinct is to edit the system prompt. The article’s data points elsewhere: more often, the fix is in the tool. Rename a confusingly-named parameter. Make a return format unambiguous. Add a missing field to an error response. The same model on the same prompt with better tools performs dramatically better. ACI is the highest-leverage intervention most teams ignore.
Real Applications — Where These Patterns Show Up
The article’s appendix grounds the abstract patterns in two well-developed real applications. Both are worth understanding because they show what “the right pattern” looks like once a real production constraint is involved.
Customer Support
Customer support combines a chatbot interface with tool integration to access customer data, history, and account systems — and to take actions like issuing refunds or updating tickets. The article identifies it as a strong fit because:
- The interaction is naturally conversational (LLMs are good at this).
- Success is measurable: did the issue get resolved? Was the customer satisfied?
- Pricing per resolution maps cleanly to value — one of the rare AI use cases where outcome-based pricing actually works.
The architecture is typically routing-at-the-top (intent classification) plus a constrained agent in each lane that has access to the tools relevant to that intent — refund tools for the billing lane, account tools for the account lane, and so on. Pure free-form agents are usually overkill.
Coding Agents
Coding agents (the SWE-bench category) are the article’s flagship example of when full agent autonomy is justified. The reasoning:
- Solutions are verifiable through automated tests — the agent doesn’t need a human in the loop on every step because the test suite is the ground truth.
- The agent can iterate using test results as feedback: try a fix, run tests, see what failed, try a refined fix.
- The problem space is genuinely open-ended — the agent might need to read 3 files or 30, edit 1 line or rewrite a module — impossible to pre-script.
Even here, the article tempers the autonomy: “human review remains crucial for ensuring solutions align with broader system requirements.” The PR review step is the human guardrail. The agent works inside a sandbox; humans gate what gets merged.
Both examples have one thing in common: a clear, automatic feedback signal. Customer support has resolution outcomes. Coding has test results. When that signal exists, agents become viable. When it doesn’t — when “good” can only be judged by a human reading the output — you’re probably looking at a workflow problem, not an agent problem.
Knowledge Check
1. Anthropic’s definition splits “agentic systems” into two specific categories. What is the actual difference?
2. Which task is best suited to routing rather than a single generalist prompt or a full agent?
3. What is the key difference between orchestrator-workers and plain parallelization?
4. The article warns that agents (full autonomy) introduce three specific risks. Which list is correct?
5. Which statement about the Agent-Computer Interface (ACI) reflects the article’s recommendation?
Your Score
Module Summary
Key Concepts
- Workflows vs. agents: The line is who decides the path. Workflows: predefined code paths you wrote. Agents: LLM dynamically directs itself.
- Start simple: A bare LLM call beats a chain. A chain beats an agent. Reach for the next pattern only when the simpler one provably fails.
- Augmented LLM: The reusable building block under everything. Retrieval + tools + memory, with each capability tailored to the use case.
- Five workflow patterns: Prompt chaining (sequential), routing (classify and dispatch), parallelization (concurrent for speed or voting), orchestrator-workers (runtime decomposition), evaluator-optimizer (generator + critic loop).
- Full agents: Worth the cost only for open-ended problems where steps are unknowable in advance and you can sandbox + add guardrails. Customer support and coding agents are the canonical fits.
- ACI: Tool design is the highest-leverage intervention most teams ignore. Better tools beat better prompts.
- Combining patterns: Real systems are trees — a workflow at the top, agents at the leaves, more workflows inside the agents.
How This Maps to the Rest of the Course
| Article Concept | Course Module(s) |
|---|---|
| Augmented LLM — retrieval | M09, M10 |
| Augmented LLM — tools / MCP | M05, M06, M07 |
| Augmented LLM — memory | M08, M11 |
| Prompt chaining | M03, M03B |
| Routing & orchestrator-workers | M14, M15B |
| Evaluator-optimizer | M17, M18 |
| Agents | M12, M13 |
| Sandboxing & guardrails | M15, M16, M17 |
| ACI / tool engineering | M05, M07, M26 |
Where to Go Next
Pick the pattern that matches your current project and dive into the corresponding course module. If you’re unsure where you are: most teams overestimate how much agent autonomy they need. Try the simpler pattern first — you can always escalate when your eval set proves it’s necessary.