---
title: "AI Agents"
track: "practical-ai"
difficulty: "intermediate"
estimatedTime: "30 min"
prerequisites: ["practical-ai/rag"]
tags: ["agents", "tool-use", "function-calling", "react", "multi-agent", "planning"]
---

## Introduction

An AI agent is a system that uses a language model as its reasoning engine to decide what actions to take, execute those actions, observe the results, and iterate until a task is complete. While a standard LLM call is a single input-output exchange, an agent operates in a **loop** — thinking, acting, and adapting.

Agents represent a fundamental shift from using LLMs as text generators to using them as autonomous decision-makers. They can browse the web, write and execute code, query databases, send emails, and orchestrate complex multi-step workflows.

## The Agent Loop

Every agent follows the same basic cycle:

```
Observe → Think → Act → Observe → Think → Act → ... → Done
```

1. **Observe** — receive the current state (user query, tool outputs, environment)
2. **Think** — the LLM reasons about what to do next
3. **Act** — execute a tool or produce a final response
4. **Repeat** until the task is complete

```python
def agent_loop(user_query, tools, max_steps=10):
    """Simplified agent loop."""
    messages = [{"role": "user", "content": user_query}]

    for step in range(max_steps):
        # Think: ask the LLM what to do
        response = call_llm(messages, tools=tools)

        # Check if the agent wants to use a tool
        if response.tool_use:
            # Act: execute the tool
            tool_name = response.tool_use.name
            tool_input = response.tool_use.input
            tool_result = execute_tool(tool_name, tool_input)

            # Observe: add tool result to context
            messages.append({"role": "assistant", "content": response})
            messages.append({
                "role": "user",
                "content": [{"type": "tool_result", "content": tool_result}],
            })
        else:
            # Done: the agent produced a final response
            return response.text

    return "Max steps reached without completing the task."
```

## Tool Use and Function Calling

Tools are the agent's hands. They bridge the gap between the LLM's reasoning ability and the real world. Modern APIs support **function calling** — you define tools with schemas, and the model decides when and how to call them.

### Defining Tools

```python
import anthropic

client = anthropic.Anthropic()

# Define tools the agent can use
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a city.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city name, e.g., 'San Francisco'",
                },
            },
            "required": ["city"],
        },
    },
    {
        "name": "search_web",
        "description": "Search the web for current information.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query",
                },
            },
            "required": ["query"],
        },
    },
]

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[{
        "role": "user",
        "content": "What's the weather like in Tokyo today?",
    }],
)

# The model will respond with a tool_use block
for block in response.content:
    if block.type == "tool_use":
        print(f"Tool: {block.name}")
        print(f"Input: {block.input}")
        # Tool: get_weather
        # Input: {"city": "Tokyo"}
```

### Executing Tools and Continuing

```python
def run_agent(user_message, tools, tool_implementations):
    """Complete agent with tool execution."""
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            tools=tools,
            messages=messages,
        )

        # Check if we need to execute tools
        tool_uses = [b for b in response.content if b.type == "tool_use"]

        if not tool_uses:
            # No tool calls — return the text response
            return response.content[0].text

        # Execute each tool and collect results
        messages.append({"role": "assistant", "content": response.content})

        tool_results = []
        for tool_use in tool_uses:
            # Run the actual tool
            result = tool_implementations[tool_use.name](tool_use.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": str(result),
            })

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

<Callout type="tip">
The quality of your tool descriptions is critical. The LLM decides which tool to use based on the name and description. Vague descriptions lead to wrong tool selections. Be specific: instead of "search for information," write "Search a product catalog by name, category, or price range. Returns up to 10 matching products."
</Callout>

## The ReAct Pattern

**ReAct** (Reasoning + Acting) is a foundational pattern where the agent alternates between reasoning (thinking through the problem) and acting (using tools). The reasoning step is explicit in the output.

```
Question: What is the population of the capital of France?

Thought: I need to find the capital of France first, then its population.
Action: search_web("capital of France")
Observation: The capital of France is Paris.

Thought: Now I need the population of Paris.
Action: search_web("population of Paris 2024")
Observation: The population of Paris is approximately 2.1 million.

Thought: I now have the answer.
Answer: The population of the capital of France (Paris) is approximately
2.1 million people.
```

The key insight is that **explicit reasoning improves tool selection and task decomposition**. Without the thought step, agents make more errors in choosing which tool to use and when.

## Planning and Reasoning

More complex agents use structured planning before executing:

### Plan-and-Execute

```python
system_prompt = """You are a research agent. When given a question:

1. PLAN: Break the question into sub-tasks
2. EXECUTE: Complete each sub-task using available tools
3. SYNTHESIZE: Combine results into a final answer

Always start by writing your plan before taking any actions."""
```

### Self-Reflection

Agents can evaluate their own outputs and correct mistakes:

```python
reflection_prompt = """Review your previous answer for:
1. Factual accuracy — are all claims supported by the retrieved data?
2. Completeness — did you address all parts of the question?
3. Coherence — does the answer flow logically?

If you find issues, fix them. If the answer is good, confirm it."""
```

## Building a Simple Agent

Here is a complete, working agent that can do math and look up information:

```python
import anthropic
import json
import math

