Architecture et Implémentation de la génération augmentée par récupération (RAG) avec Spring AI

Architecture et Implémentation de la génération augmentée par récupération (RAG) avec Spring AI

Dans le domaine en constante évolution de l’IA générative, la génération augmentée par récupération occupe une position centrale, bouleversant ainsi notre manière d'interagir avec l'information. Les applications intégrant RAG repoussent les frontières de l'expérience utilisateur en combinant harmonieusement des données externes récupérées avec un modèle génératif. Cela permet d’atteindre des niveaux de performance et de précision inédits dans la génération de contenus personnalisés.

Dans cet article, nous explorerons l'architecture et le fonctionnement de RAG, ainsi que son implémentation avec Spring AI, en mettant en lumière le concept d'advisor, élément essentiel à cette implémentation.

Note: Cet article est le deuxième de ma série consacrée à Spring AI. Si vous êtes déjà familier avec les concepts de base, vous pouvez poursuivre votre lecture. Dans le cas contraire, je vous conseille de commencer par lire le premier article, qui couvre les fondamentaux de Spring AI.

Pourquoi a-t-on besoin d’améliorer la performance d’un modèle  ?

Bien que les LLM soient extrêmement performants en génération de contenus, leur qualité dépend directement des données sur lesquelles ils ont été entraînés. Étant entraînés sur des données publiques, ces modèles peuvent parfois produire des résultats inattendus, appelés "hallucinations", surtout lorsqu'ils tentent de générer du contenu basé sur des informations qu'ils n'ont jamais rencontrées auparavant.

Diverses techniques ont été développées pour booster les performances des LLM. Parmi celles-ci, on retrouve le prompt engineering, le RAG, le fine-tuning, et bien d'autres. Il est important de souligner que ces techniques peuvent être combinées en fonction des besoins, elles ne sont donc pas exclusives les unes des autres.

Pourquoi choisir une technique plutôt qu'une autre ?

Le fine-tuning et le RAG sont les deux techniques les plus utilisées lorsqu'il s'agit de faire parvenir à un modèle des nouvelles informations.

Le fine-tuning consiste à adapter un modèle pré-entraîné en l'entraînant à nouveau sur des nouvelles données spécifiques, ce qui peut être efficace pour des tâches bien définies mais nécessite souvent des ressources computationnelles importantes et des données en quantité considérable. 

RAG de l’autre côté combine un modèle génératif avec une étape de récupération d'informations à partir de sources externes en temps réel, ce qui permet au modèle d'accéder à des données récentes sans avoir besoin d'un nouvel entraînement complet.

Choisir RAG au lieu de fine-tuning est avantageux lorsque l'on désire maintenir une flexibilité et une pertinence contextuelle, tout en minimisant les coûts liés au fine-tuning.

Étant l'une des solutions les plus populaires et accessibles, le RAG fera l'objet du reste de cet article.

Architecture fonctionnelle de RAG

RAG est  conçu pour extraire des informations factuelles à partir d'une base de connaissances externe, permettant ainsi aux LLM d'accéder aux données les plus précises et à jour. Il repose sur un concept fondamental, à savoir les bases de données vectorielles.

illustration de l'augmentation d'un prompt avec RAG

Base de données vectorielle

Une base de données vectorielle stocke l'ensemble des données nécessaires à transmettre à un modèle, afin qu'il puisse les intégrer lors de la génération de ses réponses.

Embeddings

Les données enregistrées dans une base de données vectorielle sont stockées sous une forme spécifique appelée embedding. Un embedding est une représentation numérique d'un contenu (qu'il s'agisse d'image, de texte, etc.). Cette représentation permet de réaliser des recherches basées sur la similarité entre les contenus, plutôt que sur des mots-clés, comme c'est le cas dans les bases de données traditionnelles.

Recherche de similarité 

Ce qui rend les bases de données vectorielles très efficaces, c’est la possibilité d’effectuer des recherches par similarité. Il existe plusieurs méthodes de recherches de similarité, parmi lesquelles nous avons le cosine. Il mesure la similarité entre deux vecteurs en déterminant le cosinus de leur angle.

image donnant la similarité entre les vecteurs en fonction de l'angle de leur cosinus

Implémentation de RAG dans Spring AI 

Dans la suite de cet article, j'utiliserai une étude de cas concernant une entreprise fictive, Fly Intelligent, qui souhaite équiper son assistant IA de capacités de RAG.

