Agentic AI Part 2: Anatomy of an AI Agent: Models, Tools, Instructions and Memory

Agentic AI Part 2 banner: anatomy of an AI agent with model, tools, instructions and memory around the agent loop
PART 2 Agentic AI Series · DevDojo Anatomy of an AI Agent Models, tools, instructions and memory explained with runnable code Modelthe brain Toolsthe hands Instructionsthe job description Memorythe notebook AI Agentloop + runtime Python Node.js No API key needed devdojo.co.in · Beginner friendly · Python + Node.js examples
Series: Agentic AI Part 2 of 30

What you should know first: read Agentic AI Part 1: What Is Agentic AI and How Do AI Agents Work?, where we built our first agent loop. Basic Python or JavaScript is enough. If LLM words like “tokens” and “context window” are new to you, see Generative AI Part 2: Tokens, Context Windows and Model Parameters.

In Part 1 you saw that an AI agent is “an LLM in a loop that can use tools”. That is true, but when you sit down to build one, you quickly ask: which model should I pick? How do I describe my tools? What do I write in the instructions? Where does the agent remember things? This post answers all of that. We will take a close look at the anatomy of an AI agent: the four building blocks every agent is made of (the model, the tools, the instructions and the memory) plus the small runtime loop that connects them. You will build each part in Python and Node.js, run everything without an API key, and then plug the same design into a real LLM.

Quick Summary
  • Every AI agent has four parts: a model (the brain that decides), tools (functions it can call), instructions (its job description) and memory (what it knows so far).
  • A small runtime loop in your code connects them: send instructions + memory + tool list to the model, run the tool it picks, save the result, repeat until it answers or a step limit is hit.
  • A tool = name + description + input schema + a real function. The description is what the model reads, so write it clearly.
  • Good instructions have six sections: role, goal, rules, tool tips, output format and when to stop.
  • Memory comes in three kinds: short-term (chat history), scratchpad (notes for this task) and long-term (saved facts that survive restarts).
Full Agentic AI roadmap (30 parts)

Beginner

  1. What Is Agentic AI and How Do AI Agents Work?
  2. Anatomy of an AI Agent: Models, Tools, Instructions and Memory You are here
  3. Tool Calling for Agents: Letting an LLM Use Your Functions Coming soon
  4. Build Your First AI Agent from Scratch in Python (No Framework) Coming soon
  5. The ReAct Pattern: Reason, Act, Observe in Practice Coming soon
  6. Writing Good Agent Instructions and Tool Descriptions Coming soon
  7. Agent Memory: Short-Term, Long-Term and Scratchpads Coming soon
  8. Planning Agents: Breaking Big Goals into Steps Coming soon

Intermediate

  1. Workflow Patterns: Chaining, Routing and Parallel Steps Coming soon
  2. Model Context Protocol (MCP): Connecting Agents to Tools and Data Coming soon
  3. Agent Frameworks Compared: OpenAI Agents SDK, LangGraph, Claude Agent SDK and More Coming soon
  4. Building Stateful Agents with LangGraph Coming soon
  5. Agentic RAG: Agents That Search, Read and Cite Coming soon
  6. Browser and Computer-Use Agents Coming soon
  7. Coding Agents: How AI Writes, Runs and Fixes Code Coming soon
  8. Human-in-the-Loop: Approvals, Interrupts and Checkpoints Coming soon
  9. Multi-Agent Systems: Handoffs, Supervisors and Teams Coming soon
  10. Agent-to-Agent Communication with the A2A Protocol Coming soon
  11. Guardrails and Structured Outputs for Agents Coming soon
  12. Tracing and Debugging Agents Coming soon

Advanced

  1. Evaluating Agents: Task Success, Trajectories and Test Suites Coming soon
  2. Agent Security: Prompt Injection, Permissions and Sandboxing Coming soon
  3. Controlling Agent Cost and Latency Coming soon
  4. Long-Running Agents: Queues, Retries and Durable Execution Coming soon
  5. Deep Research Agents: Search, Synthesise and Report Coming soon
  6. Reflection and Self-Correcting Agents Coming soon
  7. Voice and Real-Time Agents Coming soon
  8. Deploying Agents to Production: APIs, Serverless and Containers Coming soon
  9. Monitoring and Improving Agents in Production Coming soon
  10. Final Project: Build a Multi-Agent Developer Assistant Coming soon

