← Retourner à la liste des articles
Image blog
Auteur

Par Maxime Jumelle

CTO & Co-Founder

Publié le 14 févr. 2025

Catégorie IA Générative

Créer un RAG avec LangChain et Mistral à partir de zéro

Le RAG (Retrieval Augmented Generation) est sans doute l'un des cas d'usages de l'IA Générative les plus répandus aujourd'hui. Mais lorsque l'on débute avec les LLM, il peut être difficile de savoir comment il est possible de créer son propre RAG de toute pièce en partant de zéro.

Développé par Harrison Chase et Ankush Gola et lancé en octobre 2022, LangChain est une plateforme open source conçue pour la construction d'applications robustes alimentées par des LLM, telles que des chatbots comme ChatGPT et diverses applications sur mesure. En particulier, LangChain permet de créer des RAG très facilement, à la fois parce qu'il existe déjà des fonctions Python toutes prêtes, mais aussi parce que LangChain dispose de très nombreuses intégrations avec des API, des bases de données, et même des librairies pour exécuter des LLM en local.

Dans cet article, nous allons voir comment en quelques dizaines de lignes de code Python, nous pouvons créer un RAG de toute pièce sur des PDF de plusieurs centaines de pages, et ce grâce à LangChain.

Rappels du RAG

De manière synthétique, les RAG sont constitués de deux étapes principales.

  • Une étape de récupération (retrieval) qui a pour but d'extraire des informations pertinentes depuis la base de connaissances, que ce soit dans une base de données ou dans des documents. Bien que cette recherche puisse se faire sur des mots-clés, les techniques modernes utilisent plutôt des méthodes basées sur l'embedding que nous allons détailler.
  • Une étape de génération où, une fois l'information recherché extraite, un LLM va générer une réponse en langage naturel. L'intérêt d'utiliser un LLM ici est de ne pas directement fournir l'information extraite en brut à un utilisateur, mais de l'embellir avec un texte en langage naturel. À cette étape, plusieurs LLM comme GPT, LLaMa ou encore Mistral peuvent être utilisés.

RAG

Mais avant de pouvoir exécuter ces deux étapes, les documents doivent d'abord être vectorisés : c'est la toute première phase du RAG avec l'ingestion de documents.

Ingestion des documents

Ce qui est important de savoir lorsque l'on construit un RAG, c'est de déterminer les sources d'informations de manière précise. En effet, il ne sera pas nécessaire d'y ajouter des documents qui n'apportent pas de valeur ajoutée.


À découvrir : notre formation LLM Engineering


Dans notre situation, nous allons permettre à une banque américaine (Banque Inter-Américaine pour le Développement) de proposer à ses collaborateurs un chat qui va se baser sur ses rapports annuels en PDF pour pouvoir répondre à toutes les questions qui lui sont posées. Pour cela, nous allons utiliser l'ensemble des rapports annuels entre 2000 et 2022, totalisant plus de 2000 pages de textes et tableaux de données.

Loading

Afin de commencer le développement de notre RAG, la première étape consiste à télécharger les fichiers PDF.

!mkdir -p data
!wget \
    -O data/annuals_reports.zip \
    https://blent-learning-user-ressources.s3.eu-west-3.amazonaws.com/training/ai_gen_engineering/data/annual_reports.zip
!unzip -d data data/annuals_reports.zip
!rm data/annuals_reports.zip

Comme nous l'avons évoqué plus haut, chacun de ces fichiers contient les informations et rapport annuels et la banque, avec de nombreuses informations financières que les collaborateurs pourront récupérer très facilement sans effectuer des recherches fastidieuses.

Un exemple du rapport de 2009 afin d'inspecter la forme du document et visualiser les différentes informations disponibles.

Afin de pouvoir charger et lire facilement des documents PDF avec LangChain, nous utilisons l'objet PyPDFLoader qui facilite les opérations sur les PDF. À noter que nous pourrions nous-même créer nos propres loaders pour d'autres formats de fichier.

import glob

from langchain_community.document_loaders import PyPDFLoader

# Initialisation du loader de document pour charger un fichier PDF
documents = []

for file in glob.glob("data/annual_reports/*.pdf"):
    try:
        loader = PyPDFLoader(file)  # Retourne une liste de document (un pour chaque page)
        documents += loader.load()
    except Exception:
        print(f"Erreur survenue pour le fichier '{file}'.")

 Chunking

Une fois le contenu de tous les PDF chargés en mémoire, il nous faut maintenant découper chaque document en chunks, afin que ceux-ci soient vectorisés.

