Comment construire un système RAG simple et efficace ?

Un système RAG combine recherche pertinente et génération par IA pour fournir des réponses précises et à jour. Ce guide détaille sept étapes claires pour construire votre propre pipeline RAG, depuis la préparation des données jusqu’à la génération finale, en garantissant fiabilité et pertinence.

3 principaux points à retenir.

  • RAG améliore la précision de l’IA en intégrant des données externes récentes et spécifiques.
  • La fragmentation et vectorisation de texte sont essentielles pour un accès rapide et sémantique des informations.
  • Une intégration intelligente des résultats dans le prompt LLM optimise la qualité des réponses générées.

Qu’est-ce qu’un système RAG et pourquoi l’utiliser

Le système RAG, ou Retrieval-Augmented Generation, est une approche innovante qui combine deux composants majeurs : un récupérateur et un générateur. Le récupérateur se charge d’extraire les informations les plus pertinentes d’une base de données ou d’une collection de documents, tandis que le générateur utilise ces informations pour produire des réponses précises et contextualisées. À première vue, cela peut sembler similaire à ce que font déjà les modèles de langage génératifs (LLM). Pourtant, un LLM seul peut parfois être peu fiable en raison du décalage temporel présent dans ses données d’apprentissage et des hallucinations qui surviennent fréquemment, où le modèle crée des réponses incorrectes sans bases solides.

La magie du RAG réside dans sa capacité à garantir des réponses plus justes, actualisées et spécifiques à un domaine particulier. Pour illustrer, imaginons une requête sur les développements récents dans le domaine de l’intelligence artificielle. Un LLM, sans accès à des données récentes, risque de délivrer des informations obsolètes. En revanche, un système RAG peut interroger un ensemble de documents à jour et fournir une réponse enrichie, ancrée dans des faits récents.

Le processus de workflow général peut être décomposé en quatre étapes essentielles :

  • Question utilisateur : Tout commence par la question posée par l’utilisateur.
  • Recherche des passages pertinents : Le récupérateur scanne la base de données pour identifier les passages qui répondent le mieux à cette question.
  • Ajout comme contexte : Ces passages sont ensuite utilisés comme contexte pour la génération de la réponse.
  • Génération de réponse : Le générateur produit, en s’appuyant sur le contexte fourni, une réponse synthétiques et détaillée.

Il ne fait aucun doute que le RAG représente une avancée significative pour éviter les déboires liés aux informations périmées et corrosives des modèles de langage classiques. Pour en savoir plus sur cette technologie fascinante, rendez-vous sur Oracle.

Comment préparer et fragmenter les données pour la recherche

La préparation des données constitue une étape cruciale pour garantir l’efficacité de votre système RAG, car elle permet d’éviter les erreurs lors de la recherche. Tout commence par la collecte des documents textuels qui seront traités. Une fois vos fichiers réunis, le nettoyage est une nécessité absolue. En effet, un texte mal structuré peut entraîner des résultats non pertinents lors de la phase de requête.

Pour simplifier cette tâche, voici un exemple en Python qui charge et nettoie des fichiers texte en assurant une bonne qualité de données :

import os

def load_documents(folder_path):
    docs = []
    for file in os.listdir(folder_path):
        if file.endswith(".txt"):
            with open(os.path.join(folder_path, file), 'r', encoding='utf-8') as f:
                docs.append(f.read())
    return docs

def clean_text(text: str) -> str:
    text = re.sub(r'\s+', ' ', text)
    text = re.sub(r'[^\x00-\x7F]+', ' ', text)
    return text.strip()

Une fois les données chargées et nettoyées, vous devez les fragmenter en chunks (morceaux) de texte courts et partiellement chevauchants. Pourquoi ? La réponse réside dans la façon dont les modèles de langage fonctionnent. Ils ont une fenêtre de contexte limitée, et un chunk trop long pourrait ne pas être pris en compte efficacement. En utilisant un outil comme RecursiveCharacterTextSplitter de LangChain, vous pouvez obtenir une répartition cohérente de vos textes :

from langchain.text_splitter import RecursiveCharacterTextSplitter

def split_docs(documents, chunk_size=500, chunk_overlap=100):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap
    )
    chunks = splitter.create_documents(documents)
    return chunks

Le choix de paramètres pour la chunk_size (taille du chunk) et le chunk_overlap (recouvrement) est essentiel. En général, une taille comprise entre 300 et 500 mots est adéquate, tandis qu’un chevauchement d’environ 100 mots permet de conserver le sens aux frontières des morceaux. Cela évite ainsi aux modèles de langage de se retrouver à court d’informations pertinentes.

Intégrer correctement ces morceaux, c’est s’assurer que votre système RAG fonctionnera de manière optimale, offrant des réponses précises et contextuelles, tout en réduisant les risques d’hallucinations des modèles. Pour en savoir plus sur les systèmes RAG avancés, consultez ce lien ici.

