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:
- Tools — functions the LLM can decide to call (fetch weather, query a database, send an email).
- 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
- Python 3.9+
- An OpenAI API key
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
- Python 3.9+
- An Anthropic API key
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:
- Define tools with a name, description, and parameter schema.
- Send the prompt + tool definitions to the LLM.
- Check the response for tool call requests.
- Execute the requested tools and collect results.
- Feed results back to the LLM.
- 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
executefunction 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.