AI Agents Guide: How Autonomous Agents Actually Work

An AI agent is a language model that chooses actions in a loop until a task is done. This guide explains the loop, the tools and the stopping rules, then runs a small agent against a sales database: it explores, recovers from its own SQL errors, and still gets three of four answers wrong in instructive ways.

GloryTecks Team

Training & Placement Faculty

15 May 2026 11 min read

Last updated: 3 October 2026

Share:

"Agent" is the most stretched word in AI. Underneath the marketing, the mechanism is simple. A language model is given a goal and a set of actions. It chooses an action, your code carries it out, the result goes back to the model, and it chooses again. The loop ends when the model says it is done or a limit is reached.

Anthropic, which builds agents for a living, puts it plainly: agents are typically just language models using tools based on feedback from their environment, in a loop. The hard part is not the loop. It is making each step reliable (Anthropic: building effective agents).

The loop

Each turn has three parts:

PartWhat happensIn the example below
ThinkThe model reasons about what it knows and what it needsA short "thought" field
ActIt chooses one action and its inputLook at tables, inspect columns, run SQL, or answer
ObserveYour code runs the action and returns the resultRows from the database, or an error message

This pattern of interleaving reasoning with actions was set out in the ReAct paper, where reasoning helps the model track and update its plan and handle exceptions, and actions bring in information it does not have (Yao et al.: ReAct).

Around the loop sit the decisions that make an agent safe to run. Anthropic stresses three: real feedback from the environment at every step, stopping conditions such as a maximum number of iterations, and testing in sandboxed environments with guardrails, because autonomy brings higher costs and compounding errors.

An agent over a database

The script gives a small local model a sales database invented for the example, with products, customers and orders. It has four actions: list the tables, describe a table, run a SQL query, or give the final answer. Each step, the model must reply with JSON matching a schema, which the model runtime enforces (Ollama: structured outputs). The database connection refuses anything but reads, using SQLite's authorizer hook (Python: sqlite3), and the loop stops after eight steps.

Four questions go in. The correct answers are computed separately with SQL, so the agent's answers can be checked.

python
import json
import random
import re
import sqlite3
import urllib.request

URL = "http://localhost:11434/api/chat"
MODEL = "llama3.2"
OPTIONS = {"temperature": 0, "seed": 42, "num_ctx": 8192, "num_predict": 300}
MAX_STEPS = 8

