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:
- Observes — reads the current state (conversation, tool results, memory)
- Reasons — decides what action to take next
- Acts — calls a tool, searches, writes code, or delegates to another agent
- 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.