Course
Give your AI a permanent memory of your business. A course for people who use ChatGPT or Claude daily.Compound Context
Feb 17, 2026Tutorials8 min read

How to Build an AI Agent from Scratch: 3 Practical Examples

NBNikolas Barwicki
AI AgentsTool CallingPythonTypeScriptOpenAIAnthropicVercel AI SDKTutorial

AI agents are simpler than you think. An LLM, a few tools, and a loop — that is all you need.

Strip away the buzzwords and an AI agent is just an LLM that can use tools. It receives a prompt, decides whether it needs to call a tool, reads the result, and keeps going until the task is done. That decision loop is what separates an agent from a simple chatbot.

This tutorial walks you through building one from scratch in three different tech stacks: Python with OpenAI, Python with Anthropic Claude, and TypeScript with the Vercel AI SDK. Same concept, three implementations. By the end you will understand the core pattern well enough to build agents with any LLM provider.

What Makes an LLM an "Agent"?

A regular LLM call is stateless. You send a prompt, you get a response. An agent adds two things on top of that:

  1. Tools — functions the LLM can decide to call (fetch weather, query a database, send an email).
  2. A loop — after calling a tool and getting a result, the LLM gets another turn to reason about that result and decide what to do next.

This is sometimes called the ReAct pattern (Reason + Act). The cycle looks like this:

User prompt → LLM thinks → calls tool → gets result → LLM thinks again → ... → final answer

Every agent framework, from LangChain to OpenAI's Agents SDK, is built on this loop. Once you understand it, the frameworks become optional conveniences rather than black boxes.

What You Will Build

Each example creates a simple agent with two tools:

  • get_weather — returns weather data for a given city.
  • search_web — returns search results for a query.

Both tools are stubbed out with fake data so you can run the code without any external API keys beyond the LLM provider. In a real project you would swap these stubs for actual API calls.

Example 1: Python + OpenAI API

This is the most hands-on approach. No framework, no abstraction. You write the loop yourself.

Prerequisites

pip install openai

Define Your Tools

OpenAI expects tools as a list of JSON schema objects. Each one describes a function the model can call.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "The city name"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "Search the web for information",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"}
                },
                "required": ["query"]
            }
        }
    }
]

Implement the Functions

Map each tool name to an actual Python function. In production, these would call real APIs.

import json

def get_weather(city):
    return json.dumps({"city": city, "temperature": "22°C", "condition": "Sunny"})

def search_web(query):
    return json.dumps({"results": [f"Top result for: {query}"]})

available_tools = {
    "get_weather": get_weather,
    "search_web": search_web,
}

Build the Agent Loop

This is the core. The loop calls the API, checks whether the model wants to use a tool, executes it, feeds the result back, and repeats.

from openai import OpenAI

client = OpenAI()

def run_agent(user_message):
    messages = [
        {"role": "system", "content": "You are a helpful assistant. Use your tools when needed."},
        {"role": "user", "content": user_message},
    ]

    while True:
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
        )

        message = response.choices[0].message
        messages.append(message)

        # If no tool calls, the agent is done
        if not message.tool_calls:
            return message.content

        # Execute every tool the model requested
        for tool_call in message.tool_calls:
            fn = available_tools[tool_call.function.name]
            result = fn(**json.loads(tool_call.function.arguments))
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

print(run_agent("What's the weather in Paris and Tokyo?"))

Run this and the model will call get_weather twice (once for each city), receive both results, and compose a final answer using the data.

The key insight: message.tool_calls can contain multiple calls in a single turn. The model parallelizes when it can.

Example 2: Python + Anthropic Claude API

Same pattern, different API shape. Claude uses tool_use and tool_result content blocks instead of OpenAI's function calling format.

Prerequisites

pip install anthropic

Define Your Tools

Anthropic uses input_schema instead of parameters, but the JSON Schema inside is the same.

tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "The city name"}
            },
            "required": ["city"]
        }
    },
    {
        "name": "search_web",
        "description": "Search the web for information",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search query"}
            },
            "required": ["query"]
        }
    }
]

Build the Agent Loop

The structure mirrors the OpenAI example. The differences are surface-level: Claude returns content as a list of blocks, and tool results are sent as a user message with tool_result blocks.

from anthropic import Anthropic
import json

client = Anthropic()

def get_weather(city):
    return json.dumps({"city": city, "temperature": "22°C", "condition": "Sunny"})

