Agent Architecture
Before any framework, it is worth knowing what an agent actually is. Strip away the libraries and it is a loop you could write yourself in about forty lines ā and this page shows you those lines.
Once you have seen the loop, LangChain and LangGraph stop looking like magic and start looking like convenience.
What a model can and cannot do
A language model takes text and returns text. That is all.
It cannot look anything up, run anything, remember the previous message, or change anything in the world. It is a brain with no hands and no memory.
Everything else an "AI agent" appears to do is code you wrote around it.
The three pieces
An agent is a model, some tools, and a loop.
| Piece | What it is | Who provides it |
|---|---|---|
| Model | decides what to do next | the provider |
| Tools | Python functions that touch the world | you |
| Loop | run the model, run the tool it chose, feed the result back | you, or a framework |
| State | the messages so far | carried through the loop |
Notice what is missing: there is no separate "brain" component and no planner. The model does the deciding, one step at a time, and the loop keeps handing it the results.
The loop
1. Put the question into the message history
2. Send the history and the tool list to the model
3. Did it ask for a tool?
No -> that is the answer, stop
Yes -> run the tool, append the result, go to 2That is it. The cycle at step 3 is what makes it an agent rather than a single call. A model that answers once is a chatbot; a model that can look at a result and decide to do something else is an agent.
Written out in full
No framework, nothing hidden.
import json
TOOLS = {
"get_ticket": get_ticket,
"search_runbook": search_runbook,
}
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "get_ticket",
"description": "Fetch an incident ticket by its id.",
"parameters": {
"type": "object",
"properties": {"ticket_id": {"type": "integer"}},
"required": ["ticket_id"],
},
},
},
]
MAX_ITERATIONS = 8
def run(question):
messages = [
{"role": "system", "content": "You are an incident triage assistant."},
{"role": "user", "content": question},
]
for step in range(MAX_ITERATIONS):
reply = call_model(messages, tools=TOOL_SCHEMAS)
messages.append(reply)
calls = reply.get("tool_calls")
if not calls:
return reply["content"]
for call in calls:
name = call["function"]["name"]
args = json.loads(call["function"]["arguments"])
try:
result = TOOLS[name](**args)
except Exception as e:
result = f"Error: {e}"
messages.append({
"role": "tool",
"tool_call_id": call["id"],
"content": str(result),
})
return "Stopped: too many steps."Read it once slowly. Every idea in this tutorial is in there:
TOOLSis a dictionary of functions, looked up by nameTOOLS[name](**args)is spreading a dictionary into a callmessagesis a list of dictionaries, growing each turn- the tool schemas are JSON Schema, which Pydantic and type hints generate for you
- the
try/exceptreturns the error as text so the model can correct itself MAX_ITERATIONSstops it running forever
The model never runs anything
This is the point people most often get wrong.
The model does not execute your function. It returns a message saying "I would like to call get_ticket with ticket_id=4471" ā as text, in a structured field. Your code decides whether to run it.
That gap is where everything important lives: validation, permissions, logging, cost control, and refusing to run something dangerous. An agent that runs whatever the model asks for, unchecked, is a security problem rather than a design.
Why the ceiling matters
MAX_ITERATIONS is not defensive decoration. A model that keeps deciding one more lookup would be helpful will keep going, and each turn is a paid API call carrying the entire message history.
Two things grow every turn: the number of calls, and the size of each one. The cost is worse than linear. Cap it, and make the cap a setting you can change without a deploy.
Where memory comes from
There is none, inside the model. The messages list is the memory, and it is passed in full on every single call.
This has consequences you will meet quickly:
- a long conversation eventually exceeds the context window
- every turn re-sends everything, so cost grows with conversation length
- "memory" in a framework means summarising, trimming or storing that list, not something the model retains
What a framework adds
Nothing conceptual. It gives you:
- schema generation ā
@toolbuilds the JSON from your type hints and docstring - the loop, already written and tested
- message handling across different providers
- streaming, retries, tracing as options rather than code
- branching and cycles, which is LangGraph's contribution
The loop above is roughly what create_agent runs for you. Knowing that is what lets you debug it when it misbehaves.
The shapes you will meet
| Shape | What it is | Use it when |
|---|---|---|
| Single call | one prompt, one answer | no outside information needed |
| Chain | fixed steps, in a fixed order | you know the order in advance |
| Agent | the model chooses the order | the right order depends on the answers |
| Multi-agent | several agents, each specialised | one prompt has grown unmanageable |
Move down that table only when you need to. A chain is cheaper, faster and far easier to debug than an agent, and a great deal of production work is a chain that somebody labelled an agent.
Design rules worth keeping
- One job per tool.
get_ticketandupdate_ticket, notmanage_ticket. - Describe tools for the model, not for yourself. The docstring is what it reads to choose.
- Return errors as text. A tool that raises kills the run; a tool that returns "Error: no such ticket" lets the agent recover.
- Log every tool call and its arguments. When it goes wrong, that log is all you have.
- Cap the loop, and cap the cost.
- Never let the model decide something irreversible without a check in your code.
Practise this
- Read the loop above and name the exit conditions. There are two.
- Write the
TOOLSdictionary for two functions and call one by looking it up by name. - Explain out loud why the message list has to be sent again on every turn.
- Decide, for something you would like to automate at work, whether it needs an agent or only a chain.