L’entreprise possède plusieurs documents au format PDF (la mission de l’entreprise et les questions fréquemment posées) auxquels elle souhaite que le modèle puisse accéder lors des réponses aux clients.

J’utiliserai les modèles d’OpenAI, GPT-4o pour le chat et text-embedding-3-small pour les embeddings. Le code des exemples utilisés dans cet article sera disponible dans ce repository GitHub.

Mise en place d'une base de données vectorielle

Spring AI propose une API abstraite pour interagir avec les bases de données vectorielles via l'interface VectorStore. Ce qui permet facilement de migrer d’une base de données vectorielle à une autre. Si vous avez utilisé l’interface DataSource, le principe est le même.

Code de source de l'interface VectorStore

Pour utiliser une base de données spécifique, il suffit d’ajouter ses dépendances. 

Dans cet article j’utilise PGvector, une extension open source pour PostgreSQL qui permet de stocker et de rechercher sur des embeddings. Pour l’utiliser il suffit d’ajouter cette dépendance.

Dépendance maven pour utiliser PGVector

Configuration d'un modèle d’embedding

Afin d’enregistrer les informations dans la base de données vectorielle, Spring a besoin qu’un modèle d’embedding soit configuré. En fonction du fournisseur de modèle utilisé, Spring peut configurer automatiquement un bean de type EmbeddingModel. 

Comme je travaille avec OpenAI, ce bean a été automatiquement créé pour moi en utilisant le modèle text-embedding-3-small que j'ai ajouté dans application.properties. 

L’image ci-dessous montre à quoi ressemble l’embedding de la première page du FAQ de Fly Intelligent.

Exemple de représentation d'embedding

Note : Tous les fournisseurs de modèles n'ont pas leur propre modèle d'embedding. Il est donc nécessaire de choisir un modèle d'embedding et d'ajouter ses dépendances. Il faut aussi noter que les embeddings créés par un modèle d'un fournisseur peuvent ne pas être compatibles avec ceux d'un autre fournisseur.

Extraction, transformation et chargement des documents 

Image montrant le chargement des documents dans une base de données vectorielle

Pour l'exemple de Fly Intelligent, les documents disponibles ne sont pas utilisables en l’état. Il sera nécessaire de stocker leur contenu dans une base de données vectorielle afin d’effectuer des recherches de similarité lorsque le modèle reçoit une question.

À cet effet, Spring propose un mini framework appelé ETL (Extract, Transform, Load). Il permet d'extraire, transformer et charger des documents dans une base de données vectorielle, assurant ainsi que les données sont dans un format optimal pour être récupérées par le modèle d'IA.

Pour utiliser ETL, il est nécessaire d'ajouter des dépendances adaptées aux types de documents que nous manipulons. Dans cet article, je travaille uniquement avec des documents PDF, j'ai donc ajouté la dépendance suivante :

Dépendance maven pour extraire le contenu des documents PDF

Extraction

La plupart des informations que nous souhaitons charger proviennent des documents. En fonction de leur typologie, nous disposons de plusieurs interfaces pour extraire les informations. Parmi ces interfaces, nous avons JsonReader pour les fichiers json, TextReader pour les fichiers textuels, etc.

La méthode suivante utilise PagePdfDocumentReader pour extraire les informations d’un document PDF en se basant sur un découpage par page. 

Transformation

Une fois les documents récupérés, nous avons la possibilité d’apporter des transformations avant de les enregistrer. Il existe plusieurs interfaces pour cela. TextSplitter permet d’effectuer des transformations sur un document de telle sorte qu’il puisse tenir dans le contexte du modèle.

Dans l’exemple suivant j’utilise TokenTextSplitter qui permet de diviser les mots d’un document en gardant l’intégrité des tokens constituant le document.

Chargement

Durant le chargement, l'embedding de chaque document est créé grâce au modèle d’embedding. Ensuite l’embedding ainsi que le texte original sont enregistrés dans la base de données vectorielle. 

Dans la capture ci-dessous, on charge les documents (FAQ et mission) de Fly Intelligent dans une base donnée vectorielle en utilisant la méthode accept de VectorStore.

Une fois que les documents sont bien chargés, on peut alors effectuer des récupérations pour enrichir les prompts des utilisateurs.

Augmentation d'un prompt avec les données externes

On peut augmenter un prompt de plusieurs façons. On peut y ajouter des données contextuelles, comme des informations stockées dans une base de données vectorielle ou l'historique des précédentes conversations. Pour faciliter cette tâche, Spring AI a introduit le concept d'advisor.

