Intelligence Artificielle

RAG : Retrieval-Augmented Generation expliqué

RagEmbeddingsLlmVector-StoreRetrieval-Augmented-GenerationOpenaiPgvectorPineconeChunkingRecherche-SemantiqueIa-GenerativeLangchainKnowledge-Base
RAG : Retrieval-Augmented Generation expliqué

Comprenez et implémentez le RAG pour enrichir vos LLM avec vos données : pipeline d'embeddings, vector stores, retrieval et intégration dans vos apps.

Qu'est-ce que le RAG — architecture complète

RAG (Retrieval-Augmented Generation) enrichit les réponses d'un LLM avec des données externes récupérées dynamiquement. Sans RAG, le modèle est limité à ses données d'entraînement — il hallucine sur des faits récents ou spécifiques à votre domaine.

Flux RAG complet :
  1. Indexation offline — documents découpés en chunks → embeddings → stockés dans le vector store
  2. Requête utilisateur → embedding de la question → recherche par similarité dans le vector store
  3. Reranking — les top-K résultats sont reclassés par pertinence fine
  4. Génération — question + contexte récupéré → prompt → LLM → réponse avec sources citées
Approche Forces Limites Cas d'usage
LLM seul Simple, pas d'infra Hallucinations, données périmées Chat général, créativité
Fine-tuning Modèle spécialisé, rapide Coûteux, pas de MAJ temps réel Style d'écriture, format fixe
RAG Précis, sourcé, données fraîches Latence, complexité infra FAQ, documentation, support
RAG + Fine-tuning Meilleure précision combinée Coût élevé Produits IA production critiques

Chunking et préparation des documents

La qualité du chunking conditionne directement la qualité du RAG. Des chunks trop grands = bruit ; trop petits = perte de contexte.

import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { DirectoryLoader } from 'langchain/document_loaders/fs/directory';

// Charger des documents depuis un dossier
const loader = new DirectoryLoader('./docs', {
    '.pdf': (path) => new PDFLoader(path),
    '.txt': (path) => new TextLoader(path),
    '.md': (path) => new TextLoader(path),
});
const rawDocs = await loader.load();

// Découpage récursif — respecte les structures naturelles (paragraphes, phrases)
const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 800,       // Taille max d'un chunk en caractères
    chunkOverlap: 100,    // Chevauchement pour préserver le contexte entre chunks
    separators: ['\n\n', '\n', '. ', ' ', ''], // Ordre de préférence des séparateurs
});

const chunks = await splitter.splitDocuments(rawDocs);

// Enrichir les métadonnées pour le filtrage
const enrichedChunks = chunks.map((chunk, i) => ({
    ...chunk,
    metadata: {
        ...chunk.metadata,
        chunkIndex: i,
        processedAt: new Date().toISOString(),
        wordCount: chunk.pageContent.split(' ').length,
    }
}));

console.log(`${rawDocs.length} documents → ${enrichedChunks.length} chunks`);
Règles pratiques : Pour de la documentation technique : chunk de 500-800 caractères avec overlap 100-150. Pour des textes narratifs : chunk de 1000-1500 avec overlap 200. Toujours inclure des métadonnées (source, page, date) pour pouvoir filtrer et citer.

Embeddings et modèles disponibles

Modèle Dimensions Prix (1M tokens) Usage recommandé
OpenAI text-embedding-3-small1536$0.02Production, bon rapport qualité/prix
OpenAI text-embedding-3-large3072$0.13Haute précision, contenu complexe
Cohere embed-v31024$0.10Multilingue, performant en français
Mistral mistral-embed1024$0.10Souveraineté européenne, français
Local nomic-embed-text768GratuitDev/test, données sensibles
import { OpenAIEmbeddings } from '@langchain/openai';
import { CohereEmbeddings } from '@langchain/cohere';

// OpenAI embeddings (recommandé en prod)
const openaiEmbeddings = new OpenAIEmbeddings({
    model: 'text-embedding-3-small',
    dimensions: 1536, // Peut être réduit pour les petits projets (ex: 512)
});