Comment transformer les textes en vecteurs et les stocker efficacement

Les ordinateurs, ces bêtes de calcul en acier, ne comprennent pas le texte comme nous le faisons. Pour eux, tout n’est qu’un langage numérique. C’est ici qu’intervient la magie des embeddings vectoriels. En transformant les fragments de texte en vecteurs numériques, nous permettons au système de saisir le sens caché derrière les mots. Cette approche est essentielle pour créer des systèmes RAG efficaces.

Une des solutions les plus performantes pour générer ces embeddings est SentenceTransformers, qui offre une gamme de modèles puissants. Prenons par exemple le modèle all-MiniLM-L6-v2, une petite merveille pour obtenir des embeddings de haute qualité tout en restant léger et rapide.

from sentence_transformers import SentenceTransformer
import numpy as np

def get_embeddings(text_chunks):
    model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
    print(f"Creating embeddings for {len(text_chunks)} chunks:")
    embeddings = model.encode(text_chunks, show_progress_bar=True)
    print(f"Embeddings shape: {embeddings.shape}")
    return np.array(embeddings)

Maintenant, pourquoi stocker ces embeddings dans une base de vecteurs comme FAISS ? C’est simple : FAISS (Facebook AI Similarity Search) permet une recherche rapide et précise par similarité, ce qui est crucial dans un système RAG. Au lieu de parcourir des millions de documents pour trouver une correspondance pertinente, FAISS nous permet d’accéder instantanément aux vecteurs les plus proches.

Voici comment on peut créer un index FAISS, ajouter des embeddings et sauvegarder les métadonnées associées :

import faiss
import numpy as np
import pickle

def build_faiss_index(embeddings, save_path="faiss_index"):
    dim = embeddings.shape[1]
    print(f"Building FAISS index with dimension: {dim}")
    
    index = faiss.IndexFlatL2(dim)  # index par défaut L2
    index.add(embeddings.astype('float32'))
    
    faiss.write_index(index, f"{save_path}.index")
    print(f"Saved FAISS index to {save_path}.index")
    
    return index

L’intérêt d’utiliser une base locale comme FAISS réside dans sa légèreté. Contrairement aux solutions cloud qui peuvent être encombrantes et complexes à gérer, FAISS nous permet d’avoir une manipulation rapide et efficace sur nos données dans notre environnement de travail. Cela fait de FAISS un choix de premier plan pour ceux qui souhaitent bâtir des systèmes RAG performants.

Comment récupérer les informations pertinentes et construire le contexte

Pour bien comprendre comment récupérer des informations pertinentes et construire un contexte enrichi pour nos requêtes, nous devons d’abord convertir la requête utilisateur en un embedding vectoriel. En d’autres termes, nous transformons cette question en une représentation numérique que notre système peut traiter. Cette transformation nous permet d’effectuer une recherche sémantique efficace, c’est-à-dire de trouver les morceaux de texte les plus proches en termes de significations plutôt qu’en termes de simples mots-clés.

Ensuite, nous utilisons cet embedding pour rechercher dans notre index FAISS les fragments de texte qui sont les plus similaires à la requête. Ce processus est crucial ; il nous permet de maximiser la pertinence des réponses en extrayant uniquement les informations les plus utiles, garantissant ainsi une génération de réponse fiable et ciblée. Voici comment nous allons procéder en pratique avec un exemple de code :


import faiss
import pickle
import numpy as np
from sentence_transformers import SentenceTransformer

def load_faiss_index(index_path="faiss_index.index"):
    # Charge l'index FAISS depuis le disque
    print("Chargement de l'index FAISS.")
    return faiss.read_index(index_path)

def load_metadata(metadata_path="faiss_metadata.pkl"):
    # Charge les métadonnées des textes
    print("Chargement des métadonnées textuelles.")
    with open(metadata_path, "rb") as f:
        return pickle.load(f)

def retrieve_similar_chunks(query, index, text_chunks, top_k=3):
    # Intègre la requête
    model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
    query_vector = model.encode([query]).astype('float32')
    distances, indices = index.search(query_vector, top_k)
    print(f"Récupération des {top_k} fragments similaires.")
    return [text_chunks[i] for i in indices[0]]

# Usage
index = load_faiss_index()
text_chunks = load_metadata()
query = "Quel est le rôle des algorithmes de clustering en apprentissage non supervisé ?"
context_chunks = retrieve_similar_chunks(query, index, text_chunks, top_k=3)
context = "\n\n".join(context_chunks)

Une fois que nous avons extrait les top K résultats, nous les concaténons en un seul bloc de contexte prêt à être injecté dans notre modèle de langage. Ce contexte n’est pas qu’un simple empilement d’informations ; il doit être construit de manière à ce que chaque morceau contribue à la compréhension globale de la question posée. Le succès de l’étape finale, celle de la génération de la réponse, dépend largement de la qualité et de la pertinence de ce contexte.

