Skip to main content
AI Agents beginner Lesson 2 of 9

Introduction to AI Agents

Understand what AI agents are, how the ReAct pattern works, and build your first autonomous agent that plans and executes multi-step tasks.

What Is an AI Agent?

An AI agent is a system where an LLM acts as the decision-making core, executing actions in a loop until it completes a goal. Unlike a single LLM call that produces one response, an agent:

  1. Observes — reads the current state (conversation, tool results, memory)
  2. Reasons — decides what action to take next
  3. Acts — calls a tool, searches, writes code, or delegates to another agent
  4. Repeats — until the task is complete or a stopping condition is met

This architecture is what enables AI systems to handle open-ended tasks that require multiple steps, like “research this topic and write a report” or “find the bug in this codebase and fix it.”

The ReAct Pattern

Task: "What is the current population of Tokyo, and how does it compare to New York?"

Thought: I need to find current population data for Tokyo.
Action: search("Tokyo population 2024")
Observation: Tokyo population is approximately 13.96 million (city proper), 37.4 million (metro)

Thought: Now I need New York's population.
Action: search("New York City population 2024")
Observation: NYC population is approximately 8.3 million (city proper), 20.1 million (metro)

Thought: I have both data points. I can now answer the question.
Final Answer: Tokyo's city proper population (13.96M) is 1.68x larger than NYC (8.3M).
Tokyo metro (37.4M) vs NYC metro (20.1M): Tokyo is 1.86x larger.

Your First Agent

import anthropic
import json

client = anthropic.Anthropic()

# ─── Tools available to the agent ─────────────────────────────────────────

tools = [
    {
        "name": "search",
        "description": "Search the web for factual information. Use for current data, facts, and research.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "The search query"}
            },
            "required": ["query"]
        }
    },
    {
        "name": "calculate",
        "description": "Evaluate a mathematical expression. Input must be a valid Python expression.",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {"type": "string", "description": "Python math expression, e.g. '(42 * 1.08) ** 2'"}
            },
            "required": ["expression"]
        }
    },
    {
        "name": "read_file",
        "description": "Read the contents of a file by filename.",
        "input_schema": {
            "type": "object",
            "properties": {
                "filename": {"type": "string"}
            },
            "required": ["filename"]
        }
    },
]

# ─── Tool implementations ─────────────────────────────────────────────────

import math

MOCK_SEARCH_DB = {
    "Tokyo population": "Tokyo city: 13.96 million, Greater Tokyo Area: 37.4 million (2024)",
    "New York population": "New York City: 8.34 million, NYC Metro: 20.1 million (2024)",
    "Python GIL": "The GIL (Global Interpreter Lock) prevents true multi-threading in CPython.",
    "latest GPT model": "OpenAI's latest is GPT-4o (2024), with 128k context window.",
}

def search(query: str) -> str:
    query_lower = query.lower()
    for key, val in MOCK_SEARCH_DB.items():
        if any(kw in query_lower for kw in key.lower().split()):
            return val
    return f"No results found for: {query}"

def calculate(expression: str) -> str:
    # Safety: only allow math operations
    allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
    allowed_names.update({"abs": abs, "round": round, "min": min, "max": max})
    try:
        result = eval(expression, {"__builtins__": {}}, allowed_names)
        return str(result)
    except Exception as e:
        return f"Error: {e}"

def read_file(filename: str) -> str:
    mock_files = {
        "data.txt": "Sales Q1: $120k, Q2: $145k, Q3: $132k, Q4: $167k",
        "config.json": '{"model": "claude-sonnet-4-6", "max_tokens": 1024}',
    }
    return mock_files.get(filename, f"File not found: {filename}")

TOOL_MAP = {"search": search, "calculate": calculate, "read_file": read_file}


# ─── Agent loop ───────────────────────────────────────────────────────────

SYSTEM = """You are a helpful research assistant with access to search, calculation, and file reading tools.

When given a task:
1. Break it into steps
2. Use tools to gather information
3. Calculate or analyze as needed
4. Synthesize a clear, accurate final answer

Always use tools to verify facts rather than relying on your training data."""

MAX_ITERATIONS = 10

def run_agent(task: str, verbose: bool = True) -> str:
    messages = [{"role": "user", "content": task}]
    iteration = 0

    while iteration < MAX_ITERATIONS:
        iteration += 1
        if verbose:
            print(f"\n[Iteration {iteration}]")

        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=SYSTEM,
            tools=tools,
            messages=messages,
        )

        # Final answer — no more tool calls needed
        if response.stop_reason == "end_turn":
            answer = next((b.text for b in response.content if hasattr(b, "text")), "")
            if verbose:
                print(f"Final answer: {answer}")
            return answer

        # Process tool calls
        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    if verbose:
                        print(f"  → {block.name}({json.dumps(block.input)})")
                    result = TOOL_MAP.get(block.name, lambda **kw: "Unknown tool")(**block.input)
                    if verbose:
                        print(f"  ← {result[:100]}...")
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result),
                    })

            messages.append({"role": "user", "content": tool_results})

    return "Agent reached max iterations without completing the task."


# Run the agent
result = run_agent(
    "Find the population of Tokyo and New York, then calculate the ratio of "
    "Tokyo metro to NYC metro population, rounded to 2 decimal places."
)

Agent with Memory

import anthropic
import json
from datetime import datetime

client = anthropic.Anthropic()

class SimpleAgent:
    """An agent with a persistent scratchpad memory."""

    def __init__(self, system_prompt: str, tools: list[dict], tool_map: dict):
        self.system = system_prompt
        self.tools = tools
        self.tool_map = tool_map
        self.memory: list[str] = []      # persistent facts across sessions
        self.messages: list[dict] = []   # current conversation

    def remember(self, fact: str) -> None:
        """Store a fact in long-term memory."""
        self.memory.append(f"[{datetime.now():%Y-%m-%d}] {fact}")

    def _memory_context(self) -> str:
        if not self.memory:
            return ""
        return "## Memory\n" + "\n".join(f"- {m}" for m in self.memory[-10:]) + "\n\n"

    def run(self, task: str) -> str:
        system_with_memory = self._memory_context() + self.system
        self.messages.append({"role": "user", "content": task})

        for _ in range(15):
            resp = client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=2048,
                system=system_with_memory,
                tools=self.tools,
                messages=self.messages,
            )

            if resp.stop_reason == "end_turn":
                answer = next((b.text for b in resp.content if hasattr(b, "text")), "")
                self.messages.append({"role": "assistant", "content": answer})
                return answer

            self.messages.append({"role": "assistant", "content": resp.content})
            results = []
            for block in resp.content:
                if block.type == "tool_use":
                    fn = self.tool_map.get(block.name)
                    result = fn(**block.input) if fn else "Tool not found"
                    results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result),
                    })
            self.messages.append({"role": "user", "content": results})

        return "Max iterations reached."

Frequently Asked Questions

What makes something an 'agent' vs a regular LLM call?
A regular LLM call takes input and produces output — one step. An agent runs in a loop: observe the current state, decide on an action (which may be a tool call), execute the action, observe the result, and repeat until the task is complete. The key distinction is that an agent takes multiple actions autonomously toward a goal.
What is the ReAct pattern?
ReAct (Reason + Act) is the foundational agent pattern: the model alternates between Reasoning (thinking about what to do next) and Acting (calling a tool or taking an action). After each action, the model observes the result and reasons about the next step. This loop continues until the task is complete.