The anatomy of an AI agent at a glance

Think of an AI agent like a new employee you hire for a support desk. To do the job well, that person needs four things:

1. A brain → the modelThe LLM (for example a GPT, Claude or Gemini model) that reads the situation and decides what to do next.
2. Hands → toolsFunctions the agent can call: look up an order, search the web, run SQL, send an email.
3. A job description → instructionsThe system prompt that says who the agent is, what its goal is and which rules it must follow.
4. A notebook → memoryEverything the agent knows right now: the chat so far, tool results, notes, and saved facts.

None of these parts is an agent on its own. A model without tools can only talk. Tools without a model never run. Without instructions the agent has no focus, and without memory it forgets the result of the tool it called one second ago. The magic happens in your code, which connects the four parts in a loop.

Model (LLM) decides the next step Runtime loop (your code) step limit · error handling · logs Instructions role, goal, rules Tools schemas + functions Memory history, notes, saved facts prompt answer or tool call run tool result save read
The anatomy of an AI agent: four parts, connected by a runtime loop that you write.

Explore the four parts

Click each part to see what it does, what can go wrong, and the one question to ask when you design it.

Job: read the instructions, memory and tool list, then decide: answer now, or call a tool?

Goes wrong when: the model is too small for the task, so it picks the wrong tool or invents arguments.

Design question: “What is the cheapest model that still passes my test cases?”

Job: do real work in the outside world: read data, call APIs, change things.

Goes wrong when: descriptions are vague, two tools overlap, or a tool can do something dangerous with no checks.

Design question: “Could a new team member use this tool correctly from its name and description alone?”

Job: give the agent its identity, goal, rules, tool tips and stopping rule.

Goes wrong when: instructions are a wall of text, contradict each other, or never say when to stop.

Design question: “If the agent breaks a rule, is the rule actually written down clearly?”

Job: keep the chat history, tool results and saved facts the model needs.

Goes wrong when: history grows past the context window, or old, wrong facts are never cleaned up.

Design question: “What must the agent remember for this task, and what can it forget?”

Part 1 of the agent: the model (the brain)

The model is the large language model that makes every decision. In each turn of the loop it receives three things (instructions, memory and the list of tools) and returns either a final answer or a tool call, like “call get_order_status with order_id = A102“. It never runs the tool itself. Your code does that.

How to choose a model for an agent

Agents put more pressure on a model than a simple chat does, because the model must make many correct decisions in a row. Look at these five things:

What to checkWhy it matters for agents
Tool calling supportThe model must return clean, structured tool calls. All major API models (OpenAI, Anthropic, Google, and many open models) support this today.
Reasoning qualityMulti-step tasks need a model that plans well and recovers from errors. Bigger or “reasoning” models do this better.
Context windowEvery tool result is added to memory. Long tasks need a big context window or memory trimming.
Speed (latency)An agent may call the model 5–20 times for one task. A slow model makes the whole agent feel slow.
CostMany calls × many tokens adds up quickly. Small models can be 10× or more cheaper.
Simple rule: start building with a strong model so you know the task is possible. Then write a few test cases and try a smaller, cheaper model. Keep the small one only if it still passes. You can also mix: a small model for easy routing, a strong one for hard reasoning.

Keep the model name in an environment variable (for example OPENAI_MODEL) instead of hard-coding it. Model names change often, and this lets you switch without editing code.

Part 2 of the agent: tools (the hands)

A tool is just a normal function plus a short description that the model can read. Each tool has four pieces:

  1. Name: short and clear, like get_order_status.
  2. Description: one or two sentences saying what it does and when to use it. The model chooses tools mainly by reading this.
  3. Input schema: a JSON Schema describing the arguments (names and types).
  4. Function: the real code your program runs.

