AI Agents

Building Your First AI Agent with Claude: Step by Step

Learn to build a real AI agent using Claude's API and tool use. We'll create a research agent that browses the web, summarizes content, and saves results.

January 5, 202510 min read
Share:

AI agents are programs that use a language model to autonomously complete multi-step tasks. Unlike a single API call that generates text, an agent can decide what actions to take, use tools, observe results, and iterate until the task is complete.

In this tutorial, we'll build a research agent using Claude's tool use API. By the end, you'll have a working agent that can search for information and produce structured summaries.

What We're Building

Our research agent will:

  1. Accept a research query from the user
  2. Search for relevant information (we'll simulate this with a mock search tool)
  3. Read and analyze the results
  4. Produce a structured research brief

This pattern extends to any tool-use scenario: code execution, database queries, API calls, file management.

Prerequisites

  • Node.js 18+ or Python 3.10+
  • An Anthropic API key
  • Basic TypeScript or Python knowledge

Understanding Tool Use

Claude's tool use works through a structured conversation loop:

  1. You define tools (functions Claude can call)
  2. Claude decides if it needs a tool
  3. Claude returns a tool_use block with the tool name and input
  4. You execute the tool and return the result
  5. Claude uses the result and either calls another tool or returns a final answer
User message → Claude → tool_use → You run tool → tool_result → Claude → Final answer

This loop is the core of every AI agent.

Setting Up

npm install @anthropic-ai/sdk

Create a .env file:

ANTHROPIC_API_KEY=your-key-here

Defining Our Tools

First, let's define the tools our agent can use:

import Anthropic from '@anthropic-ai/sdk'
 
const client = new Anthropic()
 
const tools: Anthropic.Tool[] = [
  {
    name: 'search_web',
    description: 'Search the web for information on a topic. Returns a list of relevant results with titles and snippets.',
    input_schema: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'The search query',
        },
        num_results: {
          type: 'number',
          description: 'Number of results to return (1-10)',
          default: 5,
        },
      },
      required: ['query'],
    },
  },
  {
    name: 'read_page',
    description: 'Read the full content of a web page given its URL.',
    input_schema: {
      type: 'object',
      properties: {
        url: {
          type: 'string',
          description: 'The URL to read',
        },
      },
      required: ['url'],
    },
  },
  {
    name: 'save_research',
    description: 'Save the final research brief to a file.',
    input_schema: {
      type: 'object',
      properties: {
        title: { type: 'string' },
        summary: { type: 'string' },
        key_points: {
          type: 'array',
          items: { type: 'string' },
        },
        sources: {
          type: 'array',
          items: { type: 'string' },
        },
      },
      required: ['title', 'summary', 'key_points', 'sources'],
    },
  },
]

Implementing Tool Execution

Now we implement the actual functions that run when Claude calls a tool:

import * as fs from 'fs'
 
// Mock search — in production, use Brave Search API, Tavily, or SerpAPI
function searchWeb(query: string, numResults: number = 5): string {
  const mockResults = [
    {
      title: `${query} — Complete Guide 2025`,
      url: `https://example.com/guide-${query.toLowerCase().replace(/\s+/g, '-')}`,
      snippet: `Comprehensive overview of ${query}. Learn the fundamentals, best practices, and advanced techniques...`,
    },
    {
      title: `Best practices for ${query}`,
      url: `https://docs.example.com/${query.toLowerCase()}`,
      snippet: `Official documentation covering all aspects of ${query} with code examples...`,
    },
    {
      title: `${query} in 2025: What you need to know`,
      url: `https://blog.example.com/${query.toLowerCase()}-2025`,
      snippet: `The latest developments in ${query}. Updated for 2025 with new features and patterns...`,
    },
  ]
 
  return JSON.stringify(mockResults.slice(0, numResults))
}
 
function readPage(url: string): string {
  // Mock page content — in production, use a headless browser or Firecrawl
  return `Content from ${url}:\n\nThis page contains detailed information about the topic.
  Key points include: the fundamentals, advanced usage, and real-world examples.
  The author recommends starting with the basics before moving to advanced concepts.`
}
 
function saveResearch(data: {
  title: string
  summary: string
  key_points: string[]
  sources: string[]
}): string {
  const content = `# ${data.title}\n\n## Summary\n${data.summary}\n\n## Key Points\n${data.key_points.map((p) => `- ${p}`).join('\n')}\n\n## Sources\n${data.sources.map((s) => `- ${s}`).join('\n')}`
 
  fs.writeFileSync('research-brief.md', content)
  return `Research brief saved to research-brief.md`
}
 
// Tool dispatcher
function executeTool(name: string, input: Record<string, any>): string {
  switch (name) {
    case 'search_web':
      return searchWeb(input.query, input.num_results)
    case 'read_page':
      return readPage(input.url)
    case 'save_research':
      return saveResearch(input)
    default:
      return `Unknown tool: ${name}`
  }
}

