how to build a real ai agent with vercel ai sdk - By Sourav Mishra (@souravvmishra)
a super simple guide to building ai agents that use tools, make decisions, and get real work done in next.js.
most ai chatbot tutorials just show you how to stream text or make a fake weather app.
that's totally useless.
in this guide, i, sourav mishra, will show you how to build a real agent that chains tools together, makes its own decisions, and finishes real tasks.
what makes an agent "agentic"?
a simple bot just answers prompts. an agentic bot does real work. it:
- decides which tools to use on its own.
- executes those tools to do real things.
- chains multiple tools together to finish big tasks.
- adapts based on what the tools return.
the biggest difference? an agent runs in a loop until the job is done.
the setup
we are going to use the vercel ai sdk with next.js. if you don't have a project, run this:
npx create-next-app@latest my-agent --typescript --tailwind --app
cd my-agent
npm install ai @ai-sdk/openai zod
create a .env.local file and drop in your openai key:
OPENAI_API_KEY=sk-your-key-here
building real tools
forget fake tools. let's build tools that actually do things.
1. web search
// lib/tools/search.ts
import { tool } from 'ai';
import { z } from 'zod';
export const searchWeb = tool({
description: 'Search the web for current information on a topic',
inputSchema: z.object({
query: z.string().describe('The search query'),
}),
execute: async ({ query }) => {
const response = await fetch('https://google.serper.dev/search', {
method: 'POST',
headers: {
'X-API-KEY': process.env.SERPER_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ q: query, num: 5 }),
});
const data = await response.json();
return data.organic?.slice(0, 3).map((r: any) => ({
title: r.title,
snippet: r.snippet,
link: r.link,
})) ?? [];
},
});
2. read url content
// lib/tools/read-url.ts
import { tool } from 'ai';
import { z } from 'zod';
export const readUrl = tool({
description: 'Read and extract the main content from a URL',
inputSchema: z.object({
url: z.string().url().describe('The URL to read'),
}),
execute: async ({ url }) => {
const response = await fetch(url);
const html = await response.text();
const textContent = html
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 4000);
return { url, content: textContent };
},
});
3. calculator
// lib/tools/calculate.ts
import { tool } from 'ai';
import { z } from 'zod';
export const calculate = tool({
description: 'Perform mathematical calculations.',
inputSchema: z.object({
expression: z.string().describe('The mathematical expression to evaluate'),
}),
execute: async ({ expression }) => {
try {
const result = Function(`"use strict"; return (${expression})`)();
return { expression, result };
} catch (error) {
return { expression, error: 'Invalid expression' };
}
},
});
4. task manager
// lib/tools/tasks.ts
import { tool } from 'ai';
import { z } from 'zod';
const tasks: { id: string; task: string; dueDate?: string }[] = [];
export const createTask = tool({
description: 'Create a new task or reminder for the user',
inputSchema: z.object({
task: z.string().describe('The task description'),
dueDate: z.string().optional().describe('Optional due date in ISO format'),
}),
execute: async ({ task, dueDate }) => {
const id = crypto.randomUUID();
tasks.push({ id, task, dueDate });
return { success: true, id, task, dueDate };
},
});
export const listTasks = tool({
description: 'List all current tasks',
inputSchema: z.object({}),
execute: async () => {
return { tasks };
},
});
the agent route
now we wire it all up in the api route:
// app/api/chat/route.ts
import { streamText, UIMessage, convertToModelMessages, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { searchWeb } from '@/lib/tools/search';
import { readUrl } from '@/lib/tools/read-url';
import { calculate } from '@/lib/tools/calculate';
import { createTask, listTasks } from '@/lib/tools/tasks';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: `You are a helpful ai assistant. Break tasks down, use tools, and explain your steps clearly.`,
messages: await convertToModelMessages(messages),
stopWhen: stepCountIs(10),
tools: { searchWeb, readUrl, calculate, createTask, listTasks },
});
return result.toUIMessageStreamResponse();
}
stopWhen: stepCountIs(10)is super important. it stops the agent from looping forever and racking up a massive bill.
using the agent class
for a cleaner setup, you can use the ToolLoopAgent class:
// lib/agent.ts
import { ToolLoopAgent } from 'ai';
import { openai } from '@ai-sdk/openai';
import { searchWeb } from './tools/search';
import { readUrl } from './tools/read-url';
import { calculate } from './tools/calculate';
import { createTask, listTasks } from './tools/tasks';
export const researchAgent = new ToolLoopAgent({
model: openai('gpt-4o'),
instructions: `You are a research assistant. Search, read, and summarize info.`,
tools: { searchWeb, readUrl, calculate, createTask, listTasks },
});
then just use it in your route:
// app/api/chat/route.ts
import { createAgentUIStreamResponse } from 'ai';
import { researchAgent } from '@/lib/agent';
export async function POST(request: Request) {
const { messages } = await request.json();
return createAgentUIStreamResponse({ agent: researchAgent, messages });
}
this keeps your code tidy and makes testing way easier.
key takeaways
- use
stopWhen: always put a limit on loops so the agent doesn't run forever. - build real tools: mock data is just for testing. real tools make a real agent.
- use the agent class: it keeps your app clean and modular for production.
- descriptions matter: the llm uses your tool descriptions to decide when to call them. make them super clear.
written by sourav mishra, full stack engineer for next.js and ai.
frequently asked questions
q: what makes a chatbot agentic? it can use real tools to perform multi-step actions on its own, instead of just answering questions.
q: is vercel ai sdk free? yes, the sdk is free. you just pay for the llm you use, like openai.
q: can i use local models? yep! you can use things like ollama, as long as they support tool calling.
q: why use step limits?
agents can sometimes get stuck in loops. stopWhen forces them to stop before costing you tons of money.