Learn / AI / AI: One Course / Agentic AI: From Chatbots to Agents

AI: One Course · Lesson 13 of 20

Agentic AI: From Chatbots to Agents

What an agent really is, the ReAct loop with a runnable demo, workflows vs agents, patterns and failure modes.

  • Intermediate
  • 30 min read
  • 3 objectives

Before this lessonLesson 12: Tool Calling and Structured Output

What you will learn

  • Define agentic AI precisely
  • Trace a ReAct loop
  • Choose between a workflow and an agent

Your Progress

0 of 20 lessons 0%

  • Lessons0 / 20
  • Completed0
  • Est. time left~ 9 hours

Create a free account to keep your progress on every device.

"Agentic AI" is the hottest phrase in the industry and also one of the most abused. Vendors slap it on anything with a chat box. Let us define it properly, watch a real agent loop run step by step, and learn the single most valuable skill in this whole area: knowing when not to build an agent.

A precise definition

An agent is an LLM that decides its own next step, in a loop, using tools, until a goal is reached. Three ingredients: a goal, tools to act on the world, and a loop in which the model chooses what to do based on what it has observed so far.

The key word is decides. In a normal program you write the steps. In an agent, the model controls the control flow.

The autonomy spectrum

LESS autonomy                                                          MORE autonomy
  |                                                                          |
  v                                                                          v
[ single LLM call ] -> [ chain / workflow ] -> [ router ] -> [ agent ] -> [ multi-agent system ]

  "summarise this"     fixed steps you wrote:   model picks    model loops,      several agents
                        extract -> classify      one branch     picks tools       coordinating
                        -> draft                                and steps
  • Workflow: LLM calls and tools orchestrated by predefined code paths. Predictable, cheap, testable.
  • Agent: the LLM directs its own process. Flexible for open-ended problems where you cannot list the steps in advance, but slower, costlier and harder to control.

The ReAct loop: reason, act, observe

The classic agent pattern is ReAct (Reasoning + Acting). On each turn the model writes a short thought, picks an action (a tool call), you run it, and the observation is fed back. It repeats until the model decides it can give a final answer.

Goal: "What is the population of Norway divided by the population of Iceland?"

Thought: I need both populations.        Action: search("population of Norway")
Observation: 5.5 million
Thought: Now Iceland.                     Action: search("population of Iceland")
Observation: 0.38 million
Thought: I should divide, use the calculator.   Action: calculator("5.5 / 0.38")
Observation: 14.47
Thought: I have the answer.               Final answer: about 14.5 times larger.

Here it is as running code. The "model" is scripted (a lookup of what a good model would decide) so the demo is deterministic, but the loop, the tool dispatch, the observations and the stopping conditions are exactly what a real agent has:

import re

# ---------- tools ----------
FACTS = {"population of norway": "5.5 million", "population of iceland": "0.38 million"}

def search(query):
    return FACTS.get(query.lower(), "no results")

def calculator(expression):
    if not re.fullmatch(r"[0-9.\s+\-*/()]+", expression):        # never eval arbitrary text!
        return "error: only arithmetic allowed"
    return str(round(eval(expression), 2))

TOOLS = {"search": search, "calculator": calculator}

# ---------- a scripted stand-in for the LLM's decisions ----------
def decide(goal, history):
    """Returns (thought, action, argument) or (thought, 'final', answer)."""
    seen = {a: obs for a, arg, obs in history}
    got = [obs for a, arg, obs in history]
    if len(history) == 0:
        return "I need Norway's population first.", "search", "population of Norway"
    if len(history) == 1:
        return "Now I need Iceland's population.", "search", "population of Iceland"
    if len(history) == 2:
        a = float(got[0].split()[0]); b = float(got[1].split()[0])
        return "Divide the two numbers with the calculator.", "calculator", f"{a} / {b}"
    return "I have everything I need.", "final", f"Norway has about {got[2]} times as many people as Iceland."

# ---------- the agent loop ----------
def run_agent(goal, max_steps=6):
    history = []                                                   # (action, argument, observation)
    for step in range(1, max_steps + 1):
        thought, action, arg = decide(goal, history)
        print(f"step {step}")
        print(f"  thought    : {thought}")
        if action == "final":
            print(f"  FINAL      : {arg}")
            return arg
        observation = TOOLS[action](arg)
        print(f"  action     : {action}({arg!r})")
        print(f"  observation: {observation}")
        history.append((action, arg, observation))
    return "stopped: step limit reached"

