Créer un RAG avec LangChain et Mistral à partir de zéro
Une fois que des LLM sont entraînés sur un très grand corpus, ce dernier se veut le plus généraliste possible pour capter énormément de situations et de contextes, pour apporter des réponses précises. Seulement, ces LLM ne sont pas en mesure d'avoir accès à une base de connaissance personnalisée, sur des documents de tous types dans une entreprise par exemple.

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.

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
python
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}'.")
python
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.")
python
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})
python
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 !")
python
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
)
python
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.")
python
16211 vecteurs ont été ajoutés dans Pinecone.
console
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=...
python
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'
)
python
À 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.
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
)
)
python
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()
python
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)
python
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.
console
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.