LangChain met à disposition différents splitters, c'est-à-dire des objets qui permettent de découper le texte de manière intelligente. En particulier, certains splitters sont plus adaptés pour découper du code, et d'autres du Markdown par exemple.

Le RecursiveCharacterTextSplitter est le splitter le plus basique, qui permet de découper du texte en langage naturel sous forme de chunks.

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document

# Initialisation du séparateur de texte avec des paramètres spécifiques pour diviser le texte
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=600,  # Taille maximale des morceaux de texte
    chunk_overlap=60,  # Chevauchement entre les morceaux pour garder le contexte
    length_function=len,  # Fonction pour calculer la longueur des morceaux
    separators=["\n\n", "\n"]  # Séparateurs utilisés pour diviser le texte en morceaux
)

# Division du document en morceaux (chunks)
chunks = text_splitter.split_documents(documents=documents)

# Affichage du nombre de morceaux créés à partir du document PDF
print(f"{len(chunks)} chunks ont été créés par le splitter à partir du document PDF.")

 Création de l'embedding

Maintenant que le texte est nettoyé, nous passons l'étape de création des embeddings pour ensuite les stocker dans la base de données vectorielle Pinecone.

Création du Vector Store

Cette étape nécessite l'installation du client Pinecone et des bibliothèques transformers, torch et accelerate qui nous permetterons de charger et d'utiliser un modèle d'encodage de texte de HuggingFace.

from langchain_huggingface.embeddings import HuggingFaceEmbeddings

# Charger le modèle d'encodage de texte BAAI/bge-small-en-v1.5 de HuggingFace
embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-en-v1.5", encode_kwargs={"normalize_embeddings" : True})

Créons maintenant notre index Pinecone qui stockera tous nos vecteurs d'embedding.

from pinecone import Pinecone, ServerlessSpec
from pinecone.exceptions import NotFoundException

# TODO: Inscrire la clé API Pinecone
pinecone = Pinecone(api_key="")

try:
    pinecone.describe_index("rag")
except NotFoundException: 
    # Créer un index nommé "rag" de dimension 384
    pinecone.create_index("rag", dimension=384, spec=ServerlessSpec(cloud="aws", region="us-east-1"))
except Exception:
    print("Une erreur inconnue est survenue !")

Maintenant que notre index est créé, nous allons ajouter tous nos vecteurs de documents dedans. Le processus se décompose ainsi en plusieurs étapes.

  • Découper les documents en plusieurs chunks pour encapsuler localement les informations.
  • Convertir les chunks en vecteurs d'embedding.
  • Charger ces vecteurs d'embedding dans un vector store (la base de données vectorielle).

Nous disposons de l'objet PineconeVectorStore, qui nécessite à la fois l'index Pinecone, ainsi que le modèle d'embedding utilisé pour vectoriser les chunks.

from langchain_pinecone.vectorstores import PineconeVectorStore

# Initialiser le VectorStore de LangChain avec l'index de Pinecone
pinecone_index = pinecone.Index("rag")
vector_store = PineconeVectorStore(
    index=pinecone_index,
    embedding=embedding
)

Insertion des vecteurs

Il ne reste maintenant plus qu'à vectoriser tous les chunks et à les ajouter dans Pinecone.

add_result = vector_store.add_documents(chunks)
print(f"{add_result} vecteurs ont été ajoutés dans Pinecone.")
16211 vecteurs ont été ajoutés dans Pinecone.

Nous avons au total près de 16,000 vecteurs dans notre base, tous de dimension 384. Le principal intérêt ici d'avoir chargé tous les vecteurs dans une base est de ne pas avoir à répéter l'opération systématiquement. Cela est donc beaucoup plus facile si l'on souhaite à l'avenir modifier le LLM de génération sans modifier l'embedding.

Maintenant que notre embedding de documents est prêt, nous pouvons passer à l'étape de génération.

Génération

Passons maintenant à l'étape de génération. Tout d'abord, puisque nous allons télécharger un modèle depuis HuggingFace, il nous faut définir le token d'authentification.

!huggingface-cli login --token=...

Exécution d'un LLM en local

Comme nous l'avons déjà fait par le passé avec la librairie transformers, nous téléchargeons et chargeons en mémoire le modèle Mistral 7B.

import transformers
import torch

from transformers import BitsAndBytesConfig

model_id = "mistralai/Mistral-7B-Instruct-v0.3"

# Chargement de la configuration du modèle
model_config = transformers.AutoConfig.from_pretrained(model_id)

# Initialiser le tokeniseur
tokenizer = transformers.AutoTokenizer.from_pretrained(model_id)

model = transformers.AutoModelForCausalLM.from_pretrained(
    model_id,
    trust_remote_code=True,
    config=model_config,
    device_map='auto'
)