// Cohere — meilleur pour le français
const cohereEmbeddings = new CohereEmbeddings({
    model: 'embed-multilingual-v3.0',
    inputType: 'search_document', // 'search_query' pour les requêtes
});

// Test manuel d'un embedding
const vector = await openaiEmbeddings.embedQuery('Angular Signal Inputs');
console.log(`Dimensions: ${vector.length}`); // 1536
console.log(`Sample: [${vector.slice(0, 4).map(v => v.toFixed(4)).join(', ')}...]`);

Vector Store — choix et configuration

Solution Type Scalabilité Idéal pour
ChromaOpen source, localMoyenneDev, prototypes, petits projets
PineconeCloud managéTrès hauteProduction, millions de vecteurs
pgvectorExtension PostgreSQLHauteApps existantes sur PostgreSQL
QdrantOpen source, self-hostedTrès hauteDonnées sensibles, self-hosted
SupabasePostgreSQL + pgvector managéHauteStack moderne tout-en-un
import { Chroma } from '@langchain/community/vectorstores/chroma';
import { OpenAIEmbeddings } from '@langchain/openai';
import { Document } from '@langchain/core/documents';

const embeddings = new OpenAIEmbeddings({ model: 'text-embedding-3-small' });

// Créer le vector store depuis les chunks
const vectorStore = await Chroma.fromDocuments(enrichedChunks, embeddings, {
    collectionName: 'angular-docs',
    url: 'http://localhost:8000', // Chroma local
});

// Avec Pinecone pour la production
import { PineconeStore } from '@langchain/pinecone';
import { Pinecone } from '@pinecone-database/pinecone';

const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const index = pinecone.index('angular-docs');

const pineconeStore = await PineconeStore.fromDocuments(enrichedChunks, embeddings, {
    pineconeIndex: index,
    namespace: 'production-v1',
    textKey: 'text',
});

Recherche sémantique et hybride

// Recherche sémantique de base
const semanticResults = await vectorStore.similaritySearch(
    'Comment créer un service injectable Angular ?',
    5 // top-5 chunks
);

// Recherche avec filtres métadonnées
const filteredResults = await vectorStore.similaritySearch(
    'Angular signals',
    3,
    { source: 'angular-docs.pdf' } // Filtrer par source
);

// Recherche avec scores
const withScores = await vectorStore.similaritySearchWithScore('Angular 19', 5);
withScores.forEach(([doc, score]) => {
    console.log(`Score: ${(1 - score).toFixed(3)} | ${doc.pageContent.substring(0, 80)}...`);
});

// Hybrid search — combiner vecteurs + BM25 (mots-clés)
// LangChain EnsembleRetriever
import { EnsembleRetriever } from 'langchain/retrievers/ensemble';
import { BM25Retriever } from '@langchain/community/retrievers/bm25';

const vectorRetriever = vectorStore.asRetriever({ k: 5 });
const bm25Retriever = BM25Retriever.fromDocuments(enrichedChunks, { k: 5 });

// Fusion des deux approches avec pondération
const ensembleRetriever = new EnsembleRetriever({
    retrievers: [vectorRetriever, bm25Retriever],
    weights: [0.7, 0.3], // 70% sémantique, 30% mots-clés
});

Génération avec contexte et citations

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