Writing JSON schemas by hand gets boring, so let’s build a tiny tool registry. In Python a decorator reads the function’s name, docstring and type hints and builds the schema automatically. The Node.js version registers each tool with its schema and function side by side. Both include a safe call_tool helper that refuses unknown tools.

2 files — agentic-ai-part2 — Visual Studio Code
agentic-ai-part2 › tools.py
# A tiny tool registry: turn normal Python functions into agent tools.
import inspect
import json

TYPE_MAP = {str: "string", int: "integer", float: "number", bool: "boolean"}
REGISTRY = {}

def tool(func):
    """Decorator: read the function's name, docstring and type hints."""
    params = {}
    for name, p in inspect.signature(func).parameters.items():
        params[name] = {"type": TYPE_MAP.get(p.annotation, "string")}
    REGISTRY[func.__name__] = {
        "function": func,
        "schema": {
            "type": "function",
            "name": func.__name__,
            "description": inspect.getdoc(func),
            "parameters": {
                "type": "object",
                "properties": params,
                "required": list(params),
                "additionalProperties": False,
            },
        },
    }
    return func

@tool
def get_order_status(order_id: str):
    """Look up the delivery status of a customer order by its ID, e.g. 'A102'."""
    orders = {"A101": "delivered", "A102": "out for delivery"}
    return orders.get(order_id.upper(), "not found")

@tool
def convert_currency(amount: float, rate: float):
    """Convert an amount using an exchange rate. Returns the converted value."""
    return round(amount * rate, 2)

def call_tool(name, arguments_json):
    """What the agent runtime does when the model asks for a tool."""
    if name not in REGISTRY:
        return f"error: unknown tool '{name}'"
    args = json.loads(arguments_json)
    return REGISTRY[name]["function"](**args)

print(json.dumps(REGISTRY["get_order_status"]["schema"], indent=2))
print(call_tool("get_order_status", '{"order_id": "a102"}'))
print(call_tool("convert_currency", '{"amount": 25, "rate": 83.2}'))
print(call_tool("delete_database", '{}'))
TERMINALOUTPUTPROBLEMSReal output
$ python tools.py
{
  "type": "function",
  "name": "get_order_status",
  "description": "Look up the delivery status of a customer order by its ID, e.g. 'A102'.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string"
      }
    },
    "required": [
      "order_id"
    ],
    "additionalProperties": false
  }
}
out for delivery
2080.0
error: unknown tool 'delete_database'
⎇ mainLn 50, Col 1Spaces: 4UTF-8LFPython
agentic-ai-part2 › tools.mjs
// The same tool registry in Node.js: each tool = schema + function.
const REGISTRY = {};

function tool({ name, description, properties }, fn) {
  REGISTRY[name] = {
    fn,
    schema: {
      type: "function",
      name,
      description,
      parameters: {
        type: "object",
        properties,
        required: Object.keys(properties),
        additionalProperties: false,
      },
    },
  };
}

tool(
  {
    name: "get_order_status",
    description: "Look up the delivery status of a customer order by its ID, e.g. 'A102'.",
    properties: { order_id: { type: "string" } },
  },
  ({ order_id }) => {
    const orders = { A101: "delivered", A102: "out for delivery" };
    return orders[order_id.toUpperCase()] ?? "not found";
  }
);

tool(
  {
    name: "convert_currency",
    description: "Convert an amount using an exchange rate. Returns the converted value.",
    properties: { amount: { type: "number" }, rate: { type: "number" } },
  },
  ({ amount, rate }) => Math.round(amount * rate * 100) / 100
);

function callTool(name, argumentsJson) {
  if (!REGISTRY[name]) return `error: unknown tool '${name}'`;
  return REGISTRY[name].fn(JSON.parse(argumentsJson));
}

console.log(JSON.stringify(REGISTRY.get_order_status.schema, null, 2));
console.log(callTool("get_order_status", '{"order_id": "a102"}'));
console.log(callTool("convert_currency", '{"amount": 25, "rate": 83.2}'));
console.log(callTool("delete_database", "{}"));
TERMINALOUTPUTPROBLEMSReal output
$ node tools.mjs
{
  "type": "function",
  "name": "get_order_status",
  "description": "Look up the delivery status of a customer order by its ID, e.g. 'A102'.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string"
      }
    },
    "required": [
      "order_id"
    ],
    "additionalProperties": false
  }
}
out for delivery
2080
error: unknown tool 'delete_database'
⎇ mainLn 50, Col 1Spaces: 2UTF-8LFJavaScript

