Aller au contenu

Maîtriser le RAG

Mise à jour :

L’idée de développer un chatbot répondant aux questions DevOps en utilisant le contenu de mon blog me trotte dans la tête depuis des mois. Suite à un hackathon, que j’ai remporté avec mon équipe, je me lance enfin sur ce projet.

Objectif du POC

Je vais donc tenter de transformer mon blog en une ressource vivante, capable de répondre à vos questions. Le projet va démarrer par une sélection rigoureuse du contenu déjà publié, que je vais structurer de manière à ce qu’il puisse être efficacement parcouru et indexé.

Ensuite, je vais mener diverses expérimentations avec différents modèles d’embeddings et de llm pour comprendre lesquels seraient les plus efficaces pour ingérer le contenu et optimiser les réponses. Chaque test m’a apporté son lot de défis, notamment en matière de compréhension.

Ce projet est à la fois un défi technique et une passionnante aventure de création de contenu. En partageant cette expérience, je souhaite non seulement aider ceux qui envisagent des projets similaires, mais aussi inspirer d’autres rédacteurs techniques à explorer de nouvelles façons de valoriser leur travail à travers l’IA.

Historique des chatbots et évolution vers les applications spécialisées

Ma fascination pour les chatbots ne date pas d’hier. Ces assistants virtuels, conçus pour simuler des conversations avec des utilisateurs humains, ont parcouru un long chemin depuis leurs premières incarnations. Initialement, les chatbots étaient simples et se limitaient souvent à des réponses préprogrammées à des questions fréquemment posées. Ils ont été introduits pour la première fois dans les années 1960 avec le programme ELIZA, qui imitait un psychothérapeute en posant des questions ouvertes. ELIZA a ouvert la voie à des développements plus complexes, comme PARRY dans les années 70, plus sophistiqué dans la simulation de comportements humains.

Au fil des années, l’essor de l’intelligence artificielle et du machine learning a transformé radicalement les capacités des chatbots. L’avènement des réseaux de neurones et des modèles de traitement du langage naturel, comme GPT (Generative Pre-trained Transformer) et BERT (Bidirectional Encoder Representations from Transformers), a permis de créer des chatbots beaucoup plus avancés, capables de comprendre et de générer des réponses en contexte.

Dans notre domaine, le DevOps, l’utilisation des chatbots est une solution pour automatiser les interactions et améliorer l’efficacité des processus. Les IA spécialisées dans le DevOps ne se contentent pas de répondre à des questions ; elles agissent comme des membres actifs de l’équipe, , en aidant à l’écriture du code, en aidant à gérer les workflows, à surveiller les systèmes et même à déployer des mises à jour logicielles.

Ce changement de paradigme vers des applications d’IA plus ciblées et spécialisées m’a poussé à envisager la création d’un chatbot utilisant spécifiquement le contenu de mon blog sur le DevOps. En exploitant ce contenu riche et détaillé, je dois pouvoir concevoir un assistant virtuel non seulement informatif, mais aussi extrêmement pertinent pour les professionnels du DevOps cherchant des réponses immédiates et spécifiques à leur domaine.

Ce bref historique des applications d’IA montre combien cette technologie a évolué et comment elle continue de s’adapter pour répondre aux besoins spécifiques de différents domaines, y compris le DevOps. C’est dans ce contexte que mon projet de chatbot dopé au DevOps a pris forme, s’inscrivant dans la lignée des innovations qui marquent le secteur des technologies de l’information.

Comparaison entre l’utilisation du RAG et des LLMs simples

Dans le domaine de l’intelligence artificielle et de la gestion de la documentation technique, choisir entre le Retriever-Augmented Generation (RAG) et un Large Language Model (LLM) simple nécessite de comprendre les spécificités et les avantages de chaque approche. Le RAG et les LLMs, tels que GPT (Generative Pre-trained Transformer), ont chacun des caractéristiques qui les rendent adaptés à différents types d’applications. Voici pourquoi vous pourriez préférer utiliser le RAG plutôt qu’un LLM simple pour certaines tâches.

Capacité à exploiter des connaissances spécifiques

Le principal avantage du RAG par rapport à un LLM simple est sa capacité à intégrer et à exploiter des connaissances spécifiques contenues dans une base de données externe. Le RAG combine un modèle de récupération d’informations (le retriever) et un modèle de génération de texte (le generator). Cette combinaison lui permet de puiser dans une vaste quantité de données structurées ou semi-structurées pour fournir des réponses qui ne sont pas seulement générées à partir de ce qu’il a “appris” lors de sa formation initiale, mais aussi informées par des données spécifiques et actualisées.**

Un LLM simple génère des réponses basées uniquement sur le modèle appris durant sa phase de formation**, qui peut utiliser des données jusqu’à plusieurs mois ou années avant son déploiement. En revanche, le RAG peut accéder à des informations à jour en temps réel** grâce à sa capacité de récupérer des documents pertinents de sa base de données. Cela est particulièrement utile dans les domaines où les informations changent rapidement, comme la technologie, la médecine ou les actualités.

Fonctionnement du RAG

Le Retriever-Augmented Generation (RAG) est doté de fonctionnalités spécifiques qui le distinguent des autres modèles de traitement du langage naturel et qui en font un outil puissant pour l’amélioration des chatbots et autres systèmes d’IA. Voici un aperçu des principales caractéristiques et capacités de ce modèle hybride.

Combinaison de “Retriever” et de “Generator”

Le Retriever-Augmented Generation (RAG) se distingue par sa capacité à combiner la récupération d’informations avec la génération de texte, en offrant des fonctionnalités uniques qui le rendent particulièrement adapté à des applications comme les chatbots et les systèmes d’IA avancés. En combinant un retriever qui explore de vastes bases de données pour trouver des informations pertinentes, et un generator qui produit des réponses contextuelles et cohérentes, le RAG améliore la qualité des interactions en fournissant des réponses enrichies et vérifiables.

Sa flexibilité permet de personnaliser les sources de données pour différents cas d’utilisation, tandis que le modèle de génération peut être ajusté pour adopter un style spécifique. Le RAG bénéficie également de capacités d’apprentissage continu, s’adaptant aux nouvelles données sans réentraînement complet, améliorant ainsi la précision des réponses au fil du temps. De plus, il est hautement scalable, capable de gérer de nombreuses requêtes simultanées, ce qui le rend idéal pour des déploiements à grande échelle.

L’intégration transparente du RAG dans des systèmes existants, combinée à sa modularité, permet une adoption facile dans divers environnements technologiques, créant des solutions interactives complètes. Grâce à ces atouts, le RAG transforme la manière dont les systèmes interactifs accèdent et traitent les informations, offrant une expérience utilisateur plus fluide et plus précise.

Pourquoi utiliser le RAG pour votre documentation technique ?

L’utilisation du Retriever-Augmented Generation (RAG) dans la documentation technique offre de nombreux avantages qui peuvent transformer la manière dont les systèmes automatisés assistent les utilisateurs. D’abord, grâce à son retriever, le RAG accède rapidement à des bases de données techniques étendues, fournissant des réponses précises à des questions complexes, ce qui est essentiel dans des secteurs comme l’ingénierie, la santé ou l’informatique. Ensuite, il permet une réduction du temps de réponse en automatisant la recherche d’informations, améliorant ainsi l’efficacité des interactions et la satisfaction des utilisateurs.

Le RAG excelle aussi en matière de personnalisation, en adaptant ses réponses au niveau de compétence de l’utilisateur et au contexte spécifique, créant une expérience plus riche. De plus, sa capacité à traiter un large éventail de questions en fait un outil polyvalent, capable de répondre aussi bien à des requêtes simples qu’à des questions techniques avancées. Enfin, l’intégration facile du RAG avec des systèmes existants, comme des bases de données ou des systèmes de gestion de contenu, facilite sa mise en œuvre dans des environnements techniques déjà établis. Grâce à ces capacités, le RAG révolutionne la gestion de la documentation technique et les systèmes d’assistance automatisés.

Indexation des documents source dans le RAG

L’efficacité du Retriever-Augmented Generation (RAG) repose en grande partie sur sa capacité à accéder rapidement et précisément à des informations pertinentes stockées dans une base de données de documents source. Ce processus, appelé indexation des documents, est essentiel pour la performance du système de récupération d’informations. Dans cette section, je discuterai de l’importance du choix des embeddings qui influence directement la qualité des réponses générées par le llm.