async function answerWithRAG(question: string, vectorStore: any): Promise<{
    answer: string;
    sources: string[];
}> {
    // 1. Rechercher les chunks pertinents
    const results = await vectorStore.similaritySearchWithScore(question, 4);
    const relevantChunks = results.filter(([, score]) => score < 0.3); // Seuil de pertinence

    if (relevantChunks.length === 0) {
        return { answer: 'Je ne trouve pas d\'information sur ce sujet dans ma base de connaissances.', sources: [] };
    }

    // 2. Construire le contexte avec sources
    const contextWithSources = relevantChunks.map(([doc], i) =>
        `[SOURCE ${i + 1}] ${doc.metadata.source} :\n${doc.pageContent}`
    ).join('\n\n---\n\n');

    const sources = [...new Set(relevantChunks.map(([doc]) => doc.metadata.source))];

    // 3. Prompt avec instructions de citation
    const response = await client.messages.create({
        model: 'claude-sonnet-4-6',
        max_tokens: 1024,
        system: `Tu es un assistant expert. Réponds UNIQUEMENT en te basant sur les sources fournies.
Si l'information n'est pas dans les sources, dis-le clairement.
Cite les sources avec [SOURCE N] dans ta réponse.`,
        messages: [{
            role: 'user',
            content: `Sources :\n${contextWithSources}\n\nQuestion : ${question}`
        }]
    });

    return {
        answer: response.content[0].type === 'text' ? response.content[0].text : '',
        sources,
    };
}

Pipeline RAG LangChain.js complet

import { ChatAnthropic } from '@langchain/anthropic';
import { createRetrievalChain } from 'langchain/chains/retrieval';
import { createStuffDocumentsChain } from 'langchain/chains/combine_documents';
import { ChatPromptTemplate } from '@langchain/core/prompts';

// Modèle LLM
const model = new ChatAnthropic({ model: 'claude-sonnet-4-6', maxTokens: 1024 });

// Prompt template avec contexte
const prompt = ChatPromptTemplate.fromTemplate(`
Tu es un assistant expert. Réponds à la question en utilisant uniquement le contexte fourni.
Si la réponse n'est pas dans le contexte, dis que tu ne sais pas.

Contexte :
{context}

Question : {input}
`);

// Chain de combination de documents
const documentChain = await createStuffDocumentsChain({ llm: model, prompt });

// Retriever depuis le vector store
const retriever = vectorStore.asRetriever({
    k: 4,
    searchType: 'similarity', // 'mmr' pour la diversité
});

// Pipeline RAG complet
const retrievalChain = await createRetrievalChain({
    retriever,
    combineDocsChain: documentChain,
});

// Invocation
const result = await retrievalChain.invoke({
    input: 'Comment utiliser les linkedSignals dans Angular 19 ?',
});

console.log(result.answer);
console.log('Sources:', result.context.map(doc => doc.metadata.source));

RAG avancé : re-ranking et query expansion

// Re-ranking avec Cohere Rerank
import { CohereRerank } from '@langchain/cohere';
import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression';

const cohereRerank = new CohereRerank({
    apiKey: process.env.COHERE_API_KEY,
    topN: 3,  // Garder les 3 meilleurs après re-ranking
    model: 'rerank-v3.5',
});

// Compression + re-ranking en une étape
const rerankedRetriever = new ContextualCompressionRetriever({
    baseCompressor: cohereRerank,
    baseRetriever: vectorStore.asRetriever({ k: 10 }), // Récupérer plus, re-ranker ensuite
});

// Query expansion — générer plusieurs formulations de la question
async function expandQuery(question: string): Promise<string[]> {
    const response = await client.messages.create({
        model: 'claude-haiku-4-5-20251001',
        max_tokens: 200,
        messages: [{
            role: 'user',
            content: `Génère 3 formulations alternatives de cette question pour améliorer la recherche.
Retourne uniquement les 3 questions, une par ligne.
Question originale: ${question}`
        }]
    });

    const text = response.content[0].type === 'text' ? response.content[0].text : '';
    return [question, ...text.split('\n').filter(q => q.trim())];
}

Évaluation et métriques de qualité

// Évaluation RAG avec RAGAS (Retrieval Augmented Generation Assessment)
// npm install ragas

// Métriques clés à mesurer :
// - Faithfulness : la réponse est-elle fidèle aux chunks récupérés ?
// - Answer Relevancy : la réponse répond-elle à la question ?
// - Context Recall : tous les faits nécessaires sont-ils dans les chunks ?
// - Context Precision : les chunks récupérés sont-ils pertinents ?