En considérant chaque détail, nous assurons que le système puisse faire des recommandations éclairées. Cela nous ramène à l’importance d’un système RAG efficace pour gérer les informations dynamiques et variées. Vous pouvez également consulter des ressources supplémentaires pour approfondir ce sujet, comme ce guide qui aborde des tactiques avancées dans la construction de systèmes RAG.

Comment générer des réponses fiables avec un LLM intégré au RAG

Pour obtenir des réponses fiables avec un modèle de langage intégré à notre système RAG, il s’agit avant tout de lui fournir un contexte robuste. Imaginez que vous posiez une question précise ; si le modèle est nourri avec les bonnes informations préalablement récupérées, il pourra alors délivrer des réponses d’une clarté et d’une pertinence remarquables.

Considérons cet exemple avec un modèle open-source, TinyLlama, provenant de Hugging Face. Vous allez voir comment structurer votre prompt pour optimiser vos résultats.

from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

def generate_answer(query, top_k=3):
    # Charger l'index FAISS et les métadonnées
    index = load_faiss_index()
    text_chunks = load_metadata()
    
    # Récupérer les extraits les plus pertinents
    context_chunks = retrieve_similar_chunks(query, index, text_chunks, top_k=top_k)
    context = "\n\n".join(context_chunks)

    # Charger le modèle LLM open-source
    print("Chargement du LLM...")
    model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16, device_map="auto")

    # Construire le prompt
    prompt = f"""
    Contexte :
    {context}
    Question :
    {query}
    Réponse :
    """

    # Générer la sortie
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    with torch.no_grad():
        outputs = model.generate(**inputs, max_new_tokens=200, pad_token_id=tokenizer.eos_token_id)

    # Décoder et extraire la réponse
    full_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
    
    # Extraction de la réponse
    answer = full_text.split("Réponse :")[1].strip() if "Réponse :" in full_text else full_text.strip()
    
    print("\nRéponse finale :")
    print(answer)

Dans cet extrait de code, nous avons structuré le prompt pour qu’il inclue d’abord le contexte obtenu grâce aux réponses récupérées, suivi de la question. Ensuite, lorsque le modèle produit une réponse, nous veillons à extraire celle-ci de manière à éviter d’inclure des répétitions du prompt. Cela permet d’obtenir un texte clair et concis.

Il est crucial de rappeler que l’efficacité de cette technique dépend de la qualité du contexte fourni. Un contexte bien choisi et précis peut significativement améliorer la fiabilité des réponses générées par le modèle. En cherchant à construire un RAG robuste, votre système sera non seulement fonctionnel, mais aussi réellement utile. Pour aller plus loin, envisager des projets pratiques peut s’avérer très formateur. Si ça vous intéresse, consultez cet article pour des idées de projets RAG amusants et simples.

Comment tirer parti efficacement d’un système RAG dans vos projets ?

Construire un système RAG simple est accessible et transforme la qualité des réponses de n’importe quel LLM. En combinant préparation rigoureuse des données, fragmentation intelligente, vectorisation efficace et génération contextualisée, vous obtenez un assistant beaucoup plus fiable et actuel. Cette maîtrise vous donne un avantage clair dans tout projet IA nécessitant précision et pertinence. Le vrai bénéfice pour vous reste la confiance retrouvée dans les réponses générées — sans hallucinations ni décalage temporel. La mise en œuvre progressive selon ces 7 étapes est la clé du succès.

FAQ

Qu’est-ce qu’un système RAG ?

Un système RAG combine un moteur de recherche contextuel avec un modèle de langage pour fournir des réponses précises en s’appuyant sur des données externes actualisées.

Pourquoi diviser les documents en morceaux ou chunks ?

Les modèles de langage ont une fenêtre de contexte limitée, il faut donc fragmenter les textes en morceaux courts et légèrement chevauchants pour assurer une recherche et une compréhension efficace.

Quels outils utiliser pour créer et stocker des embeddings ?

Des modèles comme SentenceTransformers génèrent des vecteurs sémantiques, et des bases vectorielles comme FAISS stockent et recherchent ces embeddings rapidement et localement.

Comment garantir la pertinence des réponses générées ?

En se basant sur un contexte précis et pertinent extrait des documents via la recherche vectorielle, l’IA produit des réponses plus fidèles aux faits et adaptées à la question.

Peut-on intégrer un système RAG avec des modèles open source ?

Oui, avec des outils comme Hugging Face et des modèles open source tels que TinyLlama, il est possible de créer un système RAG efficace sans dépendre de solutions propriétaires.

 

 

A propos de l’auteur

Franck Scandolera, consultant expert et formateur indépendant en Web Analytics, Data Engineering, et IA générative, accompagne depuis plus de dix ans les professionnels dans la maîtrise des données et leur exploitation intelligente. Son expérience solide en infrastructure data, automatisation no-code, et déploiement de solutions RAG en fait un acteur reconnu pour transformer des concepts complexes en solutions métiers pragmatiques et robustes.

Retour en haut