run_agent("How many times bigger is Norway's population than Iceland's?")
Output
step 1
  thought    : I need Norway's population first.
  action     : search('population of Norway')
  observation: 5.5 million
step 2
  thought    : Now I need Iceland's population.
  action     : search('population of Iceland')
  observation: 0.38 million
step 3
  thought    : Divide the two numbers with the calculator.
  action     : calculator('5.5 / 0.38')
  observation: 14.47
step 4
  thought    : I have everything I need.
  FINAL      : Norway has about 14.47 times as many people as Iceland.

Every real agent, from a coding assistant to a research bot, is this loop with a smarter decide() (an LLM call) and more tools. Notice the two guardrails already in the code: max_steps (so it cannot run forever) and the calculator refusing anything that is not arithmetic.

Five workflow patterns you should know

Before reaching for a full agent, these composable patterns solve most problems with far more control:

  • 1. Prompt chaining. Break a task into a fixed sequence: draft, then critique, then rewrite. Each LLM call handles one easy step; you can insert checks between steps.
  • 2. Routing. Classify the input first, then send it down the right path: refund question, tech support, or sales; easy questions to a cheap model, hard ones to a big model.
  • 3. Parallelisation. Run independent subtasks at once (check one document against five criteria simultaneously) or vote (run three times, take the majority).
  • 4. Orchestrator-workers. A central LLM breaks a task into subtasks it did not know in advance and delegates them to worker LLMs, then merges results. (Great for coding across many files.)
  • 5. Evaluator-optimiser. One LLM produces, another critiques, repeat until the critic is satisfied.

What agents are genuinely good at

  • Coding: reading a repo, editing files, running tests, fixing failures. Ideal, because there is a verifiable signal (do the tests pass?).
  • Research: search, read, follow leads, synthesise.
  • Customer support with tools (look up order, issue refund) plus human escalation.
  • Data tasks: explore a database, write queries, fix them when they error.
  • Computer/browser use: operating software through screenshots and clicks.

The common thread: tasks with clear success criteria and tools that give feedback. The agent can tell when it is wrong and try again.

How agents fail

  • Compounding errors. If each step is 95% reliable, ten steps are only 60% reliable (0.9510). Long chains decay fast.
  • Infinite or wasteful loops. Retrying the same failing action forever. Fix with step limits, budgets and loop detection.
  • Context bloat. Every observation is appended, the prompt grows, cost climbs, and quality drops ("lost in the middle").
  • Tool misuse. Wrong tool, wrong arguments, or a destructive action taken too eagerly.
  • Goal drift and over-eagerness. Doing more than asked.
  • Prompt injection. A web page the agent reads says "ignore your instructions and..." (safety lesson).
  • Cost and latency. Ten LLM calls per task adds up. Measure it.

Let us put a number on that first bullet, because it changes how you design systems:

for per_step in (0.99, 0.95, 0.90):
    row = "  ".join(f"{n:>2} steps: {per_step ** n:5.0%}" for n in (1, 5, 10, 20))
    print(f"each step {per_step:.0%} reliable ->  {row}")
Output
each step 99% reliable ->   1 steps:   99%   5 steps:   95%  10 steps:   90%  20 steps:   82%
each step 95% reliable ->   1 steps:   95%   5 steps:   77%  10 steps:   60%  20 steps:   36%
each step 90% reliable ->   1 steps:   90%   5 steps:   59%  10 steps:   35%  20 steps:   12%

A step that is right 95% of the time sounds great until you chain twenty of them (36%). This is why good agent design keeps loops short, adds verification steps, and lets humans approve risky moves.

Autonomy dials: keep a human in the loop

  • Read-only agents (research, analysis) can run freely.
  • Write-capable agents should propose changes and wait for approval (a diff to review, a draft to send).
  • Irreversible actions (payments, deletions, external emails) need explicit confirmation.
  • Escalation: when confidence is low or the request is unusual, hand off to a person with the context attached.

So, should you build an agent?

Can one well-written prompt do it?            -> YES: do that.
Can you list the steps in advance?            -> YES: build a WORKFLOW (chain/router).
Steps unknown, but there is a clear success   -> Consider an AGENT, with tools that give feedback,
check and tools that give feedback?               step limits, budgets and human approval.
Stakes high, no way to verify?                -> Keep a human in the loop, or don't automate it.
# Write your solution here
Up next · Lesson 14The Agent Harness and Agent SDKsThe code around the model: loop, tools, context, memory, permissions, budgets. Build a mini harness and meet the SDKs.