// Test simple sans librairie
async function evaluateRAG(testCases: Array<{question: string; expectedAnswer: string}>) {
    let correctCount = 0;

    for (const testCase of testCases) {
        const { answer } = await answerWithRAG(testCase.question, vectorStore);

        // Évaluation avec LLM juge
        const evaluation = await client.messages.create({
            model: 'claude-opus-4-6',
            max_tokens: 100,
            messages: [{
                role: 'user',
                content: `La réponse générée correspond-elle à la réponse attendue ?
Réponse attendue: ${testCase.expectedAnswer}
Réponse générée: ${answer}
Réponds uniquement par "OUI" ou "NON".`
            }]
        });

        const isCorrect = evaluation.content[0].type === 'text' &&
            evaluation.content[0].text.includes('OUI');
        if (isCorrect) correctCount++;
    }

    return { accuracy: correctCount / testCases.length * 100 };
}

Conclusion

Le RAG est la technique de référence pour ancrer les LLMs dans des données spécifiques à votre domaine. Les points clés :

  • Chunking qualitatif (800 chars, overlap 100) — la base de tout RAG performant
  • text-embedding-3-small OpenAI pour l'équilibre qualité/coût
  • Recherche hybride (vecteurs + BM25) pour +20-30% de précision
  • Re-ranking Cohere après retrieval pour filtrer le bruit
  • Citer les sources dans le prompt système — réponses traçables
  • Évaluer régulièrement avec un jeu de tests connu

Questions fréquentes

Le RAG combine deux systèmes : un moteur de recherche vectorielle (retrieval) et un modèle de langage génératif (generation). Quand une question arrive, elle est convertie en vecteur via un modèle d'embedding (OpenAI text-embedding-3, Cohere, etc.), puis les documents les plus similaires sont récupérés depuis une base vectorielle (Pinecone, Weaviate, Chroma). Ces documents pertinents sont injectés dans le contexte du prompt envoyé au LLM, qui génère une réponse ancrée dans ces sources. Contrairement à un LLM seul, le RAG peut accéder à des données privées, récentes ou spécifiques à un domaine sans nécessiter de fine-tuning coûteux.

Le choix dépend du volume et de l'infrastructure : Pinecone est le leader managé, idéal pour démarrer rapidement sans gestion d'infrastructure, avec une latence faible garantie. Weaviate offre un filtrage hybride (vectoriel + keyword) et un déploiement on-premise. Chroma est parfait pour le développement local et les POC Python. Qdrant se distingue par sa haute performance sur de grands volumes avec payload filtering. Pour une intégration PostgreSQL existante, pgvector permet d'ajouter la recherche vectorielle sans nouvelle base. Le critère clé en production est le compromis entre rappel (tous les documents pertinents trouvés) et précision (qualité des résultats retournés).

Le chunking (découpage des documents) est critique : des chunks trop petits perdent le contexte, trop grands noient l'information pertinente. Les stratégies recommandées incluent le recursive character text splitting avec chevauchement (overlap de 10-20% entre chunks pour éviter les coupures de contexte), le semantic chunking qui regroupe les phrases par similarité sémantique plutôt que par nombre de caractères, et le parent-document retrieval qui stocke de petits chunks pour la recherche mais injecte les parents plus larges comme contexte. Avec LangChain ou LlamaIndex, tester différentes tailles (512, 1024, 2048 tokens) sur votre dataset spécifique reste la meilleure approche empirique.

L'évaluation RAG se décompose en deux axes : la qualité du retrieval et la qualité de la génération. Pour le retrieval, on mesure le recall@k (proportion de documents pertinents dans les k résultats retournés) et la précision@k. Pour la génération, RAGAS (framework open-source) mesure la faithfulness (la réponse est-elle fondée sur les contextes récupérés ?), l'answer relevancy (la réponse répond-elle à la question ?) et le context precision. Des outils comme TruLens ou DeepEval permettent d'automatiser ces évaluations. Un pipeline RAG performant atteint typiquement un faithfulness > 0.85 sur des datasets de questions métier bien définis.

Partager