Lesson 11.1 · 25 min
AI Agents: Autonomous Decision-Making Systems
A chatbot can tell you how to book a flight; what does it take for software to actually go and book it?
In short: An AI agent is a program where a language model decides, step by step, which actions to take toward a goal, runs those actions through tools, looks at the results, and repeats until the goal is met. It is built from five parts: a model, instructions, tools, memory, and a loop that ties them together. Agents shine on open-ended, multi-step tasks, but every extra step is another chance to fail, so we add limits, checks and humans where it matters.
The big picture
So far in this course, a large language model (LLM) has been a function: text goes in, text comes out. We ask a question, it answers, and the conversation ends. That is powerful, but it is also passive. The model cannot check today's price of a stock, open a file, run a test, or send an email. It can only talk about doing those things.
An AI agent removes that limit. We wrap the model in a small program that lets it do things: call a search engine, query a database, run code. After each action, the program shows the model what happened, and the model decides what to do next. The model stops being only a writer and becomes a decision maker inside a loop.
Think of it like a new intern A plain LLM is like a very well-read intern who is locked in a room with no phone and no computer. Ask anything and you get a thoughtful answer from memory, but nothing gets done. An agent is the same intern, now given a laptop, a few approved apps, a notepad, and a task list. They try something, look at the result, adjust, and keep going until the task is finished, or until they ask you for help.
In this module we will build up the full picture: function calling (how a model asks for an action), the agent loop (how actions repeat), well-known agent designs (ReAct, Plan-and-Execute, Reflection), memory, and MCP (a standard way to plug tools in). This first lesson gives the overview that everything else hangs on.
What is an AI agent?
A simple working definition: an AI agent is a system in which an LLM chooses its own next action, in a loop, to reach a goal, using tools to act on the world and observations to see the results.
Three words in that sentence do the heavy lifting:
- Goal: the agent is given an outcome (“find the three cheapest flights to Lisbon next Friday”), not a fixed script of steps.
- Chooses: the model, not the programmer, decides which step comes next. This is called autonomy, and it comes in degrees.
- Loop: the agent acts, observes, and acts again, as many times as needed. The number of steps is not known in advance.
The word agent is older than LLMs. In classic AI textbooks (Russell and Norvig's Artificial Intelligence: A Modern Approach), an agent is anything that perceives its environment through sensors and acts on it through actuators. An LLM agent fits that frame: tool results are its perceptions, tool calls are its actions, and the LLM is the part that maps one to the other.
Workflow or agent? Many useful LLM apps are workflows: the programmer fixes the steps in code (summarise → translate → email). The LLM fills in each step but never picks the path. An agent lets the model pick the path. Workflows are more predictable; agents are more flexible. Many real systems mix the two.
Pause and think: A script calls an LLM to summarise each new support ticket and then always posts the summary to Slack. Is this an agent?
No. The steps are fixed by the programmer and run the same way every time; the model never decides what to do next. It is a workflow (a pipeline) that uses an LLM. It becomes agent-like only when the model chooses actions, for example deciding whether to search the knowledge base, escalate, or reply.
AI agent vs plain LLM vs chatbot
These three terms get mixed up a lot. The difference is about who acts and how many turns happen without a human.
Notice that the line is blurry. A chatbot that can call one tool (say, a weather lookup) is a small step toward an agent. People often talk about an autonomy spectrum: from fixed pipelines, to a model that routes between a few options, to a model that plans and executes freely for many steps.
The five core parts
Almost every agent, from a weekend script to a production coding assistant, is built from the same five parts:
| Part | What it is | Travel agent example |
|---|---|---|
| 1. Model (the brain) | The LLM that reads the situation and decides the next step. | A capable chat model that supports tool calling. |
| 2. Instructions (the goal and rules) | The system prompt: role, goal, constraints, style, when to stop or ask. | “Find the cheapest refundable option. Never book without user approval.” |
| 3. Tools (the hands) | Functions the program can run for the model: APIs, search, code, databases. | search_flights, search_hotels, get_calendar, book (with approval). |
| 4. Memory (the notebook) | What the agent knows: the running history this task, plus facts saved across sessions. | The flights already found; the user's saved preference for aisle seats. |
| 5. Loop (the orchestrator) | Plain code that calls the model, runs the chosen tool, feeds results back, and enforces limits. | Repeat until a final answer, at most 15 steps, ask before paying. |
Planning is a behaviour, not a separate box You will often see “planning” listed as an agent part. In practice planning happens inside the model (it reasons about what to do) and is shaped by the instructions and the loop design. Later lessons show designs that make planning explicit, such as Plan-and-Execute.
How an AI agent works end to end
Let us follow one request from start to finish. The user types: “How old is Python 3?” The agent has two tools: search and calculator.
One agent run, step by step
- Receive the goal: The orchestrator builds the first prompt: system instructions, the list of available tools with their descriptions, and the user's question.
- Think and choose: The model reasons that it needs the release year, and replies with a structured request: call
searchwith “python release”. It does not answer yet. - Act: The orchestrator sees the tool request, runs the real
searchfunction, and gets “Python 3.0 was released in 2008.” - Observe: That result is added to the conversation as an observation, and the model is called again with everything so far.
- Repeat: The model now needs the current year, then the subtraction. Each need becomes another act-observe round.
- Finish: When the model has enough information, it replies with a plain answer instead of a tool call. The loop sees “no tool call” and returns the answer to the user.
Two details matter. First, the model only ever produces text (often structured as JSON). Our code turns that text into real actions. This is what keeps the system controllable: we decide which tools exist and we can block, log or require approval for any of them. Second, the conversation history is the agent's working memory. The model itself is stateless: between calls it remembers nothing except what we send it. Each model call re-reads the whole history, which is why long runs get slower and more expensive.
A concrete example: a tiny research agent
Here is the whole idea in about 35 lines of Python. To make it runnable without an API key, the “model” is a scripted function that makes the same decisions a real LLM would make for this question. Everything else (tools, memory, loop, step limit) is exactly how a real agent is wired.
research_agent.py
# A tiny research agent: a scripted "model" picks tools until it can answer.
DOCS = {"python release": "Python 3.0 was released in 2008.",
"python today": "The current year is 2026."}
def search(query): # tool 1: look something up
return DOCS.get(query, "no result")
def calculator(expr): # tool 2: do exact arithmetic
return str(eval(expr, {"__builtins__": {}}))
TOOLS = {"search": search, "calculator": calculator}
def fake_model(goal, memory):
"""Stands in for the LLM: decides the next action from what it has seen."""
seen = " ".join(memory)
if "2008" not in seen:
return ("search", "python release")
if "2026" not in seen:
return ("search", "python today")
if "Result:" not in seen:
return ("calculator", "2026 - 2008")
return ("final", "Python 3 is about 18 years old.")
goal = "How old is Python 3?"
memory = [] # what the agent has observed so far
for step in range(1, 6): # hard step limit = safety net
action, arg = fake_model(goal, memory)
if action == "final":
print(f"Step {step}: FINAL -> {arg}")
break
result = TOOLS[action](arg) # the app, not the model, runs the tool
memory.append(f"Result: {result}" if action == "calculator" else result)
print(f"Step {step}: {action}({arg!r}) -> {result}")
else:
print("Stopped: step limit reached")Output:
Step 1: search('python release') -> Python 3.0 was released in 2008.
Step 2: search('python today') -> The current year is 2026.
Step 3: calculator('2026 - 2008') -> 18
Step 4: FINAL -> Python 3 is about 18 years old.Swap fake_model for a real LLM call (with the tool list passed in) and you have a genuine agent. The next lesson, on function calling, shows exactly what that call looks like.
Pause and think: In the code above, what would happen if search returned “no result” for “python today” every time?
The model would keep asking for search('python today') because 2026 never appears in memory. The loop would run until the 5-step limit and print “Stopped: step limit reached”. This is why step limits (and repeated-call detection, covered in the agent-loop lesson) are essential.
Types of AI agents
There are two useful ways to classify agents: the classic textbook types (by how they decide) and the modern LLM designs (by how the loop is structured).
| Type | How it decides | Everyday example |
|---|---|---|
| Simple reflex | Fixed if-then rules on the current input only. | A thermostat: too cold → heat on. |
| Model-based reflex | Rules plus an internal picture of the world it updates over time. | A robot vacuum that remembers which rooms are already clean. |
| Goal-based | Considers which actions lead toward a goal. | A route planner finding a path to an address. |
| Utility-based | Weighs how good each outcome is, not just whether it reaches the goal. | A route planner trading off time, tolls and fuel. |
| Learning | Improves its behaviour from feedback over time. | A recommender that adapts to what you click. |
LLM agents are mostly goal-based (with some utility judgement inside the model). Within LLM agents, the common designs you will meet in this module are:
- Tool-calling agent: the basic loop from this lesson; the model calls tools until done.
- ReAct agent: the model writes an explicit Thought before each Action and reads an Observation after it.
- Plan-and-Execute agent: one call writes a full plan; the steps are then executed (and re-planned if something breaks).
- Reflection agent: the agent drafts, critiques its own draft, and revises.
- Multi-agent systems: several specialised agents (researcher, writer, reviewer) coordinated by an orchestrator, covered later in the module.
How we got to LLM agents
- The agent framing: Russell and Norvig's textbook organises AI around agents that perceive and act.
- ReAct: Researchers show that interleaving reasoning and tool actions in one prompt improves LLM performance on question answering and decision tasks.
- Autonomous agent demos and function calling: Open-source projects such as AutoGPT popularise looping LLMs; OpenAI adds function calling to its API, making structured tool requests standard.
- MCP and computer use: Anthropic releases the Model Context Protocol (a standard way to connect tools) and a computer-use capability where a model operates a desktop through screenshots.
- Agents in daily work: Coding agents, deep-research features and browser agents become mainstream products from several vendors.
What AI agents can do today
As of 2026, agents are reliable enough to be useful in areas where the work is checkable and mistakes are cheap to catch:
- Coding: read a codebase, edit files, run tests, fix failures, and open a pull request. Tests give the agent a clear signal of success.
- Research: run many searches, read pages, compare sources, and write a cited report.
- Data analysis: write and run SQL or Python against a dataset, check the numbers, and draw charts.
- Customer operations: look up an order, check a policy, issue a refund under a limit, and escalate anything unusual to a human.
- Computer and browser use: fill in web forms or operate apps that have no API, by looking at screenshots and clicking. Still slower and more error-prone than API tools.
Real-world pattern: the support agent A typical support agent has tools like get_order(id), get_refund_policy(), issue_refund(id, amount) and escalate(reason). The instructions say refunds above a set amount always need a human. The agent handles the routine 70–80% (illustrative) of tickets end to end, and the risky ones are routed to people with a summary already written.
When to use an AI agent (and when not to)
Agents cost more and are less predictable than a single LLM call or a fixed workflow. A good rule: use the simplest thing that works, and add autonomy only when the task needs it.
- Use an agent when the number and order of steps depend on what you find along the way (research, debugging, open-ended data questions), and when results can be verified (tests pass, numbers match, a human approves).
- Use a workflow when the steps are known in advance (extract → validate → store). It is cheaper, faster, and easier to test.
- Use a single LLM call when the task is one transformation of text (summarise, classify, translate, rewrite).
The chart is the single most important intuition about agents. With 95% reliability per step, a 10-step task fully succeeds only about 60% of the time (0.95¹⁰ ≈ 0.599). That is why good agent design keeps runs short, checks results along the way, and lets the agent recover from errors instead of stopping blindly.
Common failure modes
The mistake most teams make first Giving a new agent many tools, broad permissions, and no step limit, then testing it on a handful of happy-path examples. Start with few tools, read-only permissions, a hard step and cost budget, and human approval for anything irreversible.
| Failure | What it looks like | Typical fix |
|---|---|---|
| Infinite or long loops | Calls the same tool again and again. | Max steps, repeated-call detection, cost budget. |
| Wrong tool or bad arguments | Searches when it should calculate; passes a name instead of an ID. | Fewer, clearly named tools; good descriptions; argument validation. |
| Hallucinated results | Claims it did something it never did, or invents data. | Answer only from observations; log and check every tool call. |
| Losing the thread | Forgets the goal in a long run as context fills up. | Summarise history, restate the goal, use explicit plans. |
| Unsafe actions | Deletes data or sends a message without asking. | Least-privilege tools, approval steps, sandboxing. |
| Prompt injection | A web page or email it reads tells it to do something else. | Treat tool output as data, never as instructions; restrict what follows from it. |
Quick summary. An AI agent is an LLM inside a loop that chooses actions toward a goal. It has five parts: model, instructions, tools, memory, and the loop. The model only writes requests; our code executes them, which is where we add safety. Agents are great for multi-step, checkable work, but errors compound with every step, so we keep them simple, bounded and supervised.
Worked example, step by step
We said that every model call re-reads the whole history. Let us put small numbers on that, because it changes how we design agents. All numbers here are illustrative and chosen to be easy to add up.
Say the system prompt plus the tool list is 500 tokens. The user's question is 50 tokens. Each round adds about 200 tokens to the history: the model's tool request plus the tool's result. The run needs three tool calls and then a final answer, so the model is called four times.
| Model call | What it reads | Input tokens |
|---|---|---|
| 1 | Prompt + question | 550 |
| 2 | Prompt + question + 1 round | 750 |
| 3 | Prompt + question + 2 rounds | 950 |
| 4 | Prompt + question + 3 rounds | 1,150 |
| Total | All four calls | 3,400 |
Reading the table
- The history only grows: Each call reads everything the earlier calls read, plus one more round. Nothing is dropped unless we drop it.
- The total is more than the final history: The finished history is 1,150 tokens, but we paid to read 3,400. Early tokens are read again and again: the 500-token prompt was read four times.
- Cost grows faster than steps: With n calls, the total is n × 550 + 200 × (0 + 1 + … + (n − 1)). Double the steps and the second part roughly quadruples.
- Big tool results hurt most: If one search returned 3,000 tokens in round 1, every later call would carry those 3,000 tokens too. Trimming tool output is often the cheapest fix.
This is why real agents keep tool results short, summarise old history, and cap the number of steps. Many providers also offer prompt caching, which makes the repeated prefix cheaper to re-read, but the pattern of growth stays the same.
Practice: try it yourself
We will build a small support agent that handles refund requests. It has all five parts: a scripted model, instructions (a refund limit), two tools, a memory list, and a loop. The new idea is an approval rule that lives in the loop, so the model cannot skip it.
practice_support_agent.py
# A support agent with an approval rule: the loop, not the model, enforces it.
ORDERS = {"A17": {"item": "kettle", "paid": 40},
"B42": {"item": "laptop", "paid": 900}}
REFUND_LIMIT = 100 # instructions: bigger refunds need a human
def get_order(order_id): # read-only tool
return ORDERS.get(order_id, "unknown order")
def issue_refund(order_id): # tool with a real side effect
return f"refunded {ORDERS[order_id]['paid']} for {order_id}"
TOOLS = {"get_order": get_order, "issue_refund": issue_refund}
def fake_model(order_id, memory):
"""Scripted stand-in for the LLM: look up the order, then refund it."""
if not memory:
return ("get_order", order_id)
if len(memory) == 1 and isinstance(memory[0], dict):
return ("issue_refund", order_id)
return ("final", f"Ticket closed: {memory[-1]}")
def run_agent(order_id, max_steps=4):
memory = [] # observations for this task only
for step in range(1, max_steps + 1):
action, arg = fake_model(order_id, memory)
if action == "final":
return f" step {step}: {arg}"
if action == "issue_refund" and ORDERS[arg]["paid"] > REFUND_LIMIT:
result = "BLOCKED: sent to a human for approval" # guardrail in code
else:
result = TOOLS[action](arg)
memory.append(result)
print(f" step {step}: {action}({arg!r}) -> {result}")
return " stopped: step limit"
for oid in ["A17", "B42", "Z99"]:
print(f"Refund request for {oid}")
print(run_agent(oid))Output:
Refund request for A17
step 1: get_order('A17') -> {'item': 'kettle', 'paid': 40}
step 2: issue_refund('A17') -> refunded 40 for A17
step 3: Ticket closed: refunded 40 for A17
Refund request for B42
step 1: get_order('B42') -> {'item': 'laptop', 'paid': 900}
step 2: issue_refund('B42') -> BLOCKED: sent to a human for approval
step 3: Ticket closed: BLOCKED: sent to a human for approval
Refund request for Z99
step 1: get_order('Z99') -> unknown order
step 2: Ticket closed: unknown orderNow change it:
- Set
REFUND_LIMIT = 1000. Before running, predict what changes for orderB42and what stays the same forA17. - Set
max_steps=2inrun_agent. Predict which line each ticket ends with. Does any refund still happen? - Delete the guardrail
ifso every call goes straight toTOOLS[action](arg). Predict the output forB42, then say why relying on the model's instructions alone would be risky here.
Pause and think: For order Z99 the model never asks for a refund. Which line of the scripted model causes that, and what would a real LLM need in order to behave the same way?
The check isinstance(memory[0], dict) fails, because the lookup returned the text “unknown order”, so the model skips the refund and goes to the final message. A real LLM would need to read the observation and notice the order was not found. That works only because our code fed the tool result back into the history; without the observation the model would have nothing to react to.
Pause and think: The refund limit could be written only in the system prompt (“never refund more than 100”). Why do we also enforce it in the loop code?
A prompt rule is a request; the model usually follows it but can get it wrong, or be talked out of it by text it reads. A check in code runs every time, whatever the model outputs. Since the model only writes requests and our code runs the tools, the code is the one place where a rule can be guaranteed.
Key takeaways
- An AI agent is an LLM in a loop that chooses actions toward a goal, using tools and observing the results.
- Five parts: model, instructions, tools, memory, and the orchestrating loop.
- The model only writes requests; our code runs the tools, which is where safety controls live.
- Prefer a single call or a fixed workflow when the steps are known; use agents for open-ended, verifiable, multi-step work.
- Errors compound per step, so bound the loop, validate tool calls, and keep humans in charge of irreversible actions.
Key terms
- AI agent: A system where an LLM repeatedly chooses and takes actions through tools, observing results, until a goal is reached.
- Tool: A function the agent's program can run on the model's behalf, such as search, a database query or code execution.
- Observation: The result of a tool call, fed back to the model as input for its next decision.
- Orchestrator: The plain code that runs the loop: calls the model, executes tools, updates memory and enforces limits.
- Workflow: A fixed sequence of steps defined by the programmer, where an LLM may do individual steps but does not choose the path.
- Autonomy: How much the model, rather than the programmer or user, decides what happens next.
← 10.13 Vectorless RAG: Retrieval Without Embeddings or a Vector Store · 11.2 Function Calling: Giving LLMs Tools to Act on the World →