Agent Fundamentals
What actually makes something an "agent": an LLM in a loop that reasons, calls tools, observes results, and repeats until done. Build a ReAct agent from scratch, and learn the single most useful judgment in the field — when you need an agent versus a fixed chain.
- Define an agent precisely: an LLM in a loop that reasons, acts (calls tools), observes, and repeats.
- Trace the ReAct pattern (Reason → Act → Observe) and build one from scratch, no framework.
- Give an agent tools and a stopping condition, and understand why the loop — not the model — is the agent.
- Make the field's highest-value judgment: when to use an agent vs. a fixed chain (and default to the chain).
Python for JavaScript developers — idioms used below:
- A "tool" is just a function plus a name and description string; we keep them in a
dict(name → function). while True:withbreak— an explicit loop you exit on a condition, likewhile (true) { … break; }.re.search(pattern, text)— regex match, liketext.match(/pattern/);.group(1)is the first capture.messages.append({...})— push a message dict onto the conversation list, likearr.push({...}).- triple-quoted
f"""…"""— a multi-line template literal with{…}interpolation.
Every code block is commented line-by-line.
What an agent actually is
The word "agent" is overused. A useful definition: an agent is an LLM running in a loop, where the model can choose to call tools, see the results, and decide what to do next — until it reaches an answer. Strip away the hype and there are exactly three ingredients:
- An LLM that decides what to do at each step.
- Tools — functions the model can invoke to act on the world (search, run code, query a DB, call an API).
- A loop that feeds each tool's result back to the model so it can decide the next step.
The loop is the whole point. A single LLM call answers from what it already knows; an agent can take an action, observe what happened, and adjust — which is what lets it handle tasks whose steps aren't known in advance.
"Agent" vs "chain" is the distinction that matters. A chain is a fixed, predetermined sequence of steps you wired up (RAG is a chain: retrieve → generate, always). An agent decides the steps at runtime. More power, but also more cost, latency, and unpredictability. Most production "AI features" are chains — reach for an agent only when the task genuinely needs runtime decisions. We'll make this call concrete at the end.
The ReAct pattern
The dominant agent pattern is ReAct (Reason + Act). At each turn the model produces a Thought (its reasoning), then either an Action (a tool call with input) or a Final Answer. Your code runs the action, appends the Observation (the result), and loops. The transcript looks like:
Thought: I need the current price of the Pro plan. I'll search the docs.
Action: search: Pro plan price
Observation: The Pro plan is €49/month billed annually.
Thought: I now have the answer.
Final Answer: The Pro plan costs €49/month (billed annually).
The model reasons about what to do, acts, then reads the result before continuing — interleaving thinking and doing. That interleaving is what makes it robust: it can recover when a tool returns something unexpected.
Building a ReAct agent from scratch
No framework — just the chat API, a couple of tools, and a loop. First, tools are plain functions kept in a registry:
import re # regex, to parse the model's "Action:" line
def search(query: str) -> str: # a fake knowledge tool (swap for real RAG/HTTP later)
docs = {"pro plan price": "The Pro plan is €49/month billed annually.",
"free plan": "The Free plan includes 100 messages/month."}
return docs.get(query.lower().strip(), "No result.") # dict.get → value or a default (like obj[k] ?? default)
def calculator(expr: str) -> str: # a compute tool
return str(eval(expr)) # eval() runs the expression; demo-only — never eval untrusted input!
TOOLS = {"search": search, "calculator": calculator} # registry: tool name -> the function
Next the system prompt teaches the model the ReAct protocol — which tools exist and the exact format to answer in:
SYSTEM = f"""You are a tool-using agent. Answer the user's question by reasoning step by step.
You can use these tools:
- search: look up facts. Usage -> Action: search: <query>
- calculator: evaluate arithmetic. Usage -> Action: calculator: <expression>
Always reply in EXACTLY one of these two forms:
Thought: <your reasoning>
Action: <tool>: <input>
OR, when you have the answer:
Thought: <your reasoning>
Final Answer: <the answer>
""" # the format contract the loop below parses
Now the loop — the actual agent. It calls the model, parses the reply, runs any tool, feeds the result back, and repeats until a Final Answer or a step limit:
def run_agent(question: str, max_steps: int = 5) -> str:
messages = [{"role": "system", "content": SYSTEM}, # the protocol
{"role": "user", "content": question}] # the task
for step in range(max_steps): # BOUND the loop — never let an agent run forever
reply = client.chat.completions.create( # ask the model what to do next
model="gpt-4o-mini", messages=messages, temperature=0,
).choices[0].message.content
messages.append({"role": "assistant", "content": reply}) # record the model's turn
if "Final Answer:" in reply: # STOP condition: the model is done
return reply.split("Final Answer:")[1].strip() # return the text after the marker
m = re.search(r"Action:\s*(\w+):\s*(.+)", reply) # parse "Action: <tool>: <input>"
if not m: # model didn't follow the format…
messages.append({"role": "user",
"content": "Please use the Action or Final Answer format."}) # nudge it back
continue
tool_name, tool_input = m.group(1), m.group(2).strip() # capture groups: tool + input
result = TOOLS.get(tool_name, lambda _: "Unknown tool.")(tool_input) # run the tool (or report unknown)
messages.append({"role": "user", "content": f"Observation: {result}"}) # feed the result back → next loop
return "Stopped: step limit reached." # safety valve if it never finishes
That's a complete agent in ~20 lines. Everything a framework adds — LangChain, LangGraph — is a more robust, featureful version of exactly this loop.
↳ Exercise 1 has you run a single reason→act→observe turn by hand (call the model, parse the Action, run
the tool) so the mechanics are concrete, and Exercise 2 has you assemble the full run_agent loop and
watch it chain two tool calls to answer one question.
Three things that make (or break) an agent
- Tool descriptions are prompts. The model picks tools from their names/descriptions alone. Vague descriptions → wrong tool, wrong args. Write them like you're briefing a new colleague.
- Always bound the loop. A
max_stepslimit (and ideally a cost/time budget) is non-negotiable — a looping agent with no bound is a runaway bill. Every real agent framework enforces one. - Stopping conditions must be explicit. The agent needs an unambiguous "I'm done" signal (here,
Final Answer:). Fuzzy stop logic causes both premature and never-ending runs.
↳ Exercise 3 has you add a third tool and a deliberately vague vs. sharp description of it, and observe how description quality alone changes whether the agent picks it correctly.
When do you actually need an agent?
This is the judgment that separates shipping engineers from demo-builders. Default to a chain; escalate to an agent only when the task requires runtime decisions.
| Use a chain (fixed steps) when… | Use an agent (runtime decisions) when… |
|---|---|
| the steps are known in advance | the number/order of steps depends on intermediate results |
| e.g. RAG Q&A, summarize, classify, extract | e.g. "research this topic and cross-check sources" |
| you want predictable cost & latency | the task genuinely needs to branch, retry, or pick tools dynamically |
| debuggability and reliability are priorities | a fixed pipeline can't express the task |
The cost of an agent is real: more LLM calls (higher latency + spend), non-determinism (harder to test), and new failure modes (loops, wrong tool, bad args). A chain that solves the problem is almost always the better engineering choice. Many "agents" in the wild are chains with extra steps — and would be more reliable if they admitted it.
↳ Exercise 4 has you classify five tasks as chain vs. agent and justify each — the reasoning habit this whole track builds on.
Pitfalls
- Reaching for an agent by default. Most tasks are chains; an unnecessary agent adds cost, latency, and flakiness.
- Unbounded loops. No
max_steps/budget = a runaway agent. Always cap it. - Weak tool descriptions. The model routes on descriptions; vague ones cause wrong-tool/wrong-arg errors.
- No explicit stop signal. Ambiguous "done" logic causes premature or endless runs.
eval()and unguarded tools. Tools act on the world — never exposeeval, shell, or unscoped DB/HTTP to untrusted input. (Tool safety is expanded in the Security track.)
Recap
- An agent = an LLM + tools + a loop; the loop (reason → act → observe → repeat) is what makes it an agent, not the model.
- ReAct interleaves Thought / Action / Observation until a Final Answer — you built the whole thing in ~20 lines, which is all a framework wraps.
- Make agents robust with sharp tool descriptions, a bounded loop (
max_steps/budget), and an explicit stop condition. - The core judgment: default to a chain; use an agent only when steps depend on runtime results — agents cost more, are less predictable, and add failure modes.
- Worked examples map to the exercises: one manual turn (Ex 1), the full
run_agentloop (Ex 2), tool- description quality (Ex 3), and the chain-vs-agent decision (Ex 4).
Agent Fundamentals — Exercises
Use the chat API you set up in the LLM Foundations track. No agent framework — the point is to build the
loop yourself. temperature=0 keeps the format stable while you debug.
Exercise 1 — One reason→act→observe turn, by hand
Define a single search tool and the ReAct system prompt. Call the model once with a question, parse the
Action: line, run the tool, and print the observation. Don't loop yet — just do one turn manually.
Reference solution
import re
def search(q):
return {"pro plan price": "The Pro plan is €49/month."}.get(q.lower().strip(), "No result.")
SYSTEM = ("Tool-using agent. Reply as 'Thought: ...\\nAction: search: <query>' "
"or 'Thought: ...\\nFinal Answer: <answer>'.")
msgs = [{"role":"system","content":SYSTEM},
{"role":"user","content":"What does the Pro plan cost?"}]
reply = client.chat.completions.create(model="gpt-4o-mini", messages=msgs, temperature=0
).choices[0].message.content
print("MODEL:", reply)
m = re.search(r"Action:\s*(\w+):\s*(.+)", reply)
if m:
obs = search(m.group(2))
print("OBSERVATION:", obs) # feed this back next turn (Exercise 2 automates that)
You should see the model emit Action: search: Pro plan price, and your code produce the observation. That
single Reason→Act→Observe cycle is the atom of every agent.
Exercise 2 — The full ReAct loop
Wrap Exercise 1 in a bounded loop: append each observation as a new message and continue until the model
emits Final Answer: or you hit max_steps. Add a calculator tool and ask a question that needs BOTH
tools (e.g. "The Pro plan is billed monthly — what's the annual cost?").
Reference solution
TOOLS = {"search": search, "calculator": lambda e: str(eval(e))}
def run_agent(question, max_steps=5):
msgs = [{"role":"system","content":SYSTEM_WITH_BOTH_TOOLS},
{"role":"user","content":question}]
for _ in range(max_steps):
reply = client.chat.completions.create(model="gpt-4o-mini", messages=msgs, temperature=0
).choices[0].message.content
msgs.append({"role":"assistant","content":reply})
if "Final Answer:" in reply:
return reply.split("Final Answer:")[1].strip()
m = re.search(r"Action:\s*(\w+):\s*(.+)", reply)
if not m:
msgs.append({"role":"user","content":"Use the Action or Final Answer format."}); continue
result = TOOLS.get(m.group(1), lambda _: "Unknown tool.")(m.group(2).strip())
msgs.append({"role":"user","content":f"Observation: {result}"})
return "Stopped: step limit reached."
print(run_agent("The Pro plan is €49/month. What's the annual cost?"))
Watch the transcript: it should search for the price, then calculator to multiply by 12, then give a
Final Answer. Two tools chained in one run — that's the loop deciding steps at runtime, which a fixed chain
couldn't do without you pre-wiring both calls.
Exercise 3 — Tool descriptions are prompts
Add a third tool, weather(city). Run the agent twice: once with a vague description ("gets info"), once
with a sharp one ("returns today's weather for a given city name"). Ask "Should I bring an umbrella in
Paris?" and compare whether the agent picks weather.
Reference solution
The function is identical both times; only the description in the system prompt changes. With "gets info"
the model often fails to route to weather (or picks search), because it selects tools purely from their
descriptions. With the sharp description it reliably calls weather: Paris.
# Vague: "- weather: gets info"
# Sharp: "- weather: returns today's weather for a given city. Usage -> Action: weather: <city>"
Lesson: a tool's description is part of the prompt. Improving retrieval-of-the-right-tool is often just improving the description — no code change. Write them like you're briefing a new colleague on when to use each.
Exercise 4 — Chain or agent?
For each task, decide chain (fixed steps) or agent (runtime decisions) and justify in one line.
- Summarize a support email into 3 bullet points.
- "Research competitor pricing, cross-check across their site and press releases, and flag inconsistencies."
- Classify an incoming ticket into one of 8 categories.
- Answer a question from your docs (retrieve → generate).
- "Book me the cheapest flight, but if none under €200, suggest alternate dates."
Reference solution
- Chain. Single fixed step (summarize). No runtime branching.
- Agent. The number and order of lookups depend on what each source reveals; it must decide what to cross-check next — genuine runtime decisions.
- Chain. One classification call, known inputs/outputs.
- Chain. RAG is retrieve → generate, always the same two steps.
- Agent. It must act (search flights), observe results, and branch (book vs. propose alternates) based on what it finds.
The reflex: known steps → chain; steps that depend on intermediate results → agent. When in doubt, default to the chain — it's cheaper, more predictable, and easier to test.