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.
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.
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.
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.
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.
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
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 :
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.
Recommandé par LinkedIn
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.