client = anthropic.Anthropic()

# Tool implementations
def calculate(expression):
    """Safely evaluate a math expression."""
    allowed = {
        "abs": abs, "round": round,
        "min": min, "max": max,
        "sqrt": math.sqrt, "pow": pow,
        "pi": math.pi, "e": math.e,
    }
    try:
        return str(eval(expression, {"__builtins__": {}}, allowed))
    except Exception as e:
        return f"Error: {e}"

def get_current_date():
    """Get today's date."""
    from datetime import date
    return date.today().isoformat()

# Tool definitions for the API
tools = [
    {
        "name": "calculate",
        "description": "Evaluate a mathematical expression. Supports +, -, *, /, sqrt, pow, pi, e.",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "Math expression, e.g. 'sqrt(144) + pow(2, 10)'",
                },
            },
            "required": ["expression"],
        },
    },
    {
        "name": "get_current_date",
        "description": "Get today's date in ISO format (YYYY-MM-DD).",
        "input_schema": {
            "type": "object",
            "properties": {},
        },
    },
]

tool_functions = {
    "calculate": lambda args: calculate(args["expression"]),
    "get_current_date": lambda args: get_current_date(),
}

def run_agent(question):
    """Run the agent to answer a question."""
    messages = [{"role": "user", "content": question}]

    print(f"\nQuestion: {question}")
    print("-" * 50)

    for step in range(5):
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            system="You are a helpful assistant with access to tools. Use them when needed.",
            tools=tools,
            messages=messages,
        )

        tool_uses = [b for b in response.content if b.type == "tool_use"]
        text_blocks = [b for b in response.content if b.type == "text"]

        if text_blocks:
            for block in text_blocks:
                print(f"Agent: {block.text}")

        if not tool_uses:
            return

        messages.append({"role": "assistant", "content": response.content})

        tool_results = []
        for tool_use in tool_uses:
            print(f"  [Tool: {tool_use.name}({json.dumps(tool_use.input)})]")
            result = tool_functions[tool_use.name](tool_use.input)
            print(f"  [Result: {result}]")
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": result,
            })

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

# Try it
run_agent("What is the square root of 729 plus 2 to the power of 8?")
run_agent("What is today's date, and how many days until the end of the year?")
```

## Multi-Agent Systems

Complex tasks benefit from multiple specialized agents working together:

### Common Patterns

1. **Supervisor** — one agent delegates tasks to specialist agents
2. **Pipeline** — agents pass output sequentially (researcher -> writer -> reviewer)
3. **Debate** — multiple agents propose solutions and critique each other
4. **Swarm** — agents independently work on subtasks and merge results

```python
# Simplified multi-agent: Researcher + Writer
def research_agent(topic):
    """Agent that gathers information."""
    return run_agent_with_tools(
        f"Research this topic thoroughly: {topic}",
        tools=[search_web, read_page],
    )

def writer_agent(research, style):
    """Agent that writes based on research."""
    return run_agent_with_tools(
        f"Write a {style} article based on this research:\n{research}",
        tools=[],  # Writer doesn't need tools
    )

# Pipeline execution
research = research_agent("quantum computing breakthroughs 2024")
article = writer_agent(research, style="beginner-friendly blog post")
```

<Callout type="tip">
Start with a single agent and simple tools. Multi-agent systems add complexity — coordinating agents, handling failures, and managing shared state is significantly harder than a single agent loop. Only add agents when a single agent genuinely cannot handle the task.
</Callout>

## Key Takeaways

- Agents use LLMs as reasoning engines in an observe-think-act loop
- Tool use (function calling) lets agents interact with the real world
- The ReAct pattern improves accuracy by alternating explicit reasoning with actions
- Good tool descriptions are essential — the LLM selects tools based on descriptions
- Multi-agent systems are powerful but complex; start with a single agent first
- Always set a maximum step limit to prevent infinite loops

<Quiz question="What is the fundamental difference between a standard LLM API call and an AI agent?">
  <Option>Agents use larger models</Option>
  <Option>Agents are always more accurate</Option>
  <Option correct>An agent operates in a loop, using tools and iterating based on results, while a standard call is a single input-output exchange</Option>
  <Option>Agents do not use language models</Option>
</Quiz>

<Quiz question="Why is the ReAct pattern effective for agent tasks?">
  <Option>It makes the agent run faster</Option>
  <Option correct>Explicit reasoning steps improve tool selection and task decomposition</Option>
  <Option>It eliminates the need for tools</Option>
  <Option>It reduces the number of API calls</Option>
</Quiz>

<ResourceList>
  - [ReAct: Synergizing Reasoning and Acting (Yao et al., 2022)](https://arxiv.org/abs/2210.03629) - The foundational ReAct paper
  - [Anthropic Tool Use Documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) - Official guide for Claude function calling
  - [Building Effective Agents (Anthropic)](https://www.anthropic.com/research/building-effective-agents) - Anthropic's guide to agent design patterns
  - [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) - Framework for building stateful agents
  - [OpenAI Function Calling Guide](https://platform.openai.com/docs/guides/function-calling) - OpenAI's approach to tool use
</ResourceList>
