Hands-On Tutorial

Build Your First Agent in 60 Minutes

Step-by-step: build a research agent that takes a topic, searches the web, and writes a structured summary. Real code. Runs locally.

What We're Building

A research agent. Give it a topic like "quantum computing breakthroughs 2024" and it will search the web for recent articles, extract key information, synthesize findings across multiple sources, and write a structured markdown report — all autonomously.

By the end, you'll have a working agent you can customize, extend, and use as the foundation for any tool-using agent project.

1
Search the web for sources
2
Read article content in full
3
Synthesize across sources
4
Write a structured .md report
Progress: 0/7 steps
STEP 01

Set Up Your Environment

~5 minutes

Before writing any agent code, you need Python and a few dependencies. We'll set up an isolated virtual environment so your agent's packages don't conflict with anything else on your system.

bash — Project setup commands
# Create your project directory
mkdir research-agent && cd research-agent

# Create isolated Python environment
python -m venv venv

# Activate it (Mac/Linux)
source venv/bin/activate

# Activate it (Windows)
# venv\Scripts\activate

# Install all dependencies
pip install anthropic requests beautifulsoup4 python-dotenv

Next, create a .env file in your project root to hold your API key. Never hardcode API keys directly in source files.

# .env — DO NOT commit this file to git
ANTHROPIC_API_KEY=your-api-key-here

Get your API key at console.anthropic.com — click your profile, then "API Keys". New accounts get free credits to start.

Add .env to .gitignore

Run echo ".env" >> .gitignore to make sure you never accidentally commit your API key to GitHub. This is a good habit for every project.

Checkpoint: You should see (venv) at the start of your terminal prompt. If not, re-run the activate command above.

STEP 02

Define Your Tools

~10 minutes

Tools are what make an agent genuinely useful. Without tools, you just have a language model generating text. With tools, the agent can take actions in the real world — in our case, searching the web and reading articles.

We'll give our agent two tools: search_web (uses DuckDuckGo, no API key needed) and read_url (fetches and extracts text from any URL). Create a new file called tools.py.

tools.py
import requests
from bs4 import BeautifulSoup
import json

def search_web(query: str, num_results: int = 5) -> str:
    """Search using DuckDuckGo (no API key needed)"""
    try:
        url = f"https://html.duckduckgo.com/html/?q={query}"
        headers = {"User-Agent": "Mozilla/5.0 (research-agent/1.0)"}
        response = requests.get(url, headers=headers, timeout=10)
        soup = BeautifulSoup(response.text, 'html.parser')

        results = []
        for result in soup.find_all('div', class_='result', limit=num_results):
            title_tag = result.find('a', class_='result__a')
            snippet_tag = result.find('a', class_='result__snippet')
            url_tag = result.find('a', class_='result__url')

            if title_tag:
                results.append({
                    "title": title_tag.get_text(strip=True),
                    "url": url_tag.get_text(strip=True) if url_tag else "",
                    "snippet": snippet_tag.get_text(strip=True) if snippet_tag else ""
                })

        return json.dumps(results, indent=2)
    except Exception as e:
        return f"Search failed: {str(e)}"

def read_url(url: str) -> str:
    """Fetch and extract readable text from a URL"""
    try:
        headers = {"User-Agent": "Mozilla/5.0 (research-agent/1.0)"}
        response = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(response.text, 'html.parser')

        # Remove navigation, footer, and scripts — they're noise
        for tag in soup(['script', 'style', 'nav', 'footer', 'header']):
            tag.decompose()

        text = soup.get_text(separator='\n', strip=True)
        # Limit to 3000 chars — enough content, won't blow the context window
        return text[:3000] + "..." if len(text) > 3000 else text
    except Exception as e:
        return f"Failed to read URL: {str(e)}"

# Tool definitions — these tell the Anthropic API what tools the agent has
TOOL_DEFINITIONS = [
    {
        "name": "search_web",
        "description": "Search the web for current information. Returns titles, URLs, and snippets.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search query"},
                "num_results": {"type": "integer", "description": "Number of results (1-10)", "default": 5}
            },
            "required": ["query"]
        }
    },
    {
        "name": "read_url",
        "description": "Read the content of a specific URL. Use after search_web to get full article content.",
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "The URL to read"}
            },
            "required": ["url"]
        }
    }
]

Checkpoint: Run python -c "from tools import search_web; print(search_web('test')[:200])" — you should see JSON search results, not an error.

STEP 03

Write the Agent Core

~15 minutes

This is the heart of the agent: the loop that sends messages to the model, handles tool calls, feeds results back, and keeps going until the model is done. Create agent.py.

agent.py
import anthropic
import os
from dotenv import load_dotenv
from tools import search_web, read_url, TOOL_DEFINITIONS

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

