TechLead
Lesson 8 of 18
7 min read
LangChain

Building AI Chatbots

Create production-ready AI chatbots with context, memory, and streaming responses

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

Continue Learning