Crear chatbots de IA para producción
Construir un chatbot listo para producción requiere más que conectar un LLM. Necesitas buena gestión de memoria, manejo de contexto, respuestas en streaming, manejo de errores, y una gran experiencia de usuario. Esta guía reúne todo lo que has aprendido.
💬 Características del chatbot
- Memoria: Recordar el contexto de la conversación
- Streaming: Entrega de tokens en tiempo real
- Prompts de sistema: Definir la personalidad del bot
- Manejo de errores: Recuperación ante fallos
- Rate limiting: Prevenir abuso
API completa de chatbot
Una ruta API lista para producción que valida entrada, construye un prompt y transmite respuestas.
Este enfoque usa ChatPromptTemplate con un placeholder de historial para mantener el contexto
de la conversación, y LangChainAdapter para convertir el stream en un formato compatible con el cliente.
// 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 con base de conocimiento (RAG)
Añade recuperación para que el bot responda usando tu documentación y no solo la memoria del modelo. Con RAG (Retrieval-Augmented Generation), el chatbot busca documentos relevantes en un vector store y los incluye como contexto en el prompt, mejorando significativamente la precisión de las respuestas.
// 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' },
});
}
Componente de chat completo
Una UI completa con toggle, streaming, manejo de errores y renderizado de mensajes.
El componente utiliza el hook useChat para gestionar el estado de la conversación
y auto-scroll para mantener visible el mensaje más reciente.
// 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>
)}
</>
);
}
Historial de chat persistente
Persiste conversaciones en localStorage para que los usuarios puedan retomarlas en futuras sesiones. Esta técnica carga el historial al montar el componente y lo guarda tras cada interacción, ofreciendo una experiencia de continuidad sin necesidad de backend.
// 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
Protege tu API del abuso limitando solicitudes por IP en una ventana de tiempo.
// 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
}
Sistema multi-bot
Usa diferentes prompts de sistema para soportar múltiples personalidades o bots especializados.
// 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
// ...
}
🔒 Checklist de producción
- ✓ Rate limiting para prevenir abuso
- ✓ Validación y sanitización de entrada
- ✓ Manejo de errores con mensajes amigables
- ✓ Moderación de contenido para contenido inapropiado
- ✓ Logging para depuración y analítica
- ✓ Monitoreo de costos por uso de API
💡 Puntos clave
- • Los prompts de sistema definen la personalidad del chatbot
- • El streaming ofrece una mejor experiencia de usuario
- • Añade RAG para conocimiento específico del dominio
- • Implementa rate limiting y manejo de errores
- • Usa almacenamiento persistente para el historial de chat
📚 Más recursos
-
Guía de chatbots de LangChain →
Patrones oficiales para desarrollar chatbots.
-
Vercel AI + LangChain →
Buenas prácticas para chatbots en producción.