Construire un chatbot RAG avec Supabase pgvector et Next.js

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Les grands modèles de langage sont impressionnants, mais ils ont une limitation critique : ils ne connaissent que ce sur quoi ils ont été entraînés. Et si vous vouliez un assistant IA qui comprend votre documentation, vos produits ou les connaissances de votre entreprise ?

C'est là que le RAG (Retrieval-Augmented Generation) entre en jeu. Dans ce tutoriel, vous construirez un chatbot capable de répondre aux questions en utilisant vos propres données en combinant l'extension pgvector de Supabase avec les APIs d'OpenAI.

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez :

  • Une application Next.js avec une interface de chat
  • Une base de données Supabase stockant vos documents sous forme d'embeddings vectoriels
  • Une recherche sémantique qui trouve le contenu pertinent basé sur le sens
  • Un chatbot alimenté par RAG qui répond aux questions en utilisant vos données

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 18+ installé
  • Un compte Supabase (le tier gratuit fonctionne)
  • Une clé API OpenAI avec accès aux embeddings et aux modèles de chat
  • Des connaissances de base en React et TypeScript
  • Une familiarité avec Next.js App Router

Comprendre l'architecture

Avant de plonger dans le code, comprenons comment le RAG fonctionne :

Question → Embed Question → Rechercher docs similaires → Augmenter prompt → Réponse LLM
     ↓           ↓                     ↓                      ↓              ↓
"Qu'est-ce que X?"  [0.1, 0.2...]   Trouver top 5 docs    Ajouter contexte  Réponse !
  1. L'utilisateur pose une question en langage naturel
  2. Embed la question en un vecteur (tableau de nombres)
  3. Recherche sémantique trouve les documents avec des vecteurs similaires
  4. Augmenter le prompt en ajoutant les documents récupérés comme contexte
  5. Le LLM génère une réponse basée sur le contexte

La magie est à l'étape 3 : au lieu de faire correspondre des mots-clés, nous trouvons des documents qui sont sémantiquement similaires — même s'ils ne partagent pas les mêmes mots.

Étape 1 : Configuration du projet

Créez un nouveau projet Next.js avec TypeScript :

npx create-next-app@latest rag-chatbot --typescript --tailwind --app --src-dir
cd rag-chatbot

Installez les dépendances requises :

npm install @supabase/supabase-js openai ai

Créez un fichier .env.local avec vos identifiants :

NEXT_PUBLIC_SUPABASE_URL=votre-url-supabase
NEXT_PUBLIC_SUPABASE_ANON_KEY=votre-cle-anon
SUPABASE_SERVICE_ROLE_KEY=votre-cle-service-role
OPENAI_API_KEY=votre-cle-api-openai

Étape 2 : Configurer Supabase avec pgvector

Allez dans votre tableau de bord Supabase et ouvrez l'Éditeur SQL. Exécutez ce qui suit pour activer l'extension pgvector :

-- Activer l'extension pgvector
create extension if not exists vector with schema extensions;

Maintenant créez une table pour stocker vos documents avec leurs embeddings :

-- Créer la table documents
create table documents (
  id bigint primary key generated always as identity,
  content text not null,
  metadata jsonb,
  embedding extensions.vector(1536)
);
 
-- Activer Row Level Security
alter table documents enable row level security;
 
-- Créer une politique pour la lecture
create policy "Autoriser l'accès en lecture public"
  on documents for select
  using (true);
 
-- Créer un index pour une recherche de similarité plus rapide
create index on documents
using hnsw (embedding vector_cosine_ops);

Étape 3 : Créer la fonction d'embedding

Créez un nouveau fichier src/lib/embeddings.ts :

import OpenAI from 'openai';
 
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
 
export async function generateEmbedding(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  });
 
  return response.data[0].embedding;
}

Étape 4 : Construire l'API d'ingestion de documents

Créez src/app/api/ingest/route.ts pour ajouter des documents à votre base de connaissances :

import { createClient } from '@supabase/supabase-js';
import { generateEmbeddings } from '@/lib/embeddings';
import { NextResponse } from 'next/server';
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
 