Switch between Python and Node.js with the tabs, then press ▶ Run to see the real output.

Notice the last line: when the model asks for a tool that does not exist (delete_database), the runtime returns an error message instead of crashing. That error goes back into memory so the model can correct itself. This is the format OpenAI’s Responses API expects for function tools. See the official function calling guide for all options.

Safety first: a tool can do anything your code can do. Start with read-only tools (lookups, searches). For tools that change things (refunds, emails, deletes) add checks inside the function and, later, human approval. We cover this fully in Part 16 and Part 22.

Part 3 of the agent: instructions (the job description)

The instructions (also called the system prompt) are the first thing the model reads every turn. They are your main way to shape behaviour without changing code. A messy paragraph gives messy results, so use a fixed structure with six short sections:

RoleWho the agent is and who it serves.
GoalWhat “done” looks like.
RulesWhat it must always or never do.
Tool tipsWhen to use each tool.
Output formatLength, tone, structure.
When to stopSo it does not loop forever.
instructions.py — agentic-ai-part2 — Visual Studio Code
agentic-ai-part2 › instructions.py
# Build clear agent instructions from six simple sections.
def build_instructions(role, goal, rules, tool_tips, output_format, stop_when):
    lines = [f"# Role\n{role}", f"# Goal\n{goal}", "# Rules"]
    lines += [f"- {r}" for r in rules]
    lines += ["# How to use tools"] + [f"- {t}" for t in tool_tips]
    lines += [f"# Output format\n{output_format}", f"# When to stop\n{stop_when}"]
    return "\n".join(lines)

INSTRUCTIONS = build_instructions(
    role="You are ShopHelp, a friendly support agent for an Indian online store.",
    goal="Solve the customer's order or refund question in as few steps as possible.",
    rules=[
        "Never guess order details. Always check with a tool first.",
        "Never promise a refund. Explain the policy instead.",
        "If the customer is angry or asks for a human, say you will connect them.",
    ],
    tool_tips=[
        "get_order_status: use when the customer gives an order ID.",
        "get_refund_policy: use for any return or refund question.",
    ],
    output_format="Two or three short sentences in simple English.",
    stop_when="Stop as soon as the question is answered, or after 5 tool calls.",
)
print(INSTRUCTIONS)
print(f"\n({len(INSTRUCTIONS.split())} words)")
TERMINALOUTPUTPROBLEMSReal output
$ python instructions.py
# Role
You are ShopHelp, a friendly support agent for an Indian online store.
# Goal
Solve the customer's order or refund question in as few steps as possible.
# Rules
- Never guess order details. Always check with a tool first.
- Never promise a refund. Explain the policy instead.
- If the customer is angry or asks for a human, say you will connect them.
# How to use tools
- get_order_status: use when the customer gives an order ID.
- get_refund_policy: use for any return or refund question.
# Output format
Two or three short sentences in simple English.
# When to stop
Stop as soon as the question is answered, or after 5 tool calls.

(119 words)
⎇ mainLn 25, Col 1Spaces: 4UTF-8LFPython

Press ▶ Run to print the finished instructions.

About 120 words is plenty for a focused agent. Using headings and bullet points makes the instructions easy for the model to follow and easy for you to edit. If you notice the agent breaking a rule, first check whether the rule is actually written clearly. We go deeper into this in Part 6, “Writing Good Agent Instructions and Tool Descriptions”.

Part 4 of the agent: memory (the notebook)

An LLM has no memory between API calls. Every time you call it, it only knows what you send in that request. So “memory” in an agent is really data your code stores and sends back to the model. There are three useful kinds:

Short-term the conversation user + model messages tool calls and results lives: one chat session Scratchpad working notes plan, progress, findings for the current task lives: one task Long-term saved facts user preferences, profile file, database, vector DB lives: across restarts
Three kinds of agent memory and how long each one lives.

Here is a small, runnable version of all three. Short-term memory keeps only the newest messages so it never overflows the context window. Long-term memory writes to a JSON file, so a brand-new object (like after a restart) still remembers the user’s city.

memory.py — agentic-ai-part2 — Visual Studio Code
agentic-ai-part2 › memory.py
# Three kinds of agent memory in one small file.
import json
from pathlib import Path

class ShortTermMemory:
    """The conversation so far. Trimmed so it fits the context window."""
    def __init__(self, max_messages=6):
        self.messages = []
        self.max_messages = max_messages

    def add(self, role, content):
        self.messages.append({"role": role, "content": content})
        if len(self.messages) > self.max_messages:
            self.messages = self.messages[-self.max_messages:]  # keep newest

class Scratchpad:
    """Working notes for the current task only. Cleared when the task ends."""
    def __init__(self):
        self.notes = {}

class LongTermMemory:
    """Facts that survive restarts, saved in a JSON file."""
    def __init__(self, path="memory.json"):
        self.path = Path(path)
        self.facts = json.loads(self.path.read_text()) if self.path.exists() else {}

    def remember(self, key, value):
        self.facts[key] = value
        self.path.write_text(json.dumps(self.facts, indent=2))

    def recall(self, key):
        return self.facts.get(key, "I don't know yet")

short = ShortTermMemory(max_messages=4)
for i in range(1, 7):
    short.add("user", f"message {i}")
print("Short-term keeps:", [m["content"] for m in short.messages])

pad = Scratchpad()
pad.notes["order_A102"] = "out for delivery"
print("Scratchpad:", pad.notes)

long_term = LongTermMemory()
long_term.remember("user_city", "Pune")
print("Long-term recall:", LongTermMemory().recall("user_city"))  # new object, same file
TERMINALOUTPUTPROBLEMSReal output
$ python memory.py
Short-term keeps: ['message 3', 'message 4', 'message 5', 'message 6']
Scratchpad: {'order_A102': 'out for delivery'}
Long-term recall: Pune
⎇ mainLn 45, Col 1Spaces: 4UTF-8LFPython

Press ▶ Run to see which messages survive trimming and what the long-term memory recalls.

Real-world note: cutting off old messages is the simplest trimming method. Production agents often summarise old messages instead, and store long-term facts in a database or a vector store so they can search them. Part 7 of this series is all about agent memory.

The runtime loop that connects everything

The four parts sit still until something drives them. That “something” is the runtime: the loop in your code. You met it in Part 1. Here is what it does on every step:

  1. Send instructions + memory + tool schemas to the model.
  2. If the model returns a final answer, stop and return it.
  3. If it returns a tool call, check the tool exists, run it, and catch errors.
  4. Add the tool result to memory.
  5. Repeat, but never more than max_steps times.

The step limit, the unknown-tool check and the logging are not “extra”. They are what keeps an agent safe and debuggable. Anthropic’s article Building effective agents gives the same advice: keep the loop simple and make every step visible.

Build an agent with all four parts

Now let’s put the anatomy together in one small Agent class. To keep it free and runnable, the “model” is a fake function with a few if rules. The important thing is its signature: it receives exactly what a real LLM receives (instructions, memory, tools) and returns either a tool call or an answer. In the next section we swap it for a real model without touching the rest.

2 files — agentic-ai-part2 — Visual Studio Code
agentic-ai-part2 › anatomy_agent.py
# All four parts of an agent in one class. Runs without an API key.
from dataclasses import dataclass, field

def get_order_status(order_id):
    return {"A101": "delivered", "A102": "out for delivery"}.get(order_id, "not found")

def get_refund_policy(topic):
    return "Refunds are allowed within 7 days of delivery."