Un Advisor est une classe qui permet de modifier le prompt avant son envoi au modèle et d’ajuster la réponse avant de la renvoyer à l’utilisateur. Il existe plusieurs advisors standards créés pour les cas d’usage courants. On a également la possibilité de créer des advisors personnalisés.

Utilisation de QuestionAnswerAdvisor pour récupérer des documents similaires

Une fois  qu’on a mis en place une base données vectorielle, on peut implémenter le RAG en utilisant QuestionAnswerAdvisor. 

Le QuestionAnswerAdvisor interroge la base de données vectorielle pour trouver des documents similaires à la question de l’utilisateur. Il ajoute ensuite ces documents et la question de l’utilisateur dans le prompt pour fournir un contexte, aidant ainsi le modèle à générer une réponse pertinente.

Dans l’exemple de Fly Intelligent, sans utilisation de l’advisor, lorsqu’on demande au modèle :

On obtient cette réponse  qui n’a aucun lien avec Fly Intelligent.

Il nous donne cette réponse parce qu’il n'a pas eu accès aux informations de Fly Intelligent lors de son entraînement. 

Pour permettre l’accès aux informations, on peut utiliser le QuestionAnswerAdvisor. Pour le créer, nous avons besoin d’une base de données vectorielle, ici PGVector, et d’une SearchRequest pour ajouter les critères de recherche avec la question de l’utilisateur. Ici on utilise les critères par défaut.

Et comme par hasard, on obtient une réponse pertinente qui provient directement de nos sources d'information.

On peut affiner la réponse en utilisant des messages systèmes, par exemple ici on demande au modèle de résumer les informations qu’on récupère de la base de données vectorielle avant de renvoyer la réponse à l’utilisateur.

Et là on obtient la réponse suivante : 

Avec Spring AI, le plus grand travail lors de l'implémentation de RAG est le chargement des documents. Une fois fait, on peut jouer avec le prompt engineering et le SearchRequest de QuestionAnswerAdvisor pour tailler le modèle de chat à nos besoins et goûts.

Historisation de chat avec MessageChatMemoryAdvisor 

Comme les modèles d'IA n'ont pas un état fixe, ils ne se rappellent pas des questions qui les ont été posées auparavant.

Par exemple en donnant mon nom à un modèle : 

Il me répond par : 

Et ensuite je lui demande quel est mon nom :

Pour résoudre ce problème, le concept de mémoire a été mis en place afin de permettre aux modèles d’avoir accès à l'historique des conversations. 

Ce procédé consiste à inclure l'historique des conversations précédentes dans chaque prompt envoyé. Dans Spring AI, ce concept est implémenté à travers le ChatMemory et les Advisors. Actuellement, il existe deux types de ChatMemory : InMemoryChatMemory et CassandraChatMemory.

Pour permettre au modèle de se rappeler de mon nom, j’ai utilisé le InMemoryChatMemory et un MessageChatMemoryAdvisor : 

Le CHAT_MEMORY_CONVERSATION_ID_KEY permet d’enregistrer les conversations en mémoire avec une clé unique par utilisateur, et CHAT_MEMORY_RETRIEVE_SIZE_KEY pour limiter le nombre de précédentes conversations à inclure dans le prompt.

Maintenant, si je demande encore au modèle, quel est mon nom on a cette réponse :

Conclusion

RAG est une méthode très pratique pour améliorer l'efficacité d'un modèle. Pour y parvenir, il est essentiel de récupérer les documents similaires avec une grande certitude, ce qui rend crucial le choix du modèle d'embedding.

L'utilisation d'une base de données vectorielles est indispensable pour son implémentation. Le processus de stockage des documents a été simplifié grâce à l’utilisation des pipelines ETL.

Enfin, pour enrichir un prompt avec les documents similaires, il suffit d’utiliser le QuestionAnswerAdvisor. De plus, Spring AI propose d’autres types d’advisors pour différentes fonctionnalités.

Le code des exemples de cet article se trouve sur ce repository GitHub.

Le prochain article de cette série portera sur les appels des fonctions. Nous verrons ainsi comment dynamiser un modèle afin qu’il puisse accéder à nos API au moment opportun. 

Ressources

Spring AI | Architecture RAG | Bases de données vectorielles | OpenAI

Identifiez-vous pour afficher ou ajouter un commentaire

Plus d’articles de Ali Ibrahim

Autres pages consultées

Explorer les sujets