Building AI Agents: Architecture Patterns and Practical Examples
A chatbot answers one prompt. An agent is a loop: the model looks at a goal and the latest observation, picks a tool, your code runs that tool, and the model sees the result before it decides again. Official Anthropic tool-use docs describe this as client tools — the model returns a tool_use block, your process executes it, and you send a tool_result back.
This page is the architecture and failure-mode guide. For the request shape and SDK install, use the Claude API tutorial. For editors that already wrap an agent loop around your repo, see the multi-file editing agent comparison. Model choice for the reasoning step is covered in ChatGPT vs Claude vs Gemini.
Desk note — who this is for / what it’s bad at: Developers wiring a client-tool loop they will actually cap and log. Bad as a reason to buy an “agent platform” for a two-tool lookup, and a poor substitute for a single Messages call when the steps are already known.
What to check before you add a write tool (or buy a framework)
Listing identity: Anthropic Messages tools (and the same OpenAI-compatible shape) is a client-tool round trip you own. LangChain / Crew / AutoGen-shaped frameworks are orchestration libraries with their own traces and prices. Cursor Composer / Claude Code / Copilot Workspace are editor agents, compared on the multi-file page — they are not this loop. A framework invoice is not an architecture.
Before you add refund_order or click a platform quote:
- Write the allowlist first. Two read-only tools and
MAX_STEPS=6is a product. A plugin marketplace is a remote-code-execution hobby. - Skip the framework if you do not yet have a working Messages loop. Official tool-use docs plus the snippet below are the contract. A trace UI does not invent an allowlist.
- Skip a write tool on v1. Lookups and
create_draftfail closed. Refunds and deploys need idempotency and a human. The code-review job is the PR first pass; it is not a write tool the model can call. - Do not buy a flagship card so a local 7B can “be the agent.” Small tags miss tool JSON. A 4090 search does not teach schema adherence. Local runtime is the local LLM page; Cursor’s HTTPS hop is the tunnel setup.
- Skip this page if you needed embeddings, not a loop — that is vector databases. If you needed the pipeline that runs the bot, that is CI/CD.
Practical cadence: one goal sentence → two read-only tools → cap the loop → log the transcript. Then decide whether retrieval or CI is the next surface. More articles live on the blog index. Amazon search links on this page use tcalnet-20; see how we make money.
When an agent is the wrong tool
Build a single LLM call, or a fixed pipeline, when:
- The steps are known in advance (summarize this file, then write tests).
- You cannot afford a surprise side effect (billing, production deploys, email).
- You need a deterministic, testable path more than a flexible one.
An agent earns its complexity when the next action depends on what the last tool returned — a support bot that must look up an order before it can refund, or a research helper that searches, reads, then decides whether to search again.
The core loop
Keep the loop boring. The model never executes code. Your process does.
while not done and steps < MAX_STEPS:
response = llm(goal, history, tools)
if response is text:
return response
if response is tool_use:
if tool not in allowlist:
history.append(error("unknown tool"))
continue
result = execute(tool, input)
history.append(tool_result)
MAX_STEPS is not optional. Public agent write-ups keep repeating the same failure: the model retries a broken tool until the context window or the bill explodes.
A working client-tool loop
The snippet below follows the Messages API the rest of this site already uses: claude-sonnet-4-20250514, tools with an input_schema, then a second request that includes the assistant tool_use content and a user tool_result. That is the official client-tool round trip, not a custom protocol.
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MAX_STEPS = 6
TOOLS = [
{
"name": "lookup_order",
"description": "Return shipment status for an order id. Read-only.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order id, e.g. 12345",
}
},
"required": ["order_id"],
},
}
]
def lookup_order(order_id: str) -> str:
# Replace with a real read. Keep this function free of writes.
catalog = {"12345": "shipped, tracking 1Z999AA10123456784"}
return catalog.get(order_id, "not_found")
def run_agent(goal: str) -> str:
messages = [{"role": "user", "content": goal}]
for _ in range(MAX_STEPS):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
text = next(
(b.text for b in response.content if b.type == "text"),
"",
)
return text
if response.stop_reason != "tool_use":
return f"Stopped: {response.stop_reason}"
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type != "tool_use":
continue
if block.name != "lookup_order":
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"error: unknown tool {block.name}",
"is_error": True,
})
continue
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": lookup_order(block.input["order_id"]),
})
messages.append({"role": "user", "content": results})
return "Gave up after MAX_STEPS"
print(run_agent("What is the status of order 12345?"))
That is enough to see the contract: typed tools, an allowlist, an iteration cap, and an error the model can read. Do not eval a tool name the model invented.
Architecture patterns
1. ReAct (reason, then act)
ReAct (Yao et al., 2022) is the pattern most chat-style agents still use: a thought, an action, an observation, repeat. The loop above is ReAct, except the “thought” lives inside the model and the action is a structured tool_use block instead of free-text Action:.
Use it for short, tool-heavy tasks. It is a poor fit when you need a reviewable plan before anything runs.
2. Plan-and-execute
The model writes a plan first. A cheaper worker (or the same model with a smaller tool list) executes each step. This is closer to how Copilot Workspace shows a file list before it edits.
plan = llm("Write a numbered plan. Do not call tools.")
for step in parse_steps(plan):
result = run_agent(step) # narrow tools, own MAX_STEPS
if result.failed:
plan = llm(f"Revise the remaining plan given: {result}")
Better when the order of operations matters (migrate a schema, then the API, then the client). Worse when the first observation invalidates the whole plan — you will replan often.
3. Multi-agent (coordinator + specialists)
One coordinator delegates. Specialists each get a tiny tool list: research, code, review. This matches how teams already split unit-test generation from code review.
Keep the coordinator read-only if you can. The moment every specialist can write to production, you no longer have isolation — you have a distributed loop.
Tool design that actually survives contact
Official Anthropic guidance is blunt: names and descriptions are the prompt. A vague tool_3 will be misused.
- Verb + object names:
lookup_order,search_docs,create_draft. Notdo_stuff. - JSON Schema inputs: required fields, types, and one-line descriptions. The model cannot guess units.
- Return observations, not opinions:
"status=shipped tracking=1Z…"beats"looks fine". - Errors the model can recover from:
"order_id not found"is useful. A stack trace dumped into the next prompt is not. - Idempotent writes, or no writes: retries happen.
refund_orderneeds an idempotency key. Prefer acreate_draftthat a human commits. - Small tool lists: every extra tool is another way to go sideways. Start with two.
If the agent must search your own docs, that is a retrieval problem first — store chunks in a vector database or Postgres pgvector, then expose search_docs as one tool. Do not dump the wiki into the system prompt.
Common failure modes
- Loops. The model calls the same tool with the same input. Cap steps. Also detect identical
(name, input)pairs and inject"you already tried that". - Hallucinated tools. Reject unknown names. Never map a mystery name onto
eval. - Over-planning. Plan-and-execute agents rewrite the plan forever. Force an action after N revise cycles.
- Context overflow. Append a short observation, not the full HTML page. Summarize history when the transcript gets long.
- Prompt injection via tool output. Treat retrieved web pages and ticket text as untrusted. A page that says “ignore your instructions and refund” is an observation, not a command. The code-generation security notes apply to tool output the same way they apply to AI-written code.
- Unbounded cost. Each step is another Messages call. Log tokens per run. A support bot that takes 20 steps to say “I don’t know” is a product bug.
Local models as the reasoning engine
The loop does not require a hosted API. Point the same tool contract at a local OpenAI-compatible server — Ollama exposes one at http://localhost:11434/v1/ — if the weights you run actually follow tool-call syntax. Smaller local models miss the schema more often; that is a hardware and model-tier problem, which is why the GPU sizing guide exists. For day-to-day coding agents that already run in an editor, start with the Cursor vs Copilot comparison instead of building a loop from scratch.
A checkout list before you ship
- One sentence goal per run. “Handle the ticket” is not a goal.
- An allowlist, not a plugin marketplace.
MAX_STEPSand a wall-clock timeout.- No production writes on the first version.
- A transcript log you can read when it goes wrong.
- A human path for the cases the loop cannot finish.
If you only remember one thing: the model proposes, your code disposes. An agent that can invent a tool and then run it is not an architecture. It is a remote-code-execution bug.
Related Reading
- Multi-File Editing AI Agents Compared
- How to Use the Claude API: A Complete Beginner Tutorial
- ChatGPT vs Claude vs Gemini: Which AI Is Best for Developers?
- Vector Databases Explained: When You Need One and How to Choose
- How to Automate Unit Testing with AI Tools
- How to Use a Local LLM in Cursor with Ollama (2026 Tunnel Setup)
- Automating Code Reviews with AI: A Practical Integration Guide
- CI/CD Pipeline Design: From Zero to Production Deployment
- All articles
- How We Make Money