def fake_model(instructions, memory, tools):
    """Stands in for the LLM. A real model reads the same three inputs."""
    last = memory[-1]["content"]
    if memory[-1]["role"] == "user" and "A101" in last:
        return {"tool": "get_order_status", "args": {"order_id": "A101"}}
    if memory[-1]["role"] == "tool" and last == "delivered":
        return {"tool": "get_refund_policy", "args": {"topic": "refund"}}
    if memory[-1]["role"] == "tool":
        return {"answer": f"Your order A101 was delivered. {last}"}
    return {"answer": "Could you share your order ID?"}

@dataclass
class Agent:
    name: str
    model: callable                      # 1. the brain
    instructions: str                    # 2. the job description
    tools: dict                          # 3. the hands
    memory: list = field(default_factory=list)  # 4. the notebook
    max_steps: int = 5                   # safety limit for the loop

    def run(self, user_message):
        self.memory.append({"role": "user", "content": user_message})
        for step in range(1, self.max_steps + 1):
            decision = self.model(self.instructions, self.memory, self.tools)
            if "answer" in decision:
                print(f"Step {step}: ANSWER")
                return decision["answer"]
            name, args = decision["tool"], decision["args"]
            if name not in self.tools:
                result = f"error: unknown tool {name}"
            else:
                result = self.tools[name](**args)
            print(f"Step {step}: {name}({args}) -> {result}")
            self.memory.append({"role": "tool", "content": result})
        return "Stopped: step limit reached"

support_agent = Agent(
    name="ShopHelp",
    model=fake_model,
    instructions="You are ShopHelp, a polite support agent. Use tools for facts.",
    tools={"get_order_status": get_order_status, "get_refund_policy": get_refund_policy},
)
print(support_agent.run("Can I return order A101?"))
print("Memory size:", len(support_agent.memory), "messages")
TERMINALOUTPUTPROBLEMSReal output
$ python anatomy_agent.py
Step 1: get_order_status({'order_id': 'A101'}) -> delivered
Step 2: get_refund_policy({'topic': 'refund'}) -> Refunds are allowed within 7 days of delivery.
Step 3: ANSWER
Your order A101 was delivered. Refunds are allowed within 7 days of delivery.
Memory size: 3 messages
⎇ mainLn 53, Col 1Spaces: 4UTF-8LFPython
agentic-ai-part2 › anatomy-agent.mjs
// All four parts of an agent in one object. Runs without an API key.
const tools = {
  get_order_status: ({ order_id }) =>
    ({ A101: "delivered", A102: "out for delivery" })[order_id] ?? "not found",
  get_refund_policy: () => "Refunds are allowed within 7 days of delivery.",
};

function fakeModel(instructions, memory, tools) {
  // Stands in for the LLM. A real model reads the same three inputs.
  const last = memory.at(-1);
  if (last.role === "user" && last.content.includes("A101"))
    return { tool: "get_order_status", args: { order_id: "A101" } };
  if (last.role === "tool" && last.content === "delivered")
    return { tool: "get_refund_policy", args: { topic: "refund" } };
  if (last.role === "tool")
    return { answer: `Your order A101 was delivered. ${last.content}` };
  return { answer: "Could you share your order ID?" };
}

function createAgent({ name, model, instructions, tools, maxSteps = 5 }) {
  const memory = []; // the notebook
  return {
    name,
    memory,
    run(userMessage) {
      memory.push({ role: "user", content: userMessage });
      for (let step = 1; step <= maxSteps; step++) {
        const decision = model(instructions, memory, tools);
        if (decision.answer) {
          console.log(`Step ${step}: ANSWER`);
          return decision.answer;
        }
        const fn = tools[decision.tool];
        const result = fn ? fn(decision.args) : `error: unknown tool ${decision.tool}`;
        console.log(`Step ${step}: ${decision.tool}(${JSON.stringify(decision.args)}) -> ${result}`);
        memory.push({ role: "tool", content: result });
      }
      return "Stopped: step limit reached";
    },
  };
}