The Agent Loop

Now the core agent loop — this is where the magic happens:

async function runResearchAgent(query: string): Promise<void> {
  console.log(`\n🔍 Starting research on: "${query}"\n`)
 
  const messages: Anthropic.MessageParam[] = [
    {
      role: 'user',
      content: `Research the following topic and create a comprehensive brief: "${query}".
      Use the search tool to find relevant sources, read the most promising ones,
      and then save a structured research brief with your findings.`,
    },
  ]
 
  let iteration = 0
  const MAX_ITERATIONS = 10
 
  while (iteration < MAX_ITERATIONS) {
    iteration++
    console.log(`\n--- Iteration ${iteration} ---`)
 
    const response = await client.messages.create({
      model: 'claude-opus-4-6',
      max_tokens: 4096,
      tools,
      messages,
    })
 
    console.log(`Stop reason: ${response.stop_reason}`)
 
    // Add assistant's response to the message history
    messages.push({ role: 'assistant', content: response.content })
 
    // If Claude is done, exit the loop
    if (response.stop_reason === 'end_turn') {
      console.log('\n✅ Agent completed task')
      // Extract and print the final text response
      const finalText = response.content
        .filter((block): block is Anthropic.TextBlock => block.type === 'text')
        .map((block) => block.text)
        .join('\n')
      if (finalText) console.log('\nFinal response:', finalText)
      break
    }
 
    // If Claude wants to use tools
    if (response.stop_reason === 'tool_use') {
      const toolResults: Anthropic.ToolResultBlockParam[] = []
 
      for (const block of response.content) {
        if (block.type === 'tool_use') {
          console.log(`\n🔧 Calling tool: ${block.name}`)
          console.log(`   Input: ${JSON.stringify(block.input)}`)
 
          const result = executeTool(block.name, block.input as Record<string, any>)
          console.log(`   Result: ${result.substring(0, 100)}...`)
 
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: result,
          })
        }
      }
 
      // Add tool results back to the conversation
      messages.push({ role: 'user', content: toolResults })
    }
  }
 
  if (iteration >= MAX_ITERATIONS) {
    console.log('\n⚠️ Reached maximum iterations')
  }
}
 
// Run the agent
runResearchAgent('AI agents with Claude')

Running the Agent

npx ts-node agent.ts

You'll see output like:

🔍 Starting research on: "AI agents with Claude"

--- Iteration 1 ---
Stop reason: tool_use
🔧 Calling tool: search_web
   Input: {"query":"AI agents with Claude","num_results":5}
   Result: [{"title":"AI Agents — Complete Guide...

--- Iteration 2 ---
Stop reason: tool_use
🔧 Calling tool: read_page
   Input: {"url":"https://example.com/guide-ai-agents"}
   Result: Content from https://example.com/...

--- Iteration 3 ---
Stop reason: tool_use
🔧 Calling tool: save_research
   Input: {"title":"AI Agents with Claude",...}
   Result: Research brief saved to research-brief.md

--- Iteration 4 ---
Stop reason: end_turn

✅ Agent completed task

Adding a System Prompt

Make your agent more reliable by adding a system prompt:

const response = await client.messages.create({
  model: 'claude-opus-4-6',
  max_tokens: 4096,
  system: `You are a research assistant. Your job is to:
1. Search for information using the search_web tool
2. Read the most relevant pages using read_page
3. Synthesize the information into a structured brief
4. Always save your final brief using save_research
 
Be thorough but efficient. Read 2-3 sources before writing your summary.
Always cite your sources.`,
  tools,
  messages,
})

Production Considerations

Error handling — Tools can fail. Wrap executeTool in try/catch and return error messages instead of throwing:

function executeTool(name: string, input: Record<string, any>): string {
  try {
    // ... tool execution
  } catch (error) {
    return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`
  }
}

Real search integration — Replace the mock search with Tavily (built for AI agents) or Brave Search API.

Real web reading — Use Firecrawl to extract clean markdown from any URL.

Persistence — Store the message history in a database to support long-running agents that can be paused and resumed.

Streaming — For better UX, stream Claude's responses in real-time using client.messages.stream().

Next Steps

You've built a working AI agent. From here:

  1. Add real tools — connect to actual APIs (search, web scraping, databases)
  2. Add memory — store past research to avoid repeating work
  3. Multi-agent — create specialized agents that hand off to each other
  4. Eval — test your agent against a benchmark to measure quality

The agent pattern you learned here — define tools, run the loop, execute calls, return results — scales to any complexity. Every advanced AI system is just a more sophisticated version of this loop.

#ai-agents#claude#anthropic#tool-use#python#typescript
Share:

Enjoyed this article?

Join 2,400+ developers getting weekly insights on Claude Code, React, and AI tools.

No spam. Unsubscribe anytime. By subscribing you agree to our Privacy Policy.