Choix des embeddings

Les embeddings (ou plongements vectoriels) sont une technique fondamentale en apprentissage automatique, particulièrement dans le domaine du traitement du langage naturel (NLP). Ils permettent de représenter des données, souvent des mots ou des phrases, sous forme de vecteurs de nombres réels dans un espace continu de dimensions réduites. Cette représentation cherche à capturer et à refléter les similarités sémantiques entre les éléments.

Les embeddings ont été développés comme une méthode pour surmonter les limitations des représentations traditionnelles de texte, comme le “one-hot encoding”, où chaque mot est représenté par un vecteur contenant principalement des zéros, sauf à l’indice représentant le mot lui-même. Cette approche est inefficace, car elle ne capture aucune relation sémantique entre les mots et produit des vecteurs de très grande dimension.

Caractéristiques des embeddings :

  1. Dimension réduite : Contrairement au one-hot encoding, les embeddings représentent les mots dans un espace de dimensions beaucoup plus petites (typiquement de l’ordre de quelques centaines de dimensions).
  2. Capture des similitudes : Les embeddings sont conçus de sorte que les mots qui ont des significations similaires sont proches l’un de l’autre dans l’espace vectoriel. Par exemple, “roi” et “reine” seraient représentés par des vecteurs proches.
  3. Apprentissage : Les vecteurs ne sont pas statiques ; ils sont appris et affinés à travers des tâches spécifiques (comme la prédiction du mot suivant) ou en utilisant de grands ensembles de données textuelles.
  4. Usage polyvalent : Une fois appris, ces vecteurs peuvent être utilisés pour une multitude de tâches de NLP, y compris la classification de texte, la recherche sémantique et la traduction automatique.

Exemples d’embeddings :

  • Word2Vec : Développé par Google, il apprend des représentations vectorielles des mots à partir de leur contexte dans de grands corpus de texte.
  • GloVe (Global Vectors for Word Representation) : Il cherche à exploiter les statistiques globales d’un corpus pour obtenir des embeddings qui capturent les relations linéaires entre les mots.
  • BERT (Bidirectional Encoder Representations from Transformers) : Il utilise des embeddings contextuels, ce qui signifie que la représentation d’un mot peut changer en fonction de son contexte dans une phrase.

L’importance des chunks

Dans le modèle Retriever-Augmented Generation (RAG), la gestion de l’information et la manière dont elle est préparée et traitée sont essentielles pour assurer la qualité des réponses générées. Un concept clé dans ce contexte est l’utilisation des “chunks”, ou morceaux de texte, qui jouent un rôle essentiel dans l’efficacité du processus de récupération des informations. Dans ce chapitre, je vais expliquer pourquoi les chunks sont importants dans le RAG et comment ils améliorent la performance du système.

Qu’est-ce qu’un Chunk ?

Dans le cadre du RAG, un chunk est une portion de texte extrait d’un document plus large. Ces chunks sont créés en segmentant les documents en sections plus petites et gérables, qui peuvent être individuellement analysées et comparées aux requêtes des utilisateurs. Cette segmentation aide à focaliser la recherche d’informations et à réduire la complexité des données traitées par le modèle.

Rôle des Chunks dans le RAG

Les chunks permettent au modèle de retriever de travailler plus efficacement. En divisant un grand document en petits segments, le système peut cibler plus précisément les zones qui contiennent les informations les plus pertinentes par rapport à la requête de l’utilisateur. Cela est particulièrement utile dans les cas où les documents source sont volumineux et contiennent une multitude d’informations.

Avantages de l’Utilisation des Chunks

  1. Amélioration de la précision : En travaillant avec des chunks, le retriever peut identifier plus justement les parties d’un document qui correspondent à la requête. Cela limite les risques de surcharge d’information et augmente la pertinence des données récupérées.

  2. Optimisation des performances : Les chunks réduisent la quantité de données que le générateur doit traiter en une seule fois, ce qui peut accélérer le traitement des requêtes et réduire la charge sur les ressources informatiques.

  3. Gestion efficace des mises à jour : Les chunks permettent une mise à jour plus facile et plus rapide des bases de données. Si seulement certaines parties d’un document doivent être mises à jour, seul le chunk pertinent doit être remplacé, ce qui est plus efficace que de devoir retraiter de grands documents entiers.