const supportAgent = createAgent({
  name: "ShopHelp",
  model: fakeModel,
  instructions: "You are ShopHelp, a polite support agent. Use tools for facts.",
  tools,
});
console.log(supportAgent.run("Can I return order A101?"));
console.log("Memory size:", supportAgent.memory.length, "messages");
TERMINALOUTPUTPROBLEMSReal output
$ node anatomy-agent.mjs
Step 1: get_order_status({"order_id":"A101"}) -> delivered
Step 2: get_refund_policy({"topic":"refund"}) -> Refunds are allowed within 7 days of delivery.
Step 3: ANSWER
Your order A101 was delivered. Refunds are allowed within 7 days of delivery.
Memory size: 3 messages
⎇ mainLn 50, Col 1Spaces: 2UTF-8LFJavaScript

Both tabs print the same steps. Click ▶ Run on each to compare.

Read the output step by step. The agent checks the order (tool 1), sees it was delivered, looks up the refund policy (tool 2), then answers using both results from memory. Each part of the anatomy has one clear place in the code, which makes it easy to change one part without breaking the others.

Plug in a real LLM

Ready to use a real model? First set up the project. We use the official OpenAI SDK here, but the same four-part design works with any provider.

agentic-ai-part2 — bash
# 1. Create a project folder
$ mkdir agentic-ai-part2 && cd agentic-ai-part2

# 2. Python: virtual environment + OpenAI SDK
$ python -m venv .venv
$ source .venv/bin/activate      # Windows: .venv\Scripts\activate
$ pip install openai python-dotenv

# 3. Save your key and model in .env (never commit this file)
$ echo "OPENAI_API_KEY=sk-your-key" > .env
$ echo "OPENAI_MODEL=gpt-6-luna" >> .env
$ echo ".env" >> .gitignore

Now the real agent. Look for the four numbered comments: model, instructions, tools, memory. The memory list is kept between the two run() calls, so the second question (“what was the order ID?”) can be answered without any tool.

real_agent.py — agentic-ai-part2 — Visual Studio Code
agentic-ai-part2 › real_agent.py
# pip install openai python-dotenv  (needs OPENAI_API_KEY in .env)
import json
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI()
MODEL = os.getenv("OPENAI_MODEL", "gpt-6-luna")          # 1. model

INSTRUCTIONS = (                                          # 2. instructions
    "You are ShopHelp, a friendly support agent for an online store. "
    "Never guess order details: always use a tool. "
    "Answer in two or three short sentences."
)

def get_order_status(order_id: str):                      # 3. tools
    orders = {"A101": "delivered", "A102": "out for delivery"}
    return orders.get(order_id.upper(), "not found")

TOOL_FUNCTIONS = {"get_order_status": get_order_status}
TOOLS = [{
    "type": "function",
    "name": "get_order_status",
    "description": "Get the delivery status of an order by its ID, e.g. A102.",
    "parameters": {
        "type": "object",
        "properties": {"order_id": {"type": "string"}},
        "required": ["order_id"],
        "additionalProperties": False,
    },
    "strict": True,
}]

memory = []                                               # 4. memory

def run(user_message, max_steps=5):
    memory.append({"role": "user", "content": user_message})
    for _ in range(max_steps):
        response = client.responses.create(
            model=MODEL, instructions=INSTRUCTIONS, input=memory, tools=TOOLS
        )
        memory.extend(response.output)                    # remember the reply
        calls = [o for o in response.output if o.type == "function_call"]
        if not calls:
            return response.output_text
        for call in calls:
            result = TOOL_FUNCTIONS[call.name](**json.loads(call.arguments))
            print(f"  tool: {call.name} -> {result}")
            memory.append({"type": "function_call_output",
                           "call_id": call.call_id, "output": str(result)})
    return "Stopped: step limit reached"

print(run("Where is my order A102?"))
print(run("And what was the order ID I asked about?"))     # memory at work
TERMINALOUTPUTPROBLEMSExample output – the wording will vary
$ python real_agent.py
  tool: get_order_status -> out for delivery
Your order A102 is out for delivery, so it should reach you soon.
You asked about order A102.
⎇ mainLn 55, Col 1Spaces: 4UTF-8LFPython

Press ▶ Run to see an example of what this prints. Real answers will be worded a little differently each time.

Why we save response.output into memory: the Responses API needs to see its own earlier tool call next to the function_call_output with the same call_id. If you only save the tool result, you get an error about a missing tool call.

Common errors and fixes

