Building Production AI Chatbots
Building a production-ready chatbot requires more than just connecting to an LLM. You need proper memory management, context handling, streaming responses, error handling, and a great user experience. This guide brings together everything you've learned.
π¬ Chatbot Features
- Memory: Remember conversation context
- Streaming: Real-time token delivery
- System Prompts: Define bot personality
- Error Handling: Graceful failure recovery
- Rate Limiting: Prevent abuse
Complete Chatbot API
A production-ready API route that validates input, builds a prompt, and streams responses.
// app/api/chatbot/route.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
import { LangChainAdapter } from "ai";
export const runtime = 'edge';
// System prompt defines the chatbot's personality
const SYSTEM_PROMPT = `You are a helpful AI assistant for a web development learning platform.
Your role is to:
- Answer questions about JavaScript, React, Next.js, and web development
- Provide code examples when helpful
- Be concise but thorough
- If you don't know something, say so honestly
- Be friendly and encouraging to learners`;
export async function POST(req: Request) {
try {
const { messages } = await req.json();
// Validate input
if (!messages || !Array.isArray(messages)) {
return new Response('Invalid request', { status: 400 });
}
const model = new ChatOpenAI({
modelName: "gpt-4",
temperature: 0.7,
streaming: true,
});
// Build prompt with system message and history
const prompt = ChatPromptTemplate.fromMessages([
["system", SYSTEM_PROMPT],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
const chain = prompt.pipe(model).pipe(new StringOutputParser());
// Convert messages to LangChain format
const history = messages.slice(0, -1).map((m: any) =>
m.role === 'user'
? new HumanMessage(m.content)
: new AIMessage(m.content)
);
const lastMessage = messages[messages.length - 1].content;
// Stream the response
const stream = await chain.stream({
history,
input: lastMessage,
});
return LangChainAdapter.toDataStreamResponse(stream);
} catch (error) {
console.error('Chatbot error:', error);
return new Response('An error occurred', { status: 500 });
}
}
Chatbot with Knowledge Base (RAG)
Add retrieval so the bot answers using your documentation rather than model memory alone.
// app/api/chatbot/rag/route.ts
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
// Initialize vector store with your documentation
let vectorStore: MemoryVectorStore | null = null;
async function getRetriever() {
if (!vectorStore) {
// In production, load from a persistent vector DB
const docs = [
"React is a JavaScript library for building user interfaces...",
"Next.js is a React framework for production...",
// Add your documentation here
];
vectorStore = await MemoryVectorStore.fromTexts(
docs,
docs.map(() => ({})),
new OpenAIEmbeddings()
);
}
return vectorStore.asRetriever({ k: 3 });
}
const SYSTEM_PROMPT = `You are a helpful assistant that answers questions based on the provided context.
If the context doesn't contain relevant information, say "I don't have specific information about that."
Context:
{context}
`;
export async function POST(req: Request) {
const { messages } = await req.json();
const model = new ChatOpenAI({ modelName: "gpt-4", streaming: true });
const retriever = await getRetriever();
const prompt = ChatPromptTemplate.fromMessages([
["system", SYSTEM_PROMPT],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
const history = messages.slice(0, -1).map((m: any) =>
m.role === 'user' ? new HumanMessage(m.content) : new AIMessage(m.content)
);
const lastMessage = messages[messages.length - 1].content;
// Retrieve relevant documents
const docs = await retriever.invoke(lastMessage);
const context = docs.map(d => d.pageContent).join("\n\n");
const chain = prompt.pipe(model).pipe(new StringOutputParser());
const stream = await chain.stream({
context,
history,
input: lastMessage,
});
return new Response(stream as any, {
headers: { 'Content-Type': 'text/plain' },
});
}
Full-Featured Chat Component
A complete UI with toggling, streaming, error handling, and message rendering.
// app/components/Chatbot.tsx
'use client';
import { useChat } from 'ai/react';
import { useState, useRef, useEffect } from 'react';
export default function Chatbot() {
const [isOpen, setIsOpen] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
error,
reload,
stop,
} = useChat({
api: '/api/chatbot',
initialMessages: [
{
id: 'welcome',
role: 'assistant',
content: 'Hi! I\'m your AI assistant. Ask me anything about web development!',
},
],
});
// Auto-scroll to latest message
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<>
{/* Chat Toggle Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="fixed bottom-4 right-4 w-14 h-14 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 z-50"
>
{isOpen ? 'β' : 'π¬'}
</button>
{/* Chat Window */}
{isOpen && (
<div className="fixed bottom-20 right-4 w-96 h-[500px] bg-white rounded-xl shadow-2xl flex flex-col z-50 border">
{/* Header */}
<div className="p-4 bg-blue-600 text-white rounded-t-xl">
<h3 className="font-semibold">AI Assistant</h3>
<p className="text-sm opacity-80">Ask me anything!</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(m => (
<div
key={m.id}
className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] p-3 rounded-lg ${
m.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-800'
}`}
>
<p className="whitespace-pre-wrap text-sm">{m.content}</p>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-100 p-3 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Error */}
{error && (
<div className="px-4 py-2 bg-red-100 text-red-700 text-sm">
Something went wrong.{' '}
<button onClick={reload} className="underline">Try again</button>
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="flex-1 p-2 border rounded-lg text-sm"
disabled={isLoading}
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-4 py-2 bg-red-500 text-white rounded-lg text-sm"
>
Stop
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm disabled:opacity-50"
>
Send
</button>
)}
</div>
</form>
</div>
)}
</>
);
}
Persistent Chat History
Persist conversations in localStorage so users can resume chats across sessions.
// Using localStorage for persistence
import { useChat } from 'ai/react';
import { useEffect, useState } from 'react';
export function usePersistentChat(chatId: string) {
const [initialMessages, setInitialMessages] = useState([]);
// Load messages from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem(`chat-${chatId}`);
if (saved) {
setInitialMessages(JSON.parse(saved));
}
}, [chatId]);
const chatHelpers = useChat({
api: '/api/chatbot',
initialMessages,
id: chatId,
onFinish: (message) => {
// Save after each response
const allMessages = [...chatHelpers.messages, message];
localStorage.setItem(`chat-${chatId}`, JSON.stringify(allMessages));
},
});
return chatHelpers;
}
Rate Limiting
Protect your API from abuse by limiting requests per IP over a time window.
// Simple in-memory rate limiter
const rateLimiter = new Map<string, { count: number; resetTime: number }>();
function isRateLimited(ip: string): boolean {
const now = Date.now();
const windowMs = 60000; // 1 minute
const maxRequests = 20;
const userData = rateLimiter.get(ip);
if (!userData || now > userData.resetTime) {
rateLimiter.set(ip, { count: 1, resetTime: now + windowMs });
return false;
}
if (userData.count >= maxRequests) {
return true;
}
userData.count++;
return false;
}
// In your API route
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') || 'unknown';
if (isRateLimited(ip)) {
return new Response('Too many requests', { status: 429 });
}
// ... rest of handler
}
Multi-Bot System
Use different system prompts to support multiple personalities or task-specific bots.
// Different personalities for different use cases
const BOTS = {
tutor: {
name: 'Learning Tutor',
systemPrompt: `You are a patient coding tutor. Explain concepts clearly,
use simple examples, and encourage the student.`,
},
reviewer: {
name: 'Code Reviewer',
systemPrompt: `You are a senior code reviewer. Analyze code for bugs,
suggest improvements, and explain best practices.`,
},
debug: {
name: 'Debug Helper',
systemPrompt: `You are a debugging expert. Help identify issues,
explain error messages, and suggest fixes step by step.`,
},
};
// app/api/chatbot/[bot]/route.ts
export async function POST(
req: Request,
{ params }: { params: { bot: string } }
) {
const botConfig = BOTS[params.bot as keyof typeof BOTS];
if (!botConfig) {
return new Response('Bot not found', { status: 404 });
}
// Use botConfig.systemPrompt in your chain
// ...
}
π Production Checklist
- β Rate limiting to prevent abuse
- β Input validation and sanitization
- β Error handling with user-friendly messages
- β Content moderation for inappropriate content
- β Logging for debugging and analytics
- β Cost monitoring for API usage
π‘ Key Takeaways
- β’ System prompts define your chatbot's personality
- β’ Streaming provides a better user experience
- β’ Add RAG for domain-specific knowledge
- β’ Implement rate limiting and error handling
- β’ Use persistent storage for chat history
π Learn More
-
LangChain Chatbot Guide β
Official chatbot development patterns.
-
Vercel AI + LangChain β
Best practices for production chatbots.