Building an Autonomous AI Agent with Agentic RAG and Next.js

Traditional RAG systems follow a linear pattern: the user enters a question, the database is searched, then an answer is generated. But what if you wanted a smart agent that decides on its own when it needs to search, in which source, and whether it needs more context?
This is Agentic RAG — the paradigm that transforms AI from a mere responder into an autonomous agent that thinks, acts, and learns from its results.
Why Agentic RAG in 2026? As companies increasingly rely on AI agents, the agentic pattern has become the new standard. Instead of fixed pipelines, the agent intelligently decides when to use each tool.
What You Will Learn
By the end of this tutorial, you will be able to:
- Understand the difference between traditional RAG and Agentic RAG
- Build an AI agent with multiple tools using
ToolLoopAgent - Implement vector search as a tool the agent invokes autonomously
- Add computational and analytical tools the agent chooses to use
- Connect everything to a chat interface in Next.js
- Understand agent design patterns for production environments
What is the Difference Between Traditional RAG and Agentic RAG?
Traditional RAG
User question → Search the database → Generate the answer
In this pattern, a search is always performed regardless of the nature of the question. If the user asks "Hello, how are you?" the system will search the database needlessly.
Agentic RAG
User question → Agent thinks → Decides: Do I need to search?
↓ Yes ↓ No
Searches the Responds directly
appropriate source
↓
Do I need additional information?
↓ Yes
Searches again or uses another tool
↓
Generates the final answer
The agent makes intelligent decisions at each step:
- When to search: Only when it needs external information
- Where to search: It picks the appropriate source from multiple sources
- How many times to search: It can perform multiple consecutive searches
- What to do with results: It analyzes, compares, calculates, then responds
Prerequisites
Before starting, make sure you have:
- Node.js 20+ installed on your machine
- OpenAI API key with access to
gpt-4oandtext-embedding-3-smallmodels - Basic knowledge of TypeScript and React
- Familiarity with Next.js App Router
- A code editor (VS Code recommended)
Note: We will use an in-memory vector database to simplify the tutorial. In production, replace it with Supabase pgvector, Pinecone, or any other vector database.
Step 1: Project Setup
Start by creating a new Next.js project with the required dependencies:
npx create-next-app@latest agentic-rag-demo --typescript --tailwind --app --src-dir
cd agentic-rag-demoInstall the AI SDK packages and utilities:
npm install ai @ai-sdk/openai zodCreate a .env.local file to store your API key:
OPENAI_API_KEY=sk-your-api-key-hereStep 2: Build the Vector Knowledge Base
First, we will create a simple vector database layer. Create the file src/lib/vector-store.ts:
import { embed, embedMany, cosineSimilarity } from "ai";
// Stored document type
interface Document {
id: string;
content: string;
embedding: number[];
metadata: {
source: string;
category: string;
};
}
// In-memory vector database
class VectorStore {
private documents: Document[] = [];
// Add documents with embedding generation
async addDocuments(
docs: { content: string; metadata: { source: string; category: string } }[]
) {
const { embeddings } = await embedMany({
model: "openai/text-embedding-3-small",
values: docs.map((d) => d.content),
});
const newDocs: Document[] = docs.map((doc, i) => ({
id: `doc-${Date.now()}-${i}`,
content: doc.content,
embedding: embeddings[i],
metadata: doc.metadata,
}));
this.documents.push(...newDocs);
return newDocs.length;
}
// Semantic similarity search
async search(query: string, topK: number = 3): Promise<Document[]> {
const { embedding } = await embed({
model: "openai/text-embedding-3-small",
value: query,
});
const results = this.documents
.map((doc) => ({
...doc,
similarity: cosineSimilarity(embedding, doc.embedding),
}))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, topK);
return results;
}
// Search with category filtering
async searchByCategory(
query: string,
category: string,
topK: number = 3
): Promise<Document[]> {
const { embedding } = await embed({
model: "openai/text-embedding-3-small",
value: query,
});
const results = this.documents
.filter((doc) => doc.metadata.category === category)
.map((doc) => ({
...doc,
similarity: cosineSimilarity(embedding, doc.embedding),
}))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, topK);
return results;
}
// Get available categories
getCategories(): string[] {
return [...new Set(this.documents.map((d) => d.metadata.category))];
}
get size(): number {
return this.documents.length;
}
}
// Shared singleton instance
export const vectorStore = new VectorStore();Step 3: Populate the Knowledge Base with Sample Data
Create the file src/lib/seed-data.ts to populate the database with data from a fictional company:
import { vectorStore } from "./vector-store";
export async function seedVectorStore() {
// Check if the database is already populated
if (vectorStore.size > 0) return;
const documents = [
// Company policies
{
content:
"Return policy: Customers can return products within 30 days of purchase provided the product is in its original condition with the invoice. Refunds are processed within 5-7 business days to the original payment method.",
metadata: { source: "company-policies", category: "policies" },
},
{
content:
"Shipping policy: Free delivery for orders over 200 dinars. Standard delivery takes 3-5 business days at a cost of 7 dinars. Express delivery within 24 hours is available for 15 dinars.",
metadata: { source: "company-policies", category: "policies" },
},
{
content:
"Warranty policy: All electronic products are covered by a full one-year warranty against manufacturing defects. The warranty does not cover damage resulting from misuse or accidents.",
metadata: { source: "company-policies", category: "policies" },
},
// Product information
{
content:
"ProBook X1 Laptop: 14th Gen Intel Core i7 processor, 16 GB RAM, 512 GB SSD, 14-inch FHD display. Price: 2,500 dinars. Available in colors: Silver, Dark Gray.",
metadata: { source: "product-catalog", category: "products" },
},
{
content:
"ProBook X2 Laptop: Apple M3 Pro processor, 18 GB RAM, 1 TB SSD, 16-inch Liquid Retina display. Price: 4,200 dinars. Available in colors: Silver, Space Black.",
metadata: { source: "product-catalog", category: "products" },
},
{
content:
"AirSound Pro Headphones: Active noise cancellation, Bluetooth 5.3, 30-hour battery life, IPX5 water resistance. Price: 350 dinars.",
metadata: { source: "product-catalog", category: "products" },
},
{
content:
"UltraView 27 Monitor: 27-inch 4K HDR display, 144Hz refresh rate, USB-C support, adjustable stand. Price: 1,800 dinars.",
metadata: { source: "product-catalog", category: "products" },
},
// FAQ
{
content:
"How do I track my order? You can track your order status through the 'My Orders' page in your account, or via the tracking link sent to your email when the order is shipped.",
metadata: { source: "faq", category: "support" },
},
{
content:
"Available payment methods: Credit card (Visa, Mastercard), bank transfer, cash on delivery (available only within Greater Tunis), Flouci.",
metadata: { source: "faq", category: "support" },
},
{
content:
"Customer service hours: Monday to Friday, 9 AM - 6 PM. Saturday 9 AM - 1 PM. You can reach us by phone, email, or live chat.",
metadata: { source: "faq", category: "support" },
},
];
await vectorStore.addDocuments(documents);
console.log(`Loaded ${vectorStore.size} documents into the knowledge base`);
}Step 4: Build the Agent with Its Tools
Here comes the most important part. We will create the agent that owns multiple tools and decides on its own which ones to use. Create the file src/lib/agent.ts:
import { ToolLoopAgent, tool, streamText, generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { vectorStore } from "./vector-store";
// Tool 1: Search the knowledge base
const searchKnowledgeBase = tool({
description:
"Search the company knowledge base for information about products, policies, and support. Use this tool when the user asks about a product, a policy, or needs technical assistance.",
inputSchema: z.object({
query: z
.string()
.describe("Search text - a question or keywords to search for"),
category: z
.enum(["products", "policies", "support"])
.optional()
.describe("Search category to narrow down results"),
}),
execute: async ({ query, category }) => {
const results = category
? await vectorStore.searchByCategory(query, category)
: await vectorStore.search(query);
if (results.length === 0) {
return { found: false, message: "No matching results found." };
}
return {
found: true,
results: results.map((r) => ({
content: r.content,
source: r.metadata.source,
category: r.metadata.category,
})),
};
},
});
// Tool 2: Calculator
const calculator = tool({
description:
"Calculate mathematical expressions. Use this tool to compute prices, discounts, taxes, and shipping costs.",
inputSchema: z.object({
expression: z.string().describe("The mathematical expression to evaluate"),
context: z
.string()
.optional()
.describe("Context of the calculation to clarify the result"),
}),
execute: async ({ expression, context }) => {
try {
// Safe computation of simple mathematical expressions
const sanitized = expression.replace(/[^0-9+\-*/().% ]/g, "");
const result = Function(`"use strict"; return (${sanitized})`)();
return {
expression: sanitized,
result: Number(result.toFixed(2)),
context: context || "Calculation result",
};
} catch {
return { error: "Invalid mathematical expression", expression };
}
},
});
// Tool 3: Compare products
const compareProducts = tool({
description:
"Compare two or more products side by side. Use this tool when the user requests a comparison between products.",
inputSchema: z.object({
productNames: z
.array(z.string())
.min(2)
.describe("Names of the products to compare"),
}),
execute: async ({ productNames }) => {
const results = [];
for (const name of productNames) {
const searchResults = await vectorStore.searchByCategory(
name,
"products",
1
);
if (searchResults.length > 0) {
results.push({
name,
details: searchResults[0].content,
});
}
}
return {
productsFound: results.length,
comparison: results,
};
},
});
// Tool 4: Get available categories
const getAvailableCategories = tool({
description:
"Get the list of available categories in the knowledge base. Use this tool to find out what types of information are available.",
inputSchema: z.object({}),
execute: async () => {
return {
categories: vectorStore.getCategories(),
totalDocuments: vectorStore.size,
};
},
});
// Define agent instructions
const AGENT_INSTRUCTIONS = `You are an intelligent assistant for a Tunisian tech company. You communicate in English in a professional and friendly manner.
Behavior rules:
1. When the user asks about a product, policy, or support, use the knowledge base search tool.
2. If the question requires calculations (prices, discounts, shipping costs), use the calculator.
3. If a product comparison is requested, use the comparison tool.
4. For general questions like greetings or casual conversation, respond directly without using any tools.
5. If you do not find enough information, honestly tell the user and suggest contacting customer service.
6. You can use multiple tools in the same conversation if necessary.
7. Prices are in Tunisian Dinars (TND).
Response style:
- Be concise and helpful
- Use appropriate formatting (lists, numbers) to organize information
- Cite the source when quoting from the knowledge base`;
// Create the agent
export const supportAgent = new ToolLoopAgent({
model: openai("gpt-4o"),
system: AGENT_INSTRUCTIONS,
tools: {
searchKnowledgeBase,
calculator,
compareProducts,
getAvailableCategories,
},
});Notice the fundamental difference: In traditional RAG, you would search the database with every message. Here, the agent decides intelligently: does this question need a search, or can I answer directly?
Step 5: Build the API Endpoint
Create the file src/app/api/chat/route.ts to handle requests:
import { supportAgent } from "@/lib/agent";
import { seedVectorStore } from "@/lib/seed-data";
// Populate the knowledge base on the first request
let isSeeded = false;
export async function POST(req: Request) {
if (!isSeeded) {
await seedVectorStore();
isSeeded = true;
}
const { messages } = await req.json();
const result = await supportAgent.stream({
messages,
});
return result.toDataStreamResponse();
}Step 6: Build the Chat Interface
Create the file src/app/page.tsx for the user interface:
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({
api: "/api/chat",
});
// Example questions to try
const exampleQuestions = [
"What is your return policy?",
"Compare ProBook X1 and ProBook X2",
"How much will ProBook X1 cost with express shipping?",
"Hello, how are you?",
];
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* Header */}
<header className="bg-white border-b px-6 py-4">
<h1 className="text-xl font-bold text-gray-800">
🤖 Agentic RAG Assistant
</h1>
<p className="text-sm text-gray-500">
A smart agent that autonomously decides when and how to search for information
</p>
</header>
{/* Messages area */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{messages.length === 0 && (
<div className="text-center py-12">
<h2 className="text-lg font-semibold text-gray-600 mb-4">
Try one of these questions:
</h2>
<div className="flex flex-wrap justify-center gap-2">
{exampleQuestions.map((q, i) => (
<button
key={i}
onClick={() => {
handleInputChange({
target: { value: q },
} as React.ChangeEvent<HTMLInputElement>);
}}
className="bg-white border rounded-full px-4 py-2 text-sm
text-gray-700 hover:bg-blue-50 hover:border-blue-300
transition-colors"
>
{q}
</button>
))}
</div>
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
message.role === "user"
? "bg-blue-600 text-white"
: "bg-white border text-gray-800"
}`}
>
<div className="whitespace-pre-wrap">{message.content}</div>
{/* Display tool invocations */}
{message.toolInvocations &&
message.toolInvocations.length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-200">
{message.toolInvocations.map((tool, i) => (
<div
key={i}
className="text-xs text-gray-400 flex items-center gap-1"
>
<span>🔧</span>
<span>Used: {tool.toolName}</span>
</div>
))}
</div>
)}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border rounded-2xl px-4 py-3 text-gray-400">
Thinking...
</div>
</div>
)}
</div>
{/* Input field */}
<form
onSubmit={handleSubmit}
className="border-t bg-white p-4"
>
<div className="flex gap-2 max-w-4xl mx-auto">
<input
value={input}
onChange={handleInputChange}
placeholder="Type your question here..."
className="flex-1 border rounded-xl px-4 py-3 focus:outline-none
focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-600 text-white rounded-xl px-6 py-3
hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
Send
</button>
</div>
</form>
</div>
);
}Step 7: Run the Application and Test It
Start the development server:
npm run devOpen your browser at http://localhost:3000 and try these scenarios:
Scenario 1: A Question That Requires a Search
User: What is the return policy?
Agent (uses searchKnowledgeBase tool): You can return products within 30 days of purchase provided the product is in its original condition with the invoice...
Scenario 2: A Question That Does Not Require a Search
User: Hello, how are you?
Agent (responds directly without tools): Hello! I'm doing great, thanks for asking. How can I help you today?
Scenario 3: A Question That Requires Multiple Tools
User: How much will ProBook X1 cost with a 10% discount and express delivery?
Agent (uses searchKnowledgeBase then calculator):
- ProBook X1 price: 2,500 dinars
- Discount (10%): -250 dinars
- Express shipping: +15 dinars
- Total: 2,265 dinars
Scenario 4: Product Comparison
User: Compare ProBook X1 and ProBook X2
Agent (uses compareProducts): Provides a detailed comparison table...
Step 8: Advanced Production Patterns
Pattern 1: Adding Conversation Memory
In production, you need to store conversation context. You can use prepareCall to inject additional context:
const agentWithMemory = new ToolLoopAgent({
model: openai("gpt-4o"),
system: AGENT_INSTRUCTIONS,
tools: {
searchKnowledgeBase,
calculator,
compareProducts,
getAvailableCategories,
},
prepareCall: async ({ messages, ...settings }) => {
// Extract summary from previous conversation
const conversationSummary = summarizeConversation(messages);
return {
...settings,
messages,
instructions: `${AGENT_INSTRUCTIONS}
Previous conversation summary: ${conversationSummary}`,
};
},
});Pattern 2: Handling Multiple Sources
Add search tools for different sources and let the agent choose:
const searchExternalDocs = tool({
description: "Search external technical documentation when the internal knowledge base is not sufficient",
inputSchema: z.object({
query: z.string(),
docType: z.enum(["api-docs", "user-guide", "changelog"]),
}),
execute: async ({ query, docType }) => {
// Connect to a separate vector database for technical docs
const results = await externalVectorStore.search(query, docType);
return results;
},
});Pattern 3: Safety Guardrails
Add a control mechanism to prevent misuse:
const agentWithGuardrails = new ToolLoopAgent({
model: openai("gpt-4o"),
system: `${AGENT_INSTRUCTIONS}
Security constraints:
- Do not disclose sensitive system or infrastructure information
- Do not perform write or delete operations
- If asked for something outside your scope, apologize and suggest contacting the support team`,
tools: { searchKnowledgeBase, calculator },
stopWhen: stepCountIs(10), // Maximum 10 steps to prevent infinite loops
onStepFinish: async ({ step }) => {
// Log each step for monitoring
console.log(`Step ${step.stepNumber}: ${step.toolCalls?.map(t => t.toolName).join(", ") || "text response"}`);
},
});Pattern 4: Advanced Streaming with Tool Status Display
// In route.ts
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await supportAgent.stream({
messages,
onStepFinish: async ({ step }) => {
// Send tool status events to the frontend
if (step.toolCalls) {
for (const toolCall of step.toolCalls) {
console.log(`Tool: ${toolCall.toolName}`, toolCall.args);
}
}
},
});
return result.toDataStreamResponse();
}Troubleshooting
Problem: The Agent Does Not Use Tools
Likely cause: The tool description is not clear enough.
Solution: Make sure the description of each tool clearly explains when it should be used, not just what it does.
// ❌ Bad description
description: "Search the database"
// ✅ Good description
description: "Search the company knowledge base for information about products, policies, and support. Use this tool when the user asks about a product or policy."Problem: The Agent Enters an Infinite Loop
Solution: Use stopWhen: stepCountIs(N) to set a maximum number of steps:
import { stepCountIs } from "ai";
const agent = new ToolLoopAgent({
// ...
stopWhen: stepCountIs(10),
});Problem: Inaccurate Search Results
Solution: Improve embedding quality and add category filtering:
// Instead of a general search
const results = await vectorStore.search(query);
// Use a categorized search
const results = await vectorStore.searchByCategory(query, "products");Next Steps
After mastering this tutorial, you can expand in several directions:
- Real vector database: Replace the in-memory store with Supabase pgvector or Pinecone
- Multi-agent systems: Create an agent handoff system with specialized agents
- MCP Protocol: Convert your tools into an MCP server to share them with Claude Desktop and Cursor
- Monitoring: Add performance and cost tracking using LangSmith or Helicone
- Agent testing: Use evaluation frameworks like TruLens to measure response quality
Conclusion
In this tutorial, we built an AI agent that goes beyond the traditional RAG pattern. Instead of blindly searching with every request, the agent thinks at each step and decides:
- Do I need external information? If not, it responds directly
- Which source should I search? It picks the appropriate category
- Do I need calculations? It uses the calculator
- Do I need a comparison? It uses the comparison tool
- Are the results sufficient? If not, it searches again
This agentic pattern is the future of AI applications — systems that think and act autonomously instead of following predefined steps.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Building AI Agents from Scratch with TypeScript: Master the ReAct Pattern Using the Vercel AI SDK
Learn how to build AI agents from the ground up using TypeScript. This tutorial covers the ReAct pattern, tool calling, multi-step reasoning, and production-ready agent loops with the Vercel AI SDK.

Building a RAG Chatbot with Supabase pgvector and Next.js
Learn to build an AI chatbot that answers questions using your own data. This tutorial covers vector embeddings, semantic search, and RAG with Supabase and Next.js.

Build Your First MCP Server with TypeScript: Tools, Resources, and Prompts
Learn how to build a production-ready MCP server from scratch using TypeScript. This hands-on tutorial covers tools, resources, prompts, stdio transport, and connecting to Claude Desktop and Cursor.