SYSTEM_PROMPT = """You are a research agent. When given a topic, you:
1. Search for recent, authoritative sources using search_web
2. Read the most relevant ones in full using read_url
3. Synthesize the information from multiple sources
4. Write a comprehensive markdown report with:
   - Executive summary (3-5 sentences)
   - Key findings (bullet points)
   - Main trends and developments
   - Notable quotes or statistics with source attribution
   - Sources section with titles and URLs

Be thorough but concise. Prioritize sources from the last 12 months when possible.
Always read at least 2-3 full articles before writing your report."""

def execute_tool(name: str, inputs: dict) -> str:
    """Route a tool call request to the correct implementation"""
    if name == "search_web":
        return search_web(inputs["query"], inputs.get("num_results", 5))
    elif name == "read_url":
        return read_url(inputs["url"])
    return f"Unknown tool: {name}"

def run_research_agent(topic: str, max_iterations: int = 15) -> str:
    """Run the research agent on a given topic and return a markdown report"""
    print(f"\n Researching: {topic}")
    print("-" * 50)

    # Start conversation with the user's research request
    messages = [{
        "role": "user",
        "content": f"Research this topic and write a comprehensive report: {topic}"
    }]
    iteration = 0

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

        # Send messages to the model
        response = client.messages.create(
            model="claude-opus-4-6",
            max_tokens=4096,
            system=SYSTEM_PROMPT,
            tools=TOOL_DEFINITIONS,
            messages=messages
        )

        print(f"  Stop reason: {response.stop_reason}")

        if response.stop_reason == "end_turn":
            # Model finished — extract the final text response
            for block in response.content:
                if hasattr(block, 'text'):
                    return block.text
            return "Research complete (no text output generated)"

        if response.stop_reason == "tool_use":
            # Model wants to use tools — add its response to history
            messages.append({"role": "assistant", "content": response.content})

            # Execute every tool the model requested
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    print(f"   {block.name}: {str(block.input)[:80]}...")
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })

            # Return all tool results to the model and loop again
            messages.append({"role": "user", "content": tool_results})

    return "Max iterations reached. The partial research above may be incomplete."

How the message history works

Every time the model calls a tool, we append the assistant's response (including the tool call request) to messages, then append the tool results, then send the whole growing list back to the model. This is how the model "remembers" what it has already done in the conversation. Without this, each API call would be stateless — the model would forget it already searched and read articles.

Checkpoint: The file should import without errors. Run python -c "from agent import run_research_agent; print('OK')"

STEP 04

Add Output and Run It

~5 minutes

Create main.py — a simple entry point that takes a topic from the command line, runs the agent, and saves the report to a markdown file.

main.py
import sys
from agent import run_research_agent

def main():
    # Accept topic from command line args or prompt interactively
    if len(sys.argv) < 2:
        topic = input("Enter research topic: ")
    else:
        topic = " ".join(sys.argv[1:])

    # Run the agent
    report = run_research_agent(topic)

    # Save the report as a markdown file
    filename = f"report_{topic[:30].replace(' ', '_')}.md"
    with open(filename, 'w') as f:
        f.write(report)

    print(f"\n Report saved to: {filename}")
    print("\n" + "="*50)
    print(report[:500] + "...")

if __name__ == "__main__":
    main()

Now run it with a research topic:

python main.py "quantum computing breakthroughs 2024"

Your project directory should now look like this:

research-agent/
  venv/ # virtual environment (never commit this)
  tools.py # search_web, read_url
  agent.py # agent loop + system prompt
  main.py # entry point + file saving
  .env # ANTHROPIC_API_KEY (DO NOT commit)
  report_quantum_computing_breakthr....md # output

Checkpoint: You should see iteration logs printed to the terminal, then "Report saved to: report_quantum..." — open the .md file to read the agent's research output.

STEP 05

Understanding What Happened

~5 minutes

Your agent just ran a multi-step research loop autonomously. Here's a real trace showing exactly what a typical run looks like — and what was happening at each step.

Sample terminal output — research run trace
Researching: quantum computing breakthroughs 2024 -------------------------------------------------- [Iteration 1] Stop reason: tool_use search_web: {'query': 'quantum computing breakthroughs 2024', 'num_results': 5} [Iteration 2] Stop reason: tool_use search_web: {'query': 'quantum computing 2024 major advances IBM Google'} [Iteration 3] Stop reason: tool_use read_url: {'url': 'https://www.nature.com/articles/quantum-2024...'} [Iteration 4] Stop reason: tool_use read_url: {'url': 'https://research.google/pubs/quantum-supremacy...'} [Iteration 5] Stop reason: tool_use read_url: {'url': 'https://www.ibm.com/quantum/2024-roadmap-update...'} [Iteration 6] Stop reason: end_turn (model finished — writing final report) Report saved to: report_quantum_computing_breakthr.md

Walk through what the agent decided at each step:

Why max_iterations matters: Without a limit, a confused or stuck agent can loop forever — calling the same tool repeatedly, getting stuck in a cycle, or just not knowing when to stop. Always set a safety ceiling. 15 iterations is generous for most research tasks. Anything over 20 is almost certainly a bug or a runaway agent.

STEP 06

Customize Your Agent

~10 minutes

Your agent works. Now make it yours. Here are three practical customizations with complete code.

Customization 1 — Switch to a Faster (Cheaper) Model

The default claude-opus-4-6 is the most capable model but also the most expensive. For research tasks that don't need maximum depth, claude-haiku-4-5-20251001 is roughly 20x cheaper, completes in half the time, and still produces solid summaries. Edit agent.py:

# In agent.py, change this line:
response = client.messages.create(
    model="claude-haiku-4-5-20251001",  # ~20x cheaper, still capable
    max_tokens=4096,
    ...
)

Use Haiku for: quick summaries, high-volume runs, drafts. Use Opus for: deep research, complex reasoning, final reports.

Customization 2 — Add a Bookmark Tool

Give the agent a save_note tool so it can bookmark important URLs and facts mid-research. This creates a running "research notebook" you can read alongside the final report.

# Add to tools.py
notes = []  # in-memory store for this run

def save_note(content: str, source_url: str = "") -> str:
    """Save an important finding or URL to the research notebook"""
    note = {"content": content, "source": source_url}
    notes.append(note)
    return f"Note saved. Total notes: {len(notes)}"

# Add to TOOL_DEFINITIONS list:
{
    "name": "save_note",
    "description": "Save an important finding, quote, or URL for your research report.",
    "input_schema": {
        "type": "object",
        "properties": {
            "content": {"type": "string", "description": "The finding to save"},
            "source_url": {"type": "string", "description": "URL this came from"}
        },
        "required": ["content"]
    }
}

# In agent.py, add to execute_tool:
elif name == "save_note":
    from tools import save_note
    return save_note(inputs["content"], inputs.get("source_url", ""))

Customization 3 — Domain-Specific System Prompt

The system prompt is the most powerful lever you have. Changing it completely changes the agent's behavior without touching any other code. Here are three variants:

# For academic/technical research:
SYSTEM_PROMPT = """You are a technical research agent. Only use academic papers,
official documentation, and peer-reviewed sources. For each claim, cite the
specific paper or study. Prefer arXiv, Google Scholar, Nature, and IEEE sources.
Ignore news articles, blogs, and opinion pieces."""

# For competitive intelligence:
SYSTEM_PROMPT = """You are a competitive intelligence analyst. Focus on:
product features, pricing pages, job postings (reveals roadmap), press releases,
and customer reviews on G2/Trustpilot. Build a feature comparison table.
Highlight gaps and opportunities for a startup to differentiate."""

# For investment research:
SYSTEM_PROMPT = """You are a financial research agent. Focus on earnings reports,
SEC filings, analyst coverage, and industry reports. Structure your output as:
bull case, bear case, key metrics, and risk factors. Always include the source
and date for every data point — financial claims without citations are useless."""
STEP 07

What to Build Next

~5 minutes

You've got a working research agent. Here are five concrete ways to extend it, each building on what you just built.

💬

Add Slack Notifications

Post the report to a Slack channel when research completes. Use the slack_sdk package and add a notify_slack(report) call at the end of main.py. Great for scheduled overnight research runs.

🌐

Build a Web UI

Wrap the agent in a Flask or Streamlit app. Streamlit takes about 15 lines: a text input for the topic, a run button, and st.markdown(report) to display results. Deploy to Streamlit Cloud for free.

Schedule with Cron

Run python main.py "AI news this week" on a cron schedule to get automated briefings. On Mac/Linux: crontab -e then add 0 8 * * 1 cd /path/to/agent && python main.py "AI news" for Monday 8am digests.

🗃️

Store Past Research

Add SQLite storage to index all past reports. Add a search_past_reports(query) tool so the agent can check what it's already researched before making redundant web searches. Saves time and API cost on repeat topics.

📓

Connect to Notion / Obsidian

Use the Notion API or write to .md files in your Obsidian vault. Add a save_to_notion(title, content) tool so every report lands automatically in your knowledge base, fully searchable and cross-linked.

🔀

Add Multiple Agents

Split the research into parallel agents: one searches competitors, one reads academic papers, one analyzes Reddit discussions. Run all three simultaneously with asyncio.gather() then synthesize. See the Multi-Agent page for the full pattern.

Beyond this kit, here are the most useful next reads:

Design Patterns → Tools Guide → Multi-Agent →

You just built a real AI agent.

It searches the web, reads articles, and writes structured research reports. This same pattern — an agent loop, tools, and a system prompt — is the foundation of every production agent in use today. What you build next is up to you.

Copied!
Steps
0%
Kit Journey
0%