export async function POST(request: Request) {
  try {
    const { documents } = await request.json();
 
    // Générer les embeddings pour tous les documents
    const contents = documents.map((doc) => doc.content);
    const embeddings = await generateEmbeddings(contents);
 
    // Préparer les données pour l'insertion
    const rows = documents.map((doc, index) => ({
      content: doc.content,
      metadata: doc.metadata || {},
      embedding: embeddings[index],
    }));
 
    // Insérer dans Supabase
    const { data, error } = await supabase
      .from('documents')
      .insert(rows)
      .select('id');
 
    if (error) throw error;
 
    return NextResponse.json({
      success: true,
      inserted: data.length,
    });
  } catch (error) {
    console.error('Erreur d\'ingestion:', error);
    return NextResponse.json(
      { error: 'Échec de l\'ingestion des documents' },
      { status: 500 }
    );
  }
}

Étape 5 : Créer la fonction de recherche sémantique

Créez src/lib/search.ts pour trouver les documents pertinents :

import { createClient } from '@supabase/supabase-js';
import { generateEmbedding } from './embeddings';
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
 
export async function semanticSearch(
  query: string,
  topK: number = 5,
  threshold: number = 0.5
) {
  // Générer l'embedding pour la requête
  const queryEmbedding = await generateEmbedding(query);
 
  // Appeler la fonction RPC de recherche de similarité
  const { data, error } = await supabase.rpc('match_documents', {
    query_embedding: queryEmbedding,
    match_threshold: threshold,
    match_count: topK,
  });
 
  if (error) throw error;
 
  return data;
}

Étape 6 : Construire l'API de chat RAG

Créez src/app/api/chat/route.ts :

import OpenAI from 'openai';
import { semanticSearch } from '@/lib/search';
import { NextResponse } from 'next/server';
 
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
 
export async function POST(request: Request) {
  try {
    const { message, conversationHistory = [] } = await request.json();
 
    // Étape 1 : Rechercher les documents pertinents
    const relevantDocs = await semanticSearch(message, 5, 0.5);
 
    // Étape 2 : Construire le contexte
    const context = relevantDocs
      .map((doc, i) => `[Document ${i + 1}]\n${doc.content}`)
      .join('\n\n');
 
    // Étape 3 : Créer le prompt augmenté
    const systemPrompt = `Vous êtes un assistant utile qui répond aux questions basées sur le contexte fourni.
 
CONTEXTE:
${context || 'Aucun document pertinent trouvé.'}
 
INSTRUCTIONS:
- Répondez à la question de l'utilisateur basé sur le contexte ci-dessus
- Si le contexte ne contient pas d'informations pertinentes, dites-le
- Soyez concis mais complet
- Citez les documents que vous référencez`;
 
    // Étape 4 : Générer la réponse
    const completion = await openai.chat.completions.create({
      model: 'gpt-4-turbo-preview',
      messages: [
        { role: 'system', content: systemPrompt },
        ...conversationHistory,
        { role: 'user', content: message },
      ],
      temperature: 0.7,
      max_tokens: 1000,
    });
 
    return NextResponse.json({
      response: completion.choices[0].message.content,
      sources: relevantDocs.map((doc) => ({
        id: doc.id,
        preview: doc.content.slice(0, 200) + '...',
        similarity: doc.similarity,
      })),
    });
  } catch (error) {
    console.error('Erreur chat:', error);
    return NextResponse.json(
      { error: 'Échec du traitement du chat' },
      { status: 500 }
    );
  }
}

Conclusion

Vous avez construit un chatbot alimenté par RAG qui peut répondre aux questions en utilisant vos propres données. La combinaison de l'extension pgvector de Supabase et des APIs d'OpenAI fournit une base puissante et évolutive pour les applications IA.

Les points clés à retenir :

  • Les embeddings vectoriels permettent une recherche sémantique au-delà de la correspondance de mots-clés
  • pgvector apporte des capacités de recherche vectorielle à PostgreSQL
  • Le RAG ancre les réponses du LLM dans vos données réelles
  • Supabase fournit un backend complet avec une configuration minimale

Ressources :


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur 12 Les Bases de Laravel 11 : Sessions.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes