Aller au contenu principal

Maîtriser le RAG

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 RAG utilise une combinaison de deux composants principaux : un système de récupération d'informations (le retriever) et un modèle de génération de texte (le generator). Le retriever est conçu pour rechercher rapidement des informations pertinentes dans une vaste base de données textuelle, tandis que le generator utilise ces informations pour construire des réponses cohérentes et contextuellement adaptées. Cette synergie permet de produire des réponses qui non seulement répondent de manière pertinente à la question posée, mais qui sont également enrichies par des détails spécifiques et vérifiables extraits des données de référence.

Flexibilité et personnalisation

Le RAG offre une grande flexibilité dans sa mise en œuvre. Les développeurs peuvent personnaliser le retriever pour qu'il s'adapte à différentes sources d'informations ou bases de données, ce qui est crucial pour des applications spécifiques où les données peuvent varier considérablement en format et en complexité. De plus, le modèle de génération peut être entraîné pour adopter un style de réponse spécifique ou pour respecter certaines contraintes linguistiques, ce qui est particulièrement utile pour les chatbots représentant des marques ou des entités spécifiques.

Apprentissage continu

Une autre caractéristique importante du RAG est sa capacité à apprendre de manière continue à partir de nouvelles données sans nécessiter un ré-entrainement complet du modèle. Cela permet au système de s'adapter et de s'améliorer au fil du temps, en intégrant de nouvelles informations et en affinant ses capacités de réponse en fonction des interactions réelles avec les utilisateurs.

Scalabilité

Grâce à sa structure modulaire, le RAG est hautement scalable. Il peut être déployé à grande échelle pour gérer des volumes élevés de requêtes simultanées, ce qui est essentiel pour les applications commerciales où les demandes peuvent être imprévisibles et massives. Cette scalabilité est soutenue par la capacité du retriever à gérer efficacement de grandes bases de données de documents et par l'efficacité du generator à produire des réponses en temps réel.

Intégration transparente

Le modèle RAG a été conçu pour être facilement intégré dans des architectures d'IA existantes. Il peut être combiné avec d'autres technologies d'IA, comme les systèmes de reconnaissance vocale ou les interfaces utilisateur graphiques, pour créer des solutions complètes et interactives. Cette intégration transparente facilite l'adoption du RAG dans des environnements technologiques complexes et diversifiés.

Ces fonctionnalités font du RAG un outil extrêmement puissant et polyvalent, capable de transformer la manière dont les chatbots et autres systèmes interactifs comprennent et traitent l'information, tout en offrant une expérience utilisateur améliorée grâce à des réponses plus précises et informatives.

Pourquoi utiliser le RAG pour votre documentation technique ?

L'utilisation du Retriever-Augmented Generation (RAG) dans le traitement de la documentation technique présente plusieurs avantages significatifs qui peuvent révolutionner la façon dont les systèmes d'assistance automatisés interagissent avec les utilisateurs. Voici pourquoi le RAG est particulièrement adapté à ce contexte.

Amélioration de la précision des réponses

L'un des principaux atouts du RAG est sa capacité à fournir des réponses extrêmement précises. Grâce au retriever, le système a accès à une vaste base de données de documentation technique, lui permettant de récupérer des informations spécifiques et détaillées en réponse à des questions complexes. Cela est particulièrement utile dans des domaines où les réponses nécessitent une grande exactitude, comme dans les secteurs de l'ingénierie, de la technologie de l'information ou de la santé.

Réduction du temps de réponse

Avec le RAG, les réponses aux questions techniques peuvent être générées beaucoup plus rapidement qu'avec les méthodes traditionnelles de recherche manuelle. Le processus automatisé de récupération et de génération de réponse diminue considérablement le délai entre la pose d'une question et la fourniture d'une réponse, ce qui améliore l'efficacité des interactions utilisateur et la satisfaction globale.

Personnalisation des interactions

Le RAG permet une personnalisation poussée des réponses en fonction du contexte spécifique de chaque utilisateur. Par exemple, un chatbot utilisant le RAG peut adapter ses réponses en fonction des préférences linguistiques, du niveau de compréhension technique de l'utilisateur, ou même des interactions précédentes, offrant ainsi une expérience utilisateur beaucoup plus riche et personnalisée.

Capacité à répondre à un large éventail de questions

La flexibilité du RAG en fait un outil idéal pour gérer un large spectre de questions techniques, des plus basiques aux plus complexes. Cette polyvalence est essentielle dans un contexte où les utilisateurs peuvent avoir des niveaux de compétence très variés et des besoins d'information diversifiés.

Intégration facile avec d'autres outils

Le modèle RAG peut être facilement intégré avec d'autres technologies, comme les bases de données existantes, les systèmes de gestion de contenu, ou même d'autres outils d'IA. Cette intégrabilité facilite l'adoption du RAG dans des infrastructures techniques déjà en place, permettant ainsi une transition en douceur vers des systèmes de réponse automatisés plus sophistiqué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 crucial 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 crucial 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.

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 crucial.

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.