À partir de là, LangChain offre une interface par l'intermédiaire de l'objet HuggingFacePipeline, qui permet de créer un pipeline LangChain. Autrement dit, on utilise le LLM chargé avec transformers pour être utilisable avec tous les autres composants de LangChain.


À lire : découvrez notre formation LLM Engineering


Cette logique est d'ailleurs fréquemment utilisée avec LangChain : il existe de nombreux objets qui fournissent cette inter-opérabilité entre un objet issu d'une autre librairie et LangChain.

from langchain_huggingface.llms import HuggingFacePipeline

from transformers import pipeline

llm=HuggingFacePipeline(
    pipeline=pipeline(
        "text-generation",
        model=model,
        tokenizer=tokenizer,
        max_new_tokens=4096,
        do_sample=False,
        return_full_text=False  # Très important ! On ne veut pas le prompt initial
    )
)

Puisque l'on utilise transformers, on retrouve ainsi tous les paramètres que nous avions déjà rencontrés auparavant.

Création du pipeline RAG

La dernière étape consiste à créer notre pipeline RAG, que l'on encapsule dans une fonction sous forme de plusieurs étapes.

from langchain_core.prompts import PromptTemplate

prompt_template = PromptTemplate.from_template("""You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Question: {question} 
Context: {context} 
Answer: """)

def rag_pipeline(query):
    # Tout d'abord, on recherche les documents
    retrieved_docs = vector_store.similarity_search(query)
    # Ensuite, on injecte les documents dans le prompt
    prompt = prompt_template.invoke({
        "question": query,
        "context": "\n\n".join(doc.page_content for doc in retrieved_docs)
    })
    # Enfin, on envoit l'intégralité du prompt au LLM
    return llm.invoke(prompt).strip()

Il ne nous reste plus qu'à exécuter notre fonction pour réaliser une inférence de notre pipeline RAG.

query = """
In 2009, how much loans and guarantees were proposed by the bank? Please give details about this number, such as the biggest loans involved. Please also give the source.
"""

# Effectuer une requête
response = rag_pipeline(query)
print(response)
In 2009, the bank proposed loans and guarantees totaling $15.5 billion. The biggest loan involved was for $15.3 billion from the Ordinary Capital. This loan was part of a cumulative total of 2,225 loans for $160.8 billion from the Ordinary Capital. The source is the context provided.

Nous voyons que notre RAG fonctionne très bien, et que les réponses apportées sont cohérentes avec les documents stockés dans notre base vectorielle.

Conclusion

Comme nous venons de le voir, créer un RAG avec LangChain ne demande pas beaucoup de code. En revanche, si l'on souhaite avoir beaucoup plus de flexibilité et de possibilité de personnalisation, on pourra être tenté de développer son propre pipeline RAG, sans avoir recours à LangChain.

Ce qui est important à prendre en compte dans un RAG, c'est que la pertinence des documents et la qualité du chunking sont primordiales pour avoir de bonnes performances. Contrairement à ce que l'on pourrait penser, le point critique dans un RAG se situe dès la première étape, lorsque les documents sont découpés et vectorisés.

Articles similaires

Blog

4 mars 2025

IA Générative

Pour garantir l'efficacité d’un système RAG, , il est primordial de disposer de méthodes d’évaluation robustes et adaptées. Ces évaluations permettent non seulement d’identifier les faiblesses des différent composants du système (notamment du retriever et du générateur), mais aussi d’optimiser ses performances globales.

Maxime Jumelle

Maxime Jumelle

CTO & Co-Founder

Lire l'article

Blog

19 févr. 2025

IA Générative

Comme pour la plupart des outils, les systèmes RAG sont faciles à utiliser, mais difficiles à maîtriser. La réalité est que le RAG ne se limite pas à insérer des documents dans une base de données vectorielle et à y ajouter un modèle de langage. Cela peut fonctionner, mais ce n’est pas toujours suffisant pour garantir des résultats fiables et cohérents.

Maxime Jumelle

Maxime Jumelle

CTO & Co-Founder

Lire l'article

Blog

13 févr. 2024

IA Générative

Avec l'explosion de l'IA Générative appliquée à la génération de texte ces dernières années, de nombreuses entreprises ont souhaité pouvoir déployer en interne leur propres LLM. Elles disposent ainsi de deux possibilités : utiliser directement des modèles disponibles en version SaaS comme ChatGPT ou Cohere, ou déployer dans leur propre SI un LLM open source adaptés à leurs propres besoins.

Maxime Jumelle

Maxime Jumelle

CTO & Co-Founder

Lire l'article