Intelligence Artificielle angularforall.com

- 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

Partager