# --- a small sales database, invented for this example -------------------------------
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE products (product_id INTEGER PRIMARY KEY, name TEXT, category TEXT, unit_price INTEGER);
CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT, city TEXT);
CREATE TABLE orders (order_id INTEGER PRIMARY KEY, order_date TEXT, customer_id INTEGER, product_id INTEGER, quantity INTEGER);
""")
products = [("desk lamp", "lighting", 1499), ("floor lamp", "lighting", 2999), ("office chair", "seating", 8999),
            ("stool", "seating", 1999), ("standing desk", "desks", 24999), ("writing desk", "desks", 11999),
            ("monitor arm", "accessories", 3499), ("cable tray", "accessories", 799)]
db.executemany("INSERT INTO products (name, category, unit_price) VALUES (?, ?, ?)", products)
cities = ["Pune", "Hyderabad", "Chennai", "Bengaluru"]
db.executemany("INSERT INTO customers (name, city) VALUES (?, ?)",
               [(f"Customer {i}", cities[i % 4 if i % 5 else 0]) for i in range(1, 13)])
rng = random.Random(7)
db.executemany("INSERT INTO orders (order_date, customer_id, product_id, quantity) VALUES (?, ?, ?, ?)",
               [(f"2026-{rng.randint(1, 4):02d}-{rng.randint(1, 28):02d}", rng.randint(1, 12),
                 rng.randint(1, 8), rng.randint(1, 4)) for _ in range(150)])


def read_only(action, *args):
    # the agent may read anything and change nothing
    allowed = (sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ, sqlite3.SQLITE_FUNCTION)
    return sqlite3.SQLITE_OK if action in allowed else sqlite3.SQLITE_DENY


# --- tools --------------------------------------------------------------------------------
def list_tables(_):
    return ", ".join(r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'"))


def describe_table(table):
    cols = db.execute("SELECT name, type FROM pragma_table_info(?)", (table.strip(),)).fetchall()
    return ", ".join(f"{name} {kind}" for name, kind in cols) or f"error: no table named {table}"


def run_sql(query):
    db.set_authorizer(read_only)
    try:
        rows = db.execute(query).fetchmany(10)
        return "\n".join(str(r) for r in rows) or "(no rows)"
    except sqlite3.Error as err:
        return f"error: {err}"
    finally:
        db.set_authorizer(None)


TOOLS = {"list_tables": list_tables, "describe_table": describe_table, "run_sql": run_sql}


def call(messages, **extra):
    body = {"model": MODEL, "messages": messages, "stream": False, "options": OPTIONS, **extra}
    req = urllib.request.Request(URL, json.dumps(body).encode(), {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=900) as r:
        return json.load(r)["message"]


# --- the agent: each step the model returns one action as JSON, and code runs it -------------
ACTION = {"type": "object", "required": ["thought", "action", "input"], "properties": {
    "thought": {"type": "string"},
    "action": {"type": "string", "enum": ["list_tables", "describe_table", "run_sql", "final_answer"]},
    "input": {"type": "string"}}}
SYSTEM = ("You answer questions about a shop's sales database. Work in steps. At each step reply with a thought, "
          "one action and its input. Actions: list_tables (input ignored), describe_table (input: table name), "
          "run_sql (input: one SQLite SELECT query), final_answer (input: the answer for the user). "
          "Look at the tables and columns before writing SQL. Revenue is quantity times unit_price.")


def agent(question):
    messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": question}]
    trace = []
    for step in range(1, MAX_STEPS + 1):
        reply = call(messages, format=ACTION)
        messages.append(reply)
        act = json.loads(reply["content"])
        if act["action"] == "final_answer":
            return act["input"], step, trace
        result = TOOLS[act["action"]](act["input"])
        trace.append((step, act["action"], act["input"], result))
        messages.append({"role": "user", "content": f"Observation: {result}"})
    return "(stopped: step limit reached)", MAX_STEPS, trace


TASKS = [
    ("How many customers are in Pune?",
     "SELECT COUNT(*) FROM customers WHERE city = 'Pune'"),
    ("What was the total quantity of desk lamps sold?",
     "SELECT SUM(o.quantity) FROM orders o JOIN products p USING (product_id) WHERE p.name = 'desk lamp'"),
    ("Which product category had the highest revenue in March 2026?",
     "SELECT p.category FROM orders o JOIN products p USING (product_id) WHERE o.order_date LIKE '2026-03-%' "
     "GROUP BY p.category ORDER BY SUM(o.quantity * p.unit_price) DESC LIMIT 1"),
    ("Which city's customers placed the most orders?",
     "SELECT c.city FROM orders o JOIN customers c USING (customer_id) GROUP BY c.city ORDER BY COUNT(*) DESC LIMIT 1"),
]

traces = []
for question, truth_sql in TASKS:
    truth = str(db.execute(truth_sql).fetchone()[0])
    answer, steps, trace = agent(question)
    errors = sum(1 for t in trace if t[3].startswith("error"))
    correct = re.search(rf"\b{re.escape(truth.lower())}\b", answer.lower().replace(",", "")) is not None
    traces.append(trace)
    print(f"Q: {question}")
    print(f"   steps {steps}, tool calls {len(trace)}, errors returned {errors}, correct answer {truth}")
    print(f"   agent: {' '.join(answer.split())[:150]}")
    print(f"   {'RIGHT' if correct else 'WRONG'}\n")

for number in (1, 3):
    print(f"trace of question {number}")
    for step, name, arg, result in traces[number - 1]:
        print(f"step {step}: {name}({' '.join(arg.split())!r})")
        print("   -> " + result.replace("\n", " | ")[:120])
    print()
text
Q: How many customers are in Pune?
   steps 6, tool calls 5, errors returned 0, correct answer 5
   agent: $967,845
   WRONG

Q: What was the total quantity of desk lamps sold?
   steps 4, tool calls 3, errors returned 0, correct answer 45
   agent: 45
   RIGHT

Q: Which product category had the highest revenue in March 2026?
   steps 8, tool calls 7, errors returned 3, correct answer seating
   agent: 271,962
   WRONG

Q: Which city's customers placed the most orders?
   steps 8, tool calls 8, errors returned 3, correct answer Pune
   agent: (stopped: step limit reached)
   WRONG

trace of question 1
step 1: describe_table('customers')
   -> customer_id INTEGER, name TEXT, city TEXT
step 2: run_sql("SELECT COUNT(*) FROM customers WHERE city = 'Pune'")
   -> (5,)
step 3: describe_table('orders')
   -> order_id INTEGER, order_date TEXT, customer_id INTEGER, product_id INTEGER, quantity INTEGER
step 4: describe_table('products')
   -> product_id INTEGER, name TEXT, category TEXT, unit_price INTEGER
step 5: run_sql("SELECT SUM(p.unit_price * o.quantity) FROM orders o JOIN customers c ON o.customer_id = c.customer_id JOIN products p ON o.product_id = p.product_id WHERE c.city = 'Pune'")
   -> (967845,)

trace of question 3
step 1: run_sql("SELECT p.category, SUM(s.quantity * s.unit_price) AS revenue FROM sales s JOIN products p ON s.product_id = p.id WHERE s.date LIKE '2026-03%' GROUP BY p.category ORDER BY revenue DESC LIMIT 1;")
   -> error: no such table: sales
step 2: list_tables('')
   -> products, customers, orders
step 3: describe_table('orders')
   -> order_id INTEGER, order_date TEXT, customer_id INTEGER, product_id INTEGER, quantity INTEGER
step 4: run_sql("SELECT p.category, SUM(o.quantity * o.unit_price) AS revenue FROM orders o JOIN products p ON o.product_id = p.id WHERE o.order_date LIKE '2026-03%' GROUP BY p.category ORDER BY revenue DESC LIMIT 1;")
   -> error: no such column: o.unit_price
step 5: describe_table('products')
   -> product_id INTEGER, name TEXT, category TEXT, unit_price INTEGER
step 6: run_sql("SELECT p.category, SUM(o.quantity * p.unit_price) AS revenue FROM orders o JOIN products p ON o.product_id = p.id WHERE o.order_date LIKE '2026-03%' GROUP BY p.category ORDER BY revenue DESC LIMIT 1;")
   -> error: no such column: p.id
step 7: run_sql("SELECT p.category, SUM(o.quantity * p.unit_price) AS revenue FROM orders o JOIN products p ON o.product_id = p.product_id WHERE o.order_date LIKE '2026-03%' GROUP BY p.category ORDER BY revenue DESC LIMIT 1;")
   -> ('seating', 271962)

What the agent did

The mechanics worked. The agent explored the database, wrote queries, read errors and corrected itself. The outcomes are where it gets interesting.

  • One question was answered correctly, the total desk lamps sold, in four steps.
  • It recovered from its own mistakes. On the third question it guessed a table called "sales", which does not exist, then listed the tables, described them, and fixed two broken joins in turn until its query returned the right row: seating, with revenue of 271,962.
  • Then it answered the wrong thing. Asked which category had the highest revenue, it replied with the revenue figure instead of the category. The data was right; the final answer did not match the question.
  • It found the answer and kept going. Asked how many customers are in Pune, its second step returned the right count, 5. It then explored the orders and products tables and answered with the revenue from Pune's customers, 967,845. Each step looked reasonable; the agent had no sense that it was already done.
  • It ran out of steps. On the last question it was still correcting errors when the eight-step limit stopped it. Without the limit it would have kept spending.

None of this is unusual for a model of this size, and larger models fail less often. But the failure types are the general ones: drifting from the question, compounding small errors, and looping. They are what agent design has to catch.

Designing agents that hold up

Give the agent ground truth. Errors returned as observations let it correct course, as the third question showed. Silent failures teach it nothing.

Make tools hard to misuse. Read-only access was enforced by the database, not by asking the model nicely. Anthropic describes spending more time improving tools than prompts, and changing tool arguments so mistakes become harder.

Check the final answer against the question. A last validation step, by code or a second model call, would have caught "271,962" as the answer to a "which category" question.

Set limits and report them. Maximum steps, tool calls and tokens, with a clear message to the user when a limit is hit.

Keep traces. The two traces above explain more about the agent than its score. Log every step in production.

Prefer a workflow when the steps are known. If the task always follows the same path, fixed code calling the model at each stage is cheaper and more predictable than an agent choosing its own path. See the agentic AI roadmap.

Memory and context

An agent's memory is mostly its context: every action and observation so far, sent back to the model each step. That grows quickly, and long tasks run into the context limit and into rising cost. Longer-running agents summarise completed work or save notes to external storage and read them back when needed. See context windows explained.

Where to go from here

The actions in this example are plain functions. In production they are usually tools defined through a model API's function calling, see function calling and tool use, or exposed by MCP servers, see MCP explained. When one agent is not enough, see multi-agent systems.

Common mistakes

  • No step limit, so a confused agent loops until the budget runs out.
  • Permissions enforced by the prompt instead of by the system.
  • Trusting the final answer without checking it matches the question.
  • Judging an agent by a demo instead of a set of tasks with known answers.
  • Long contexts with no summarising, so cost and confusion grow each step.
  • Using an agent where a fixed workflow would do.

What is an AI agent?

A language model that works towards a goal by choosing actions in a loop: it decides what to do, code carries out the action, the result goes back to the model, and it decides again until it answers or reaches a limit.

How is an AI agent different from a chatbot?

A chatbot replies to each message. An agent takes several actions on its own between messages, such as searching, querying a database or calling an API, and uses the results to decide its next step.

Why do AI agents fail?

Small errors compound over many steps, the model can drift from the original question, and it can loop without finishing. Tool errors, missing information and long contexts make this worse. Limits, validation and good tools reduce it.

Do AI agents need a framework?

No. The loop is a few dozen lines of code, as in the example above. Frameworks help with persistence, human approval steps and tracing once agents become complex.

Building AI agents and assistants with tools is part of the Building Gen AI Applications module of the Generative AI course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Generative AI?

Join GloryTecks’ expert-led Agentic AI training in Hyderabad with real-time projects and 100% placement support.

Explore course