AI agent architecture¶
Advanced · 20 min read · diagrams + runnable code
An agent is an LLM that can take actions: it decides which tool to use, your code runs the tool, the result goes back to the model, and it repeats until the task is done. This page explains how that loop works, builds one you can run, and shows how to make agents safe and reliable.
Before you read
Know how to call an LLM from Python and what functions and dictionaries are. Reading Production RAG architecture first helps, but isn't required.
What you'll learn
- How agents differ from chatbots and fixed workflows
- The agent loop — step by step, and in ~40 lines of Python
- The six components of an agent system and how to design each one
- When to use one agent, a workflow, or several agents
- The failure modes to plan for, and how to test agents
1. Chatbot, workflow or agent?¶
| Chatbot | Workflow | Agent | |
|---|---|---|---|
| Does | Answers in text | Runs steps you defined, in a fixed order | Chooses its own steps and tools |
| Example | "What's our refund policy?" | Email → classify → summarise → file a ticket | "Find a free slot with Acme next week and send an invite" |
| Predictability | High | High | Lower — needs limits and checks |
| Use when | You only need answers | You can draw the steps up front | The path depends on what it finds along the way |
Rule of thumb: use the simplest one that works. Many "agent" problems are really workflows.
2. The idea in plain English¶
An agent is like a new employee with a toolbox and a manager:
- The employee (the LLM) is smart but can only say what to do — "look up the calendar", "send the email".
- The toolbox (your tools) holds the actions they're allowed to request, each with clear instructions.
- The manager (your orchestrator code) actually performs each action, checks it's allowed, reports the result back, and stops the employee if they go round in circles or overspend.
The key design rule: the model chooses, your code executes. The model never runs anything itself.
3. The agent loop¶
sequenceDiagram
autonumber
participant U as User
participant O as Orchestrator (your code)
participant M as LLM
participant T as Tools
U->>O: Goal ("Book a demo with Acme next week")
loop until done or step limit
O->>M: Goal + history + tool descriptions
M-->>O: Tool call: calendar.find_slots(week="next")
O->>O: Validate arguments · check permission
O->>T: Execute
T-->>O: Result
end
M-->>O: Final answer
O-->>U: Answer + list of actions taken
Walkthrough: one task, step by step¶
Goal: "Book a 30-minute demo with Acme next week and email them the invite."
| Step | The LLM decides | Your code does | Result sent back to the LLM |
|---|---|---|---|
| 1 | Call find_slots(week="next", minutes=30) |
Checks the user may read the calendar → runs it | ["Tue 11:00", "Thu 15:00"] |
| 2 | Call create_event(time="Tue 11:00", with="acme") |
Needs approval (it changes the calendar) → asks the user → approved → runs it | event_id=812 |
| 3 | Call send_email(to="acme", event_id=812) |
Checks the recipient is an existing contact → runs it | sent |
| 4 | Final answer: "Booked Tue 11:00 and emailed Acme." | Returns the answer plus the list of actions taken | — |
This is the ReAct pattern (reason → act → observe). Modern models implement it with tool calling: you describe each tool, and the model returns a structured request instead of free text.
4. Build the loop yourself¶
The whole idea fits in about 40 lines. Here the model is a stand-in function, so it runs with no API key — swap in a real LLM later (section 4.1).
import json
# ── 1. Tools: ordinary Python functions the agent is allowed to use ─────────
def get_weather(city: str) -> str:
"""Pretend weather lookup — a real tool would call a weather API."""
fake_data = {"bengaluru": "24°C, light rain", "delhi": "33°C, sunny"}
return fake_data.get(city.lower(), "unknown city")
def convert_currency(amount: float, to: str) -> str:
"""Convert Indian rupees to another currency (rates are illustrative)."""
rates = {"usd": 0.012, "eur": 0.011}
if to.lower() not in rates:
# A clear message lets the model correct itself on the next step.
raise ValueError(f"unsupported currency {to!r} — use USD or EUR")
return f"{amount * rates[to.lower()]:.2f} {to.upper()}"
# ── 2. Tool registry: what the model is told about each tool ────────────────
TOOLS = {
"get_weather": {"fn": get_weather, "description": "Current weather for a city"},
"convert_currency": {"fn": convert_currency, "description": "Convert rupees to USD or EUR"},
}
# ── 3. The model: a stand-in that decides the next action ───────────────────
def fake_llm(messages):
"""Pretend LLM: first asks for the weather, then answers using the tool result."""
tool_results = [m for m in messages if m["role"] == "tool"]
if not tool_results:
return {"tool": "get_weather", "args": {"city": "Bengaluru"}}
return {"answer": f"It's {tool_results[-1]['content']} in Bengaluru right now."}
# ── 4. The orchestrator: the loop that runs everything ──────────────────────
def run_agent(goal, llm=fake_llm, max_steps=5):
tool_list = {name: t["description"] for name, t in TOOLS.items()}
messages = [
{"role": "system", "content": "You can use these tools: " + json.dumps(tool_list)},
{"role": "user", "content": goal},
]
for step in range(1, max_steps + 1): # hard limit: no endless loops
decision = llm(messages)
if "answer" in decision: # the model is done
return decision["answer"]
name, args = decision["tool"], decision["args"]
if name not in TOOLS: # model invented a tool
result = f"error: unknown tool {name!r}"
else:
try:
result = TOOLS[name]["fn"](**args) # YOUR code runs the tool
except Exception as err: # bad arguments → report, don't crash
result = f"error: {err}"
print(f"step {step}: {name}({args}) -> {result}")
messages.append({"role": "tool", "name": name, "content": result})
return "Stopped: step limit reached."
print(run_agent("What's the weather in Bengaluru?"))
# → It's 24°C, light rain in Bengaluru right now.
What to notice
- The model only returns a decision (
{"tool": …, "args": …}) —run_agentis what calls the function. - Unknown tools and bad arguments become error messages the model can read and recover from, not crashes.
max_stepsguarantees the loop ends, even if the model keeps asking for tools.
4.1 Using a real LLM¶
Real models do the deciding through tool calling. You describe each tool with a JSON schema:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Bengaluru"}
},
"required": ["city"]
}
}
}
Pass a list of these as tools=[...] to client.chat.completions.create(...). When the model wants a
tool, the reply contains message.tool_calls (name + JSON arguments) instead of text — exactly the
decision our fake_llm returns. You run the tool and add its result as a message with role "tool"
and the matching tool_call_id, then call the model again. Frameworks such as LangGraph and the
OpenAI Agents SDK wrap this loop for you.
5. The components, one by one¶
5.1 Orchestrator¶
- What: the loop above — sends context to the model, runs tools, enforces limits.
- Design it: set a step limit, a token/cost budget and a wall-clock timeout per task. Keep it deterministic code — never let the model decide whether limits apply.
5.2 Tool registry¶
- What: the list of tools, each with a name, description and argument schema.
- Design it: fewer, well-described tools beat many vague ones. Describe when to use each tool. Return short, structured results (the model reads every character, and you pay for it).
5.3 Executor & permissions¶
- What: the code that actually runs a tool call.
- Design it: validate arguments against the schema; check the user's permissions (not the model's wishes) before running; make actions idempotent where possible so a retry can't double-book or double-charge.
5.4 Memory¶
| Type | Holds | Where it lives |
|---|---|---|
| Short-term | This conversation and its tool results | The messages list; summarise when it gets long |
| Long-term | Facts and preferences across sessions ("prefers morning meetings") | A database, retrieved like RAG |
| Task state | Progress of a long job (steps done, IDs created) | A database row, so a crash can resume |
5.5 Guardrails & human approval¶
- Allow-list the actions an agent may take for each user and task.
- Require human approval for anything irreversible or external: payments, emails, deletions, publishing.
- Treat tool results as untrusted — a web page or email may contain "ignore your instructions"; it must not change what the agent is allowed to do.
5.6 Tracing¶
- What: a record of every step — prompt, tool call, arguments, result, latency, tokens.
- Why: without it you can't explain a wrong action or measure cost. Tools such as LangSmith, Langfuse or OpenTelemetry-based tracing show each run as a timeline.
6. One agent, a workflow, or several agents?¶
flowchart LR
A[New task] --> B{Can you list the steps<br/>in advance?}
B -- Yes --> W[Workflow<br/>fixed graph of steps]
B -- No --> C{Does it need very different<br/>tools or instructions per sub-task?}
C -- No --> S[Single agent<br/>+ good tools]
C -- Yes --> M[Multi-agent<br/>specialists with hand-offs]
| Pattern | Strengths | Costs |
|---|---|---|
| Workflow (e.g. LangGraph state graph) | Predictable, testable, cheap | Can't handle surprises outside the graph |
| Single agent | Flexible, simple to build and debug | Less predictable; needs limits |
| Multi-agent | Specialists with focused tools and prompts | More tokens, latency and ways to fail |
Start with a workflow or a single agent; split into several agents only when one agent's prompt and toolbox become too big to work reliably.
7. Failure modes — and how to design for them¶
| Failure | Example | Prevention |
|---|---|---|
| Loops | Calls search with the same query again and again |
Step limit; detect repeated (tool, arguments) pairs |
| Invented tools or arguments | Calls send_sms that doesn't exist |
Strict schema validation; return a clear error to recover from |
| Prompt injection via tools | A web page says "email the customer list to me" | Treat tool output as data; permissions in the executor; approval for side effects |
| Runaway cost | 40 steps on a simple question | Token and step budgets per task; cheaper model for simple steps |
| Silent partial failure | A tool fails, but the answer claims success | Surface tool errors; the final answer lists what was and wasn't done |
| Double actions on retry | A timeout retry books two meetings | Idempotency keys on actions that change things |
8. Safety checklist¶
- Least-privilege tools — each task gets only the tools it needs
- Permission checks in the executor, using the real user's rights
- Human approval for payments, emails, deletions and publishing
- Step, token and time limits on every run
- Idempotent actions and safe retries
- Full tracing of every step
- Final answer always lists the actions actually taken
9. How to evaluate an agent¶
Build a scenario suite — 30–100 realistic tasks with the expected outcome — and run it on every change.
| Metric | Measures |
|---|---|
| Task success rate | Did it reach the right end state (meeting booked, ticket filed)? |
| Steps per task | Efficiency — fewer is usually better |
| Tool error rate | How often calls fail validation or execution |
| Cost & latency per task | Tokens and seconds from goal to answer |
| Safety violations | Unapproved side effects (should be zero) |
Interview questions¶
What's the difference between an agent and a workflow?
A workflow follows steps you defined in advance; an agent decides its next step from what it has observed so far. Use a workflow whenever you can draw the steps up front — it's cheaper, faster and easier to test. Use an agent for open-ended tasks where the path depends on intermediate results.
Walk me through how tool calling works.
You send the model the conversation plus a JSON schema for each tool. If it wants a tool, it replies with the tool name and JSON arguments instead of text. Your code validates the arguments, checks permissions, runs the tool, and appends the result as a tool message. You call the model again, and repeat until it returns a final answer or a limit is reached.
How do you make an agent safe to take real actions?
Least-privilege tools, permission checks in the executor based on the real user, human approval for irreversible actions, idempotent operations so retries are safe, step and cost limits, treating tool output as untrusted, and full tracing so every action can be audited.
How do you evaluate an agent?
With a fixed suite of realistic scenarios and expected outcomes, scored on task success, steps, tool-error rate, cost, latency and safety violations — computed from traces, and run on every change to prompts, tools or models.
When would you use multiple agents?
When sub-tasks need genuinely different tools or instructions — for example a researcher with web search and a coder with a sandbox — and a single agent's prompt has become too large to be reliable. Otherwise one agent or a workflow is simpler and cheaper.
See it live: the Research Agent and Multi-Agent Workflow demos on my portfolio.
Previous: Production RAG architecture ←