def search_web(query):
    return json.dumps({"results": [f"Top result for: {query}"]})

available_tools = {"get_weather": get_weather, "search_web": search_web}

def run_agent(user_message):
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-5-20250929",
            max_tokens=4096,
            system="You are a helpful assistant. Use your tools when needed.",
            tools=tools,
            messages=messages,
        )

        # Find any tool_use blocks in the response
        tool_blocks = [b for b in response.content if b.type == "tool_use"]

        # If no tool calls, return the text response
        if not tool_blocks:
            return next(b.text for b in response.content if b.type == "text")

        # Add the full assistant message to history
        messages.append({"role": "assistant", "content": response.content})

        # Execute tools and send results back
        tool_results = []
        for block in tool_blocks:
            fn = available_tools[block.name]
            result = fn(**block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })

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

print(run_agent("What's the weather in Paris and Tokyo?"))

Compare this side by side with the OpenAI example. The loop logic is identical: call the API, check for tool requests, execute, feed back, repeat. Only the message format changes.

Example 3: TypeScript + Vercel AI SDK

The Vercel AI SDK takes a higher-level approach. Instead of writing the loop yourself, you tell the SDK the maximum number of steps and it handles the rest.

Prerequisites

  • Node.js 18+
  • Any supported LLM API key (OpenAI, Anthropic, Google, etc.)
npm install ai @ai-sdk/openai zod

Define Tools and Run the Agent

The AI SDK uses Zod schemas for tool parameters and includes the execute function directly in the tool definition. The maxSteps parameter controls the loop.

import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const result = await generateText({
  model: openai("gpt-4o"),
  system: "You are a helpful assistant. Use your tools when needed.",
  prompt: "What's the weather in Paris and Tokyo?",
  tools: {
    get_weather: {
      description: "Get the current weather for a city",
      parameters: z.object({
        city: z.string().describe("The city name"),
      }),
      execute: async ({ city }) => ({
        city,
        temperature: "22°C",
        condition: "Sunny",
      }),
    },
    search_web: {
      description: "Search the web for information",
      parameters: z.object({
        query: z.string().describe("Search query"),
      }),
      execute: async ({ query }) => ({
        results: [`Top result for: ${query}`],
      }),
    },
  },
  maxSteps: 10,
});

console.log(result.text);

That is the entire agent. The SDK runs the same loop internally: call the model, check for tool calls, execute them, feed results back, repeat until maxSteps is reached or the model returns a final text response.

You can swap openai("gpt-4o") for anthropic("claude-sonnet-4-5-20250929") or google("gemini-2.5-flash") by installing the corresponding provider package. The tool definitions stay exactly the same.

The Pattern Behind All Three

Look at the three examples side by side. Despite different languages, SDKs, and API formats, every one of them does the same thing:

  1. Define tools with a name, description, and parameter schema.
  2. Send the prompt + tool definitions to the LLM.
  3. Check the response for tool call requests.
  4. Execute the requested tools and collect results.
  5. Feed results back to the LLM.
  6. Repeat until the LLM returns a final text answer.

That is the entire mental model. Every agent framework — LangChain, CrewAI, OpenAI Agents SDK, AutoGen — implements some version of this loop with extra features layered on top. Understanding the raw pattern means you are never locked into a single framework.

Where to Go From Here

You have the foundation. Here is how to build on it:

  • Add real tools. Connect to actual APIs: weather services, databases, file systems, email. The tool interface stays the same, only the execute function changes.
  • Add memory. Right now, your agent forgets everything between runs. Persist the message history to a database or add a vector store for long-term recall.
  • Add guardrails. Limit the maximum number of loop iterations. Validate tool inputs. Add error handling for failed tool calls so the agent can recover gracefully.
  • Connect to external services via MCP. The Model Context Protocol lets you expose any API as a tool that any MCP-compatible AI client can use.
  • Scale to multi-agent systems. Once a single agent isn't enough, split responsibilities across multiple specialized agents that collaborate on complex tasks.

Conclusion

An AI agent is not a mysterious black box. It is an LLM, a set of tool definitions, and a loop. The three examples in this tutorial prove that the pattern is universal across languages and providers.

Pick the stack you are most comfortable with, swap the stub functions for real APIs, and you have a working agent. Start simple, add complexity only when you need it, and remember: the loop is all there is.

Reach 25,000+ AI enthusiasts every month

Promote your AI tool with featured placement, measurable visibility, and referral traffic.

Learn more →