Guillaume Duvernay

Conseils concrets pour passer d'un RAG simple à un meilleur RAG

RAGn8nagentsAI efficiency

Presque tous les tutoriels RAG sur n8n aboutissent au même schéma : un agent IA, auquel est rattaché un nœud de stockage vectoriel utilisé directement comme outil. J’ai monté cette configuration maintes fois. Ça fonctionne, et pendant un temps, je ne me suis pas posé de questions.

Puis j’ai commencé à lui poser des questions plus complexes, et trois problèmes sont apparus, tous issus de la même cause. L’agent gère la récupération, et vous n’avez aucun contrôle.

Ceci est la version écrite d’une vidéo, avec le même cours de biologie et les mêmes chiffres.

Le schéma de départ classique

Ma base de connaissances pour cet exemple est un cours de biologie, vectorisé dans Supabase. Si on lui demande « qu’est-ce qu’une cellule », il répond très bien. C’est la démo que tout le monde montre, et ce n’est pas un mensonge : les questions simples fonctionnent.

Trois sous-nœuds, dont la récupération. Tout ce qui se passe dans cette troisième boîte est décidé à l’exécution par l’agent.

Voici ce que vous contrôlez dans cette configuration : une description de l’outil, indiquant à l’agent quand l’utiliser. C’est toute la surface de réglage.

La colonne de gauche est toute la surface de configuration. Tout ce qui est à droite est décidé pour vous.

Vous ne voyez jamais la requête

L’agent écrit la requête lui-même et l’envoie. Vous pouvez essayer de l’orienter via la description de l’outil et espérer. Sur une question nécessitant une reformulation, ou lorsque la formulation de l’utilisateur est très différente de celle de vos documents, vous ne le découvrez qu’en consultant le journal d’exécution après coup.

Une seule requête par appel

Une question réelle n’est souvent pas une question unique. Demandez quelque chose qui nécessite la définition d’un terme, puis la mise en relation de ce terme avec un autre, et une seule recherche de similitude devra couvrir les deux à la fois. Elle retournera alors des chunks médiocres pour les deux, plutôt que pertinents pour l’un ou l’autre.

L’agent peut appeler l’outil deux fois, mais cela représente deux tours complets du modèle, et il ne s’en donne généralement pas la peine.

Quatre chunks sont retournés, peu importe la question

C’est ce point qui m’a poussé à changer la configuration. Le vector store retourne les quatre chunks les plus proches. Les plus proches, pas forcément proches.

Demandez à mon cours de biologie quand nous dînons ce soir, et vous recevrez quand même quatre chunks. Ils sont loin, mais ce sont les quatre les moins loin, donc ils remontent. L’agent les reçoit sans aucune indication qu’ils sont inutiles, et les modèles sont conciliants, donc il les traite comme du matériel pertinent.

Même format de réponse qu’une bonne réponse. Rien n’indique à l’agent qu’il s’agit des meilleurs éléments d’un ensemble médiocre.

Remplacer l’outil par un sous-workflow

La solution consiste à ne plus donner le vector store à l’agent, mais à lui donner un sous-workflow à la place. Dans n8n, il s’agit de l’outil Call n8n Workflow : l’agent l’appelle, un workflow s’exécute sur son propre canevas, et l’agent reçoit ce que le dernier nœud produit.

Côté agent, le changement est minime. Même déclencheur, même agent, et le vector store est remplacé par deux outils.

L’agent tourne ici sur gpt-5-mini, pas sur un modèle de pointe. Une fois que la récupération a fait le tri, le travail de l’agent consiste à rédiger une réponse à partir de ce qu’on lui a donné, et ce n’est pas la partie difficile.

C’est dans le sous-workflow que se trouve toute la nouvelle surface de contrôle. Chaque bloc ci-dessous est une étape que vous avez écrite, et que vous pouvez ouvrir dans le journal d’exécution après coup.

Le filtre et l’IF adjacent constituent tout l’argument. L’un décide de ce qui est assez bon, l’autre s’assure qu’une requête n’ayant rien trouvé le dise explicitement au lieu de rester silencieuse.

Un tableau de requêtes, en un seul appel d’outil

Le déclencheur du sous-workflow accepte un champ appelé queries. La description de l’outil indique à l’agent qu’il s’agit d’un tableau de un à cinq éléments, et le prompt système dit la même chose dans l’autre sens : plus la question de l’utilisateur est complexe, plus vous devez la diviser en sous-requêtes, jusqu’à cinq.