ProblemLikely causeFix
Agent never uses a tool and guesses insteadVague tool description, or instructions don’t say “use tools for facts”Describe when to use the tool and add a rule like “Never guess order details”
Agent calls the wrong toolTwo tools with overlapping descriptionsMerge them, or make each description clearly different
KeyError / crash on an unknown tool nameThe runtime trusts the model blindlyCheck the name against your registry and return an error string
TypeError: unexpected keyword argumentModel sent an argument your function doesn’t acceptUse "strict": true and additionalProperties: false in the schema
“No tool call found for function call output”Tool result saved without the model’s tool-call itemAdd response.output to memory before the outputs
Context length exceeded after many stepsMemory grows foreverTrim or summarise old messages; keep tool results short
Agent loops foreverNo stopping rule and no step limitAdd “When to stop” to instructions and max_steps in code

Best practices for each part

  • Model: keep the name in config, test with a strong model first, then try cheaper ones against the same test cases.
  • Tools: few, focused tools beat many overlapping ones. Return short, useful results (not a 5,000-line JSON dump). Start read-only.
  • Instructions: use the six-section structure, keep it short, and put important rules near the top.
  • Memory: decide what must be remembered and for how long. Never store secrets like passwords or API keys in agent memory.
  • Runtime: always have a step limit, catch tool errors, and log every step so you can see why the agent did something.

Practice task

Your task: extend the Agent class from “Build an agent with all four parts”:

  1. Add a third tool, get_delivery_date(order_id), that returns a date for order A102.
  2. Update the fake model so that the question “When will A102 arrive?” calls get_order_status and then get_delivery_date.
  3. Add a LongTermMemory object (from the memory example) that saves the customer’s last order ID, and print it after the run.
  4. Bonus: set max_steps=1 and check that the agent stops with “Stopped: step limit reached”.

Hint: you only need to change the tools dictionary and the fake model. The loop stays the same. That is the whole point of a clean anatomy.

FAQ: anatomy of an AI agent

What are the main components of an AI agent?

Four parts: a model (LLM) that makes decisions, tools that let it act, instructions that define its role and rules, and memory that holds what it knows. A runtime loop in your code connects them.

Is the system prompt the same as agent instructions?

Yes, mostly. “Instructions” is the name used by many agent SDKs (for example the instructions parameter in OpenAI’s Responses API). It is sent to the model on every step, like a system prompt.

Does the LLM run the tools itself?

No. The model only says which tool to call and with which arguments. Your code runs the function and sends the result back. This keeps you in control.

How many tools should one agent have?

Start with 2–5 focused tools. As the number grows, the model gets more confused about which one to use. When you need many tools, split the work across several agents (Part 17).

What is the difference between short-term and long-term memory?

Short-term memory is the current conversation and tool results, sent with each request. Long-term memory is saved outside the model (file, database or vector store) and loaded when needed, so it survives restarts.

Do I need a vector database for agent memory?

Not at first. A list for short-term memory and a JSON file or normal database table for long-term facts is enough for most beginner projects. Vector search helps when you have lots of notes to search by meaning.

Conclusion

You now know the full anatomy of an AI agent. The model decides, the tools act, the instructions guide and the memory remembers, and a small runtime loop in your code ties them together with a step limit and error handling. You built a tool registry, structured instructions, three kinds of memory and a complete agent in Python and Node.js, then saw how to connect the same design to a real LLM. When an agent misbehaves, you can now ask a clear question: which of the four parts is the problem?

Missed the basics? Go back to Agentic AI Part 1. Want to see a full SDK in action? Read our OpenAI Agents SDK tutorial.

Next part – Agentic AI Part 3: Tool Calling for Agents: Letting an LLM Use Your Functions. We will zoom into the “hands”: how tool calling works under the hood, parallel tool calls, strict schemas, handling tool errors, and returning results the model can actually use.

New posts every day on DevDojo

Follow the full Agentic AI series – 30 parts from “what is an agent” to building a multi-agent developer assistant. Bookmark devdojo.co.in and tell us in the comments which part of the agent anatomy you want to explore first.

Share