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
Last updated: 3 October 2026
"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:
| Part | What happens | In the example below |
|---|---|---|
| Think | The model reasons about what it knows and what it needs | A short "thought" field |
| Act | It chooses one action and its input | Look at tables, inspect columns, run SQL, or answer |
| Observe | Your code runs the action and returns the result | Rows 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.
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()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?
How is an AI agent different from a chatbot?
Why do AI agents fail?
Do AI agents need a framework?
Building AI agents and assistants with tools is part of the Building Gen AI Applications module of the Generative AI course.
Sources
- Building Effective AI Agents (Anthropic) — Anthropic
- ReAct: Synergizing Reasoning and Acting in Language Models (arXiv) — Yao et al.
- Structured outputs (Ollama documentation) — Ollama
- sqlite3: DB-API 2.0 interface for SQLite databases (Python documentation) — Python Software Foundation
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.
Model Context Protocol (MCP) Explained for Beginners
NextLangGraph Tutorial: Building Stateful AI Agents
Related articles
Prompt Engineering: Techniques That Actually Improve Output
Most prompt advice is a list of tips. This guide takes one job, pulling three fields out of customer emails, and rewrites the prompt seven times, scoring each version on the same twelve emails. An example and a schema fixed the format; definitions and asking for evidence before the verdict fixed the meaning.
Agentic AI Roadmap 2026: From Prompts to Autonomous Systems
Agentic AI is the most hyped corner of generative AI and the easiest to learn in the wrong order. This roadmap builds it in seven stages, from reliable prompts and structured output through retrieval and tools to workflows, agents and production safeguards, with something to build at each stage.
What Is Generative AI? A Complete Beginner's Guide
What generative AI is, in plain language: how language models produce text one token at a time (shown with a tiny runnable model), how chat models are trained, how image models differ, what the technology is reliable for, and why it confabulates.