Exemple d’Application

Plutôt que d’utiliser un simple fichier PDF, je vais utiliser le site de documentation d’Outscale comme exemple. Je vais me placer dans le cas où je n’ai pas accès au code source du site. Je vais donc aspirer le site avec wget.

Terminal window
wget -mpEk https://docs.outscale.com/

Lorsqu’il s’agit de gérer de grandes quantités de données dans le cadre du Retriever-Augmented Generation (RAG), la découpe de ces documents en segments basés sur les niveaux de titres offre une méthode efficace pour améliorer la précision des réponses générées. Plutôt que d’utiliser un loader du framework LangChain, je vais utiliser beautifulSoup (alias bs4). Après une rapide analyse des fichiers html j’ai écrit ce code :

#!/usr/bin/env python3
from langchain_community.embeddings import SentenceTransformerEmbeddings
from pathlib import Path
from urllib.parse import urlparse, urlunparse, urljoin
from concurrent.futures import ThreadPoolExecutor
from langchain.docstore.document import Document
from langchain_community.vectorstores import Chroma
import bs4
import shutil
import os
import re
def cleaned_link(url, link):
link = urljoin(url, link["href"])
return link
def cleaned_image(url, image):
image = urljoin(url, image["src"])
return image
def extract_docs(url, html):
soup = bs4.BeautifulSoup(html, "html.parser")
title = ""
if soup.find("h1") is not None:
title = soup.find("h1").text.strip()
parsed = urlparse(url)
article = soup.find("article", class_="doc")
cleaned_links = []
sections = []
if article is not None:
links = article.find_all("a")
for link in links:
if link.get("href") != None and not link.get("href").startswith("#"):
cleaned_links.append(cleaned_link(url, link))
for section in soup.select("#preamble, .sect1"):
header = section.find("h2")
section_links = [
cleaned_link(url, link)
for link in section.find_all("a")
if link.text != ""
]
anchor = section.find("a", class_="anchor")
section_url = urlunparse(
(
parsed.scheme,
parsed.netloc,
parsed.path,
parsed.params,
parsed.query,
anchor.get("href").replace("#", "") if anchor else "preamble",
)
)
code_blocks = []
for pre in section.find_all("div", class_="listingblock"):
language = pre.find("code").get("data-lang")
section_title = pre.find("div", class_="title")
code = pre.find("code")
if code:
code_blocks.append(
{
"language": language if language is not None else None,
"title": (
section_title.text.strip()
if section_title is not None
else None
),
"code": code.text,
}
)
sections.append(
{
"url": section_url,
"title": header.text.strip() if header is not None else title,
"text": re.sub(r"\n+", "\n", section.text).strip(),
"anchor": anchor.get("href") if anchor else None,
"links": section_links,
"images": [
cleaned_image(url, img) for img in section.find_all("img")
],
"code": code_blocks,
}
)
return {"url": url, "title": title, "links": cleaned_links, "sections": sections}
# Function to process a batch of files
def process_batch(url, files):
batch_docs = []
for file in files:
with open(file, "rb") as f:
batch_docs.append(
extract_docs(
url + "/" + os.path.basename(file),
f.read().decode("utf-8", "ignore"),
)
)
return batch_docs
root_directory = "./docs.outscale.com/en/userguide"
# Set the batch size (number of files to process in each batch)
batch_size = 100
# Initialize an empty list to store loaded documents
docs = []
# Get the list of files to process
files_to_process = []
for root, dirs, files in os.walk(root_directory):
files_to_process.extend(
[os.path.join(root, file) for file in files if file.lower().endswith(".html")]
)
# Create a ThreadPoolExecutor for parallel processing
with ThreadPoolExecutor() as executor:
total_files = len(files_to_process)
processed_files = 0
# Iterate through the files in batches
for i in range(0, total_files, batch_size):
batch = files_to_process[i : i + batch_size]
batch_docs = list(
executor.map(
process_batch, ["https://docs.outscale.com/en/userguide"], [batch]
)
)
for batch_result in batch_docs:
docs.extend(batch_result)
processed_files += len(batch)
print(f"Processed {processed_files} / {total_files} files")
all_sections = []
for doc in docs:
for section in doc["sections"]:
current_section = {
"title": section["title"],
"content": section["text"],
"source": section["url"],
}
all_sections.append(current_section)