C’est là que réside la partie multi-requête, et cela coûte un seul appel d’outil au lieu de trois.

Ici, l\'agent a décidé de la répartition lui-même. Je lui ai seulement indiqué que le champ acceptait jusqu\'à cinq éléments.

Le score est l’élément central

Dans le sous-workflow, Supabase est un nœud classique, je mappe donc la requête moi-même et la réponse contient un score de similarité. Le filtre élimine tout ce qui est égal ou inférieur à 0,4.

Deux détails m’ont fait perdre du temps. Le filtre doit être configuré pour toujours sortir des données, sinon une requête où rien ne passe ne produit aucun élément et la branche suivante ne s’exécute jamais. Et le IF qui suit vérifie si un chunk a survécu, car c’est le cas que l’on veut gérer explicitement plutôt que par le silence : l’agent reçoit alors une phrase indiquant que la base de connaissances n’a rien contenu d’utile pour cette requête.

Indiquer à l’agent la provenance d’un chunk

L’étape de nettoyage est plus importante qu’il n’y paraît. Supabase renvoie des métadonnées dont je n’ai pas besoin, comme le type de contenu du fichier source, et cela consomme des tokens pour chaque chunk de chaque requête.

Ce que je conserve, c’est le texte du chunk plus deux champs : le chapitre dont il est issu et son score de pertinence arrondi à deux décimales. Les deux sont transmis à l’agent. Le nom du chapitre lui permet de citer sa source, et la transmission du score permet à l’agent de voir que l’une de ses cinq requêtes a atteint 0,42 tandis qu’une autre a atteint 0,81, et de les pondérer en conséquence.

Nommer les données retournées

L’agrégation finale ne produit pas un bloc anonyme. Elle génère un champ appelé Knowledge base retrieval, et à l’intérieur, chaque entrée associe Query to the knowledge base à Chunks returned.

Cette association est fondamentale. L’agent a envoyé trois requêtes, il reçoit trois groupes étiquetés et peut ainsi savoir quels chunks répondent à quelle partie de sa propre question. Si vous lui donnez un tableau plat de douze chunks, il devra en déduire la correspondance, ce qu’il fera, parfois mal.

Nommer les champs nécessite un nœud Set. C’est l’élément le moins coûteux de cette liste et celui que j’ai ignoré le plus longtemps.

Une étape de réflexion avant la réponse

L’autre changement concerne l’agent lui-même. Je lui ai donné un outil de réflexion, et le prompt système lui ordonne d’utiliser cet outil dès que la récupération est terminée : analyser la question par rapport aux résultats et vérifier s’il dispose réellement de ce dont il a besoin pour répondre.

La description de l’outil limite la réponse à 50 mots. Sans cela, il écrit des paragraphes, et réfléchir à voix haute sur quatre chunks ne justifie pas une page de tokens.

Vous pouvez lire ces notes dans le journal d’exécution, l’aspect que je n’espérais pas apprécier autant. Lorsqu’une réponse est erronée, les notes indiquent généralement si c’est la récupération qui était mauvaise ou le raisonnement.

Les dernières lignes du prompt système sont celles que je garderais si je ne pouvais en garder que deux. Répondre uniquement à partir du contenu du cours, et si une question sort de ce cadre, rediriger plutôt que répondre. Et si vous n’avez pas les informations nécessaires, dites-le plutôt que de répondre en utilisant vos connaissances générales.

Tout cela ne fonctionne que parce que la récupération est honnête lorsqu’elle revient vide. Une instruction demandant d’admettre son ignorance est inutile si chaque requête renvoie quatre chunks qui ressemblent à des preuves.

Ce que cela enseigne réellement

Chaque correction ici est la même. Quelque chose était décidé à ma place, je l’ai donc déplacé vers une étape que je maîtrise.

C’est plus précieux que le workflow lui-même. Désormais, quand je travaille avec Claude Code, les questions que je pose sont celles que cette configuration m’a forcé à me poser : qu’est-ce qui a été recherché exactement, quelle quantité de données est revenue, quelle part était utile, et qu’est-ce qui encombre la fenêtre de contexte inutilement. Un agent générique masque tout cela par défaut, et lira volontiers vingt fichiers pour répondre à une question qui n’en nécessitait que deux.

C’est en construisant soi-même la récupération des données, une fois, que l’on apprend à être attentif.

Sources