AI Agents
Building AI that acts, not just talks — the LLM brain, tools, memory and planning, the ReAct framework, state management and LangGraph.
On this page
From Chatbots to Agents
The Brain, Hands & Instructions
Every agent is built from three pillars:
| Pillar | What it is | Role |
|---|---|---|
| The Brain | The LLM | Reasoning, language understanding, decision-making — the "common sense" that figures out how to solve a problem |
| The Hands | Tools | Capabilities that let the agent interact with the world — search files, run code, call APIs |
| The Instructions | Prompts | The logic defining the agent's purpose, behaviour and workflow |
The Brain — an LLM is a probabilistic prediction engine
The LLM brain is a "next-word predictor", not a sentient mind. It has a training phase (learns patterns by minimising prediction error on huge text data) and a decoding phase (generates text by producing a probability distribution over the next token). Decoding strategies: greedy search (always pick the most likely word) or stochastic sampling (get creative, controlled by temperature/top-p).
Why a "naked" LLM is not enough
Tools & the Agentic Loop
Common tools: web search (real-time info), calculator (precise maths), API requests (weather, stock prices, email), code interpreter (run code, analyse data).
The agentic workflow loop
Example: "Is it raining in London?" → the LLM reasons "I need real-time data" → selects the Weather tool → the system executes it and returns "Rainy, 12°C" → the LLM synthesises "Yes, it is raining in London." The hallmark of a true agent is that moment when the model stops generating text and instead writes a command to call a tool.
from langchain_core.tools import tool
# 1. Define a tool - a normal function with a docstring
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers together."""
return a * b
# 2. Bind the tool to the LLM (the "calculator" is handed over)
llm_with_tools = llm.bind_tools([multiply])
# 3. The LLM does not answer directly - it requests the tool
response = llm_with_tools.invoke("Calculate 50 times 173")
print("Tool call:", response.tool_calls)
Notice the LLM did not compute the answer itself — it recognised it needs the tool and produced a structured tool call. The system then runs multiply(50, 173) = 8650 and feeds the result back.
Memory & Planning
Memory Management
| Type | Analogy | Holds | Lifespan |
|---|---|---|---|
| Short-term memory | Sticky note | Immediate info for the current task (e.g. "flight AA123 on March 15") | The current session/task |
| Long-term memory | Filing cabinet | Permanent info across sessions (e.g. "user prefers aisle seats") | Permanent — stored in vector databases |
Short-term memory handles the "what" of the current task; long-term memory stores the "who" and user preferences for personalised experiences.
Planning & Goal Decomposition
Example — "Plan my vacation" decomposes into: (1) Find flights → (2) Book hotel → (3) List restaurants. Planning makes impossible tasks possible, enables step-by-step progress tracking, and allows error recovery at each step.
Closed vs Open-Source LLMs & Ollama
| Closed Source ("Walled Garden") | Open Source ("Community Park") | |
|---|---|---|
| Examples | GPT (OpenAI), Gemini (Google), Claude | LLaMA (Meta), Gemma (Google), Mistral |
| Pros | Top reasoning benchmarks, managed infrastructure, regular updates | Free to run, privacy (runs locally), customisable & transparent |
| Cons | Paid API, your data leaves your network, "black box" | Needs local hardware (RAM/GPU), manual setup |
Why go open source?
- Complete data privacy — prompts and data never leave your device.
- Security & compliance — essential for HIPAA (healthcare), finance, enterprise secrets.
- Offline capability — works without internet once downloaded.
- Cost control — no per-token API fees; predictable fixed costs.
Ollama — the "MP3 player" for AI models
| Ollama command | What it does |
|---|---|
ollama pull llama4 | Downloads the model to your machine |
ollama run llama4 | Loads the model into memory and starts a chat session (auto-pulls if missing) |
ollama list | Shows all models installed in your library |
ollama rm llama4 | Deletes a model to free disk space |
ollama ps | Shows which models are currently running & using RAM |
ollama pull just downloads the model (like downloading an app). ollama run downloads if needed AND starts the chat (boots up the agent). If you only want the files for later, use pull.
The ReAct Framework
- Think — analyse the situation and decide what action to take.
- Act — execute the chosen action (search, calculate, call an API).
- Observe — review the result and determine the next step.
State Management of AI Agents
State management lets the agent: prevent forgetting previous context, support multi-turn conversations, track progress through complex workflows, and validate inputs.
Types of state (recap)
- Ephemeral — temporary, single-computation values; deleted after use.
- Persistent — survives across steps/sessions (conversation history, preferences); stored via databases/checkpointers.
- Shared — a unified object multiple nodes/agents read and write — crucial for multi-agent collaboration.
Complex Control Flow
Simple chatbots are linear — one straight path. Real agents need complex control flow:
- Branching — choose different paths based on conditions (if price > budget, ask the user; else book).
- Loops / cycles — repeat steps (the ReAct loop; retry a failed tool call).
- Conditional routing — dynamically decide the next node from the current state.
- Human-in-the-loop (HITL) — pause the workflow for human approval before a critical action.
LangGraph: Graphs, Nodes, Edges, State
LangGraph has four core components:
1. State — the shared diary
A typed Python dictionary (TypedDict) passed between every step. Each node reads from it and writes to it, carrying context forward. TypedDict gives type safety and prevents key errors. The add_messages reducer appends new messages instead of overwriting the history.
2. Nodes — the workers
Python functions that perform work. Each node receives the current state, performs an operation (call an LLM, run a tool, compute), and returns updates to the state. Keep each node to a single responsibility.
3. Edges — the logic gates
- Standard edge — a fixed path: Node A → Node B.
- Conditional edge — a dynamic path; a routing function inspects the state and decides which node runs next. Like a GPS that reroutes based on traffic.
4. The StateGraph — the builder
The StateGraph assembles nodes and edges into an executable workflow. Three essential methods: add_node() (register a worker), add_edge() / add_conditional_edges() (connect them), and set_entry_point() (define where execution starts). Finally .compile() turns it into a runnable app.
Building an Agent Graph
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
# State = a typed dictionary shared across all nodes
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # add_messages APPENDS
flight_price: int # other fields OVERWRITE
decision: str
# Node: receives state, returns state updates
def check_price(state: AgentState):
if state["flight_price"] > 500:
return {"decision": "ask_user"}
return {"decision": "book"}
# Routing function for the conditional edge
def route(state: AgentState):
return "ask" if state["decision"] == "ask_user" else "book"
from langgraph.graph import StateGraph, START, END
builder = StateGraph(AgentState)
# 1. Register nodes (workers)
builder.add_node("find_flight", find_flight)
builder.add_node("check_price", check_price)
builder.add_node("book_flight", book_flight)
builder.add_node("ask_user", ask_user)
# 2. Connect with edges
builder.add_edge(START, "find_flight")
builder.add_edge("find_flight", "check_price")
builder.add_conditional_edges("check_price", route,
{"book": "book_flight", "ask": "ask_user"})
builder.add_edge("book_flight", END)
# 3. Compile into a runnable app
app = builder.compile()
result = app.invoke({"flight_price": 0, "decision": "", "messages": []})
from langgraph.prebuilt import ToolNode
from langgraph.graph import StateGraph, START, END
def assistant_node(state): # the "Think" node
return {"messages": [llm_with_tools.invoke(state["messages"])]}
tool_node = ToolNode([multiply]) # the "Act" node
def should_continue(state): # router: loop or stop?
last = state["messages"][-1]
return "tools" if last.tool_calls else END
builder = StateGraph(State)
builder.add_node("assistant", assistant_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", should_continue)
builder.add_edge("tools", "assistant") # loop back -> ReAct cycle
graph = builder.compile()
The edge tools → assistant creates the loop — the agent keeps reasoning and acting until should_continue returns END. That loop is the ReAct cycle implemented as a graph.
MemorySaver) when compiling the graph to save state snapshots. The agent can then recover from crashes, resume multi-turn conversations after a restart, and avoid recomputing completed work.
Agent components and Ollama commands are common exam material.
The key difference between Generative AI and Agentic AI is that Agentic AI:
Generative AI is a "thinker" that produces content. Agentic AI is a "doer" — it reasons, plans, and uses tools to act on the world.
In the "digital intern" framework, what plays the role of the Brain?
The LLM is the Brain (reasoning); Tools are the Hands; Prompts are the Instructions.
Why does an agent need a calculator tool for "What is 2347 × 8563?"
LLMs are linguistic engines, not computational ones — they often give approximate, wrong arithmetic. A calculator tool gives exact results.
When an LLM with tools decides it needs a tool, it produces:
The model pauses generation and emits a tool call (name + arguments). The system runs the tool and feeds the result back to the LLM.
An agent permanently remembering "this user prefers vegetarian meals" is using:
Long-term memory (the "filing cabinet", often a vector DB) stores permanent preferences across sessions. Short-term memory is just for the current task.
Goal decomposition / planning means the agent:
Planning splits a goal (e.g. "plan my vacation") into manageable steps (find flights → book hotel → list restaurants), enabling progress tracking and error recovery.
Which is an open-source / open-weight LLM family?
LLaMA (Meta), Gemma (Google) and Mistral are open-weight models you can run locally. GPT, Gemini and Claude are closed-source/proprietary.
Which Ollama command downloads a model AND immediately starts a chat session?
ollama run loads the model into memory and starts the chat (auto-pulling it first if missing). ollama pull only downloads.
A major reason a hospital might choose an open-source LLM run locally is:
Local open-source models keep prompts and data on-device — essential for HIPAA and other regulations. Closed APIs send data to a third party.
Using LangChain, define a tool that returns the length of a string, and bind it to an LLM.
from langchain_core.tools import tool
@tool
def string_length(text: str) -> int:
"""Return the number of characters in a string."""
return len(text)
# Hand the tool to the LLM
llm_with_tools = llm.bind_tools([string_length])
response = llm_with_tools.invoke("How long is the word 'agent'?")
print(response.tool_calls)
The @tool decorator + docstring tell the LLM what the tool does and when to call it.
List the four main limitations of a "naked" LLM (without tools) and explain how tools address them.
(1) Training cutoff — no real-time info; a web-search tool fetches current data. (2) Hallucinations — it invents facts; tools/RAG return verified data. (3) No actions — it only outputs text; API tools let it send emails, query databases. (4) Poor maths — it predicts tokens, not computes; a calculator tool gives exact results.
Describe the agentic workflow loop for the request "What's the weather in Tokyo?".
User request → "What's the weather in Tokyo?". LLM reasoning → the model recognises it needs real-time data it does not have. Tool selection → it generates a call to the Weather tool with location = Tokyo. Execution → the system runs the tool and returns raw data (e.g. "Sunny, 22°C"). Synthesis → the LLM turns that into a natural reply: "It is currently sunny and 22°C in Tokyo."
The ReAct framework cycles through which three steps?
ReAct = Reason + Act: Think (decide), Act (execute), Observe (review the result) — then think again. (Retrieve→Augment→Generate is RAG.)
The main benefit of the ReAct loop is that the agent can:
Each Observe feeds back into Think, so when an action fails the agent reconsiders and tries a different approach — it self-corrects.
In an AI agent, "state" is best described as:
State is the constantly-updated "order pad" that stores messages, intermediate results and progress so the agent stays coherent.
In LangGraph, a node is:
A node is a worker function: it takes the current state, does an operation, and returns updates. Edges connect nodes.
A conditional edge in LangGraph:
A conditional edge runs a routing function that inspects the state and dynamically picks which node executes next — like a GPS rerouting.
Annotating the messages field with add_messages ensures that new messages are:
add_messages is a reducer that appends new messages, preserving the full conversation history instead of replacing it.
Human-in-the-Loop (HITL) is used to:
HITL inserts a human checkpoint — the workflow pauses for approval before irreversible actions (sending an email, making a purchase).
Why is a graph-based approach better than linear code for AI agents?
Graphs allow loops (ReAct), conditional routing based on state, and explicit, predictable execution that is easy to debug and scale.
Define a LangGraph State TypedDict with a messages list (appended) and a task_status string.
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # appended each step
task_status: str # overwritten each step
The Annotated[..., add_messages] reducer makes messages append; plain fields like task_status are overwritten on update.
Build a minimal LangGraph with a single node chat connected from START to END, and compile it.
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import AIMessage
def chat_node(state):
text = state["messages"][-1].content
return {"messages": [AIMessage(content=f"I heard: {text}")]}
graph = StateGraph(AgentState)
graph.add_node("chat", chat_node)
graph.add_edge(START, "chat")
graph.add_edge("chat", END)
app = graph.compile()
add_node registers the worker, two add_edge calls wire START → chat → END, and .compile() produces a runnable app.
Write a routing function and add a conditional edge so that, after a check node, the graph goes to "approve" if state["amount"] > 1000, otherwise to "auto".
def route(state):
if state["amount"] > 1000:
return "approve" # needs human approval
return "auto" # auto-process
builder.add_conditional_edges(
"check", route,
{"approve": "approve_node", "auto": "auto_node"}
)
The routing function inspects the state and returns a key; the mapping dictionary sends the flow to the matching node.
Name LangGraph's four core components and state the role of each in one line.
State — a typed dictionary (the shared "diary") passed between steps, holding all context. Nodes — Python worker functions that receive state and return updates. Edges — connections defining flow; conditional edges route dynamically based on state. StateGraph — the builder that registers nodes, connects edges and compiles everything into a runnable app.
pull, run, list, rm). ReAct = Think → Act → Observe (loop, enables self-correction). State = the agent's memory "order pad"; also validates inputs. Complex control flow: branching, loops, conditional routing, HITL (pause for human approval). LangGraph: State (typed dict), Nodes (worker functions), Edges (standard or conditional), StateGraph builder → .compile(). add_messages appends history; checkpointing enables recovery.