Explication du code :

  1. Chargement du Document : Le document est chargé et une recherche du titre de niveau 1 est lancé pour récupérer le titre du document.
  2. Recherche de l’article : On recherche l’article
  3. Boucle sur les sections : On recherche dans l’article les sections pour détecter les titres de niveau 2.
  4. Extraction des Sections : Une fois les titres identifiés, on recherche tous les liens, toutes les images et toutes les sections de code (qui seront utilisés dans une prochaine version).
  5. Conversion en Chunks Textuels : Les sections extraites sont ensuite converties en texte brut, prêtes à être traitées ou indexées.

Choix de la base de données pour stocker les vecteurs

Une fois que les documents sont transformés en vecteurs par l’embedding, ces derniers doivent être stockés dans une base de données conçue pour supporter des recherches rapides et efficaces. Le choix de cette base de données est également important. Elle doit non seulement être capable de gérer le volume et la complexité des vecteurs, mais aussi offrir des temps de réponse rapides pour les requêtes.

Dans ce contexte, Chroma est une option idéale. Chroma est une base de données de vecteurs conçue pour être facilement intégrée dans les applications d’IA et offre des fonctionnalités optimisées. Elle permet d’indexer des documents en utilisant différents types d’embeddings et d’effectuer des recherches rapides, soutenant ainsi la fluidité et la réactivité des systèmes basés sur le RAG.

text_docs = [
Document(page_content=section.pop("content"), metadata=section)
for section in all_sections
]
embeddings = SentenceTransformerEmbeddings(
model_name="sentence-transformers/all-mpnet-base-v2"
)
persist_directory = "./chroma"
dirpath = Path(persist_directory)
if dirpath.exists() and dirpath.is_dir():
shutil.rmtree(dirpath)
db = Chroma.from_documents(
text_docs,
embeddings,
collection_name="outscale",
persist_directory=persist_directory,
)
db.persist()

Conclusion

Au fil de ce billet, nous avons exploré en détail le fonctionnement et les applications du Retriever-Augmented Generation (RAG) pour améliorer l’indexation de la documentation technique. En comparant le RAG avec les modèles de langage de grande taille traditionnels, il devient évident que le RAG offre des avantages significatifs en termes de précision, de personnalisation et de mise à jour des connaissances, surtout dans des domaines où l’accès rapide à des informations actualisées est indispensable.

Nous avons également discuté de l’importance de découper les documents en chunks pour optimiser les processus de recherche et de récupération d’informations, en soulignant comment cette pratique peut améliorer la gestion des grands volumes de texte et la précision des réponses fournies. Bien que le framework Langchain apporte directement cette fonctionnalité, je vous ai proposé une alternative pour optimiser le découpage en chunks.

Le choix des embeddings est hyper important pour optimiser les performances du Retriever-Augmented Generation (RAG), car il influence directement la qualité de la récupération des informations et, par conséquent, la pertinence des réponses générées. Les embeddings jouent le rôle de transformer des textes en vecteurs numériques que les machines peuvent comprendre et comparer. Cela permet de mesurer la similarité sémantique entre la requête de l’utilisateur et les données stockées, ce qui est essentiel pour identifier les informations les plus pertinentes à retourner.

L’aventure ne s’arrête pas ici. Dans la deuxième partie de notre série, nous plongerons dans le développement pratique d’une interface de chatbot en utilisant ChainLit, une plateforme innovante pour construire des applications interactives basées sur le traitement du langage naturel. Nous aborderons comment intégrer le RAG dans une interface utilisateur conviviale, permettant ainsi aux utilisateurs de dialoguer efficacement avec le système pour obtenir des informations précises et contextualisées.

Restez à l’écoute pour cette prochaine partie où nous transformerons la théorie en pratique, en créant une interface qui non seulement répond aux questions, mais le fait d’une manière intuitive et engageante. Nous verrons comment ChainLit peut faciliter ce processus, rendant accessible à tous la puissance des technologies de langage avancées.