---
title: "Conseils concrets pour passer d'un RAG simple à un meilleur RAG"
description: "La configuration classique ne vous donne aucun contrôle sur la requête : une seule requête par appel et quatre segments retournés. Voici six changements pour y remédier, du multi-query au seuil de pertinence, nœud par nœud sur n8n."
date: 2025-08-24
language: fr
canonical: https://gduv.club/fr/articles/better-rag
source: gduv.club
---
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.

[Améliorez vos agents RAG n8n : Multi-Query et Raisonnement (avec Supabase + GPT-5)](https://www.youtube.com/watch?v=rKTM_SWLHLI)

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

**Agent IA**

**Lors de la réception d'un message** → **Agent IA** (Modèle de chat: Modèle de chat OpenAI; Mémoire: Mémoire simple; Outil: Stockage vectoriel Supabase (L'agent écrit la requête, quatre segments sont retournés))

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

**Ce que vous réglez** (Une seule fois, au build)

- Une description de l’outil
- Le nombre de chunks à retourner

**Ce que l’agent décide** (À chaque appel)

- Le texte exact de la requête envoyée au vector store
- Le fait qu’il n’y ait qu’une seule requête, jamais deux
- Rien concernant le degré de proximité requis pour un chunk

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

**Quatre chunks retournés pour une question sans rapport, avec des scores de similitude entre 0,11 et 0,19, tous transmis à l’agent comme s’ils étaient pertinents.**

  <div class="dg-json">
    <p class="dg-label">Requête : "quand dînons-nous ce soir"</p>
    <div class="dg-box dg-box--bad">
      <span class="dg-box__title">4 chunks retournés</span>
      <span class="dg-box__note">Transport membranaire cellulaire <span class="dg-json__score">0.19</span></span>
      <span class="dg-box__note">Mitose, chronologie des phases <span class="dg-json__score">0.16</span></span>
      <span class="dg-box__note">Cinétique enzymatique <span class="dg-json__score">0.13</span></span>
      <span class="dg-box__note">Photosynthèse <span class="dg-json__score">0.11</span></span>
    </div>
    <p class="dg-note">Les scores existent dans le vector store. L’outil ne vous les transmet pas.</p>
  </div>

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

**Agent IA**

**Quand un message de chat est reçu** → **Agent IA** (gpt-5-mini, Modèle de chat: Modèle de chat OpenAI (gpt-5-mini); Mémoire: Mémoire simple (Les 8 derniers tours); Outil: Interroger la base de connaissances (Le sous-workflow. Prend un tableau de 1 à 5 requêtes); Outil: Think (Utilisé juste après la récupération, 50 mots max))

_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 sous-workflow de récupération : un déclencheur prenant un tableau de requêtes, un Split Out, une boucle, un nœud vector store Supabase avec embeddings attachés, une étape de nettoyage, un filtre à 0,4, et un IF qui agrège les chunks ou écrit une phrase indiquant qu’aucun résultat ne correspond, avant de boucler.**
 0,4',
      note: 'Filtre. Produit toujours des données, donc une requête sans correspondance continue quand même',
      tone: 'good',
      mark: true,
    },
    { label: 'Un chunk existant ?', note: 'IF, vérifie si quelque chose a survécu' },
  ]}
  branches={[
    { route: 'vrai, un élément est passé', tone: 'good', nodes: [{ label: 'Agrégé chunks' }] },
    {
      route: 'faux, rien n\'est passé',
      tone: 'warn',
      nodes: [
        {
          label: 'Indiquer aucun match',
          note: '"Aucun chunk n\'a atteint le seuil de pertinence, la base de connaissances n\'a pu fournir d\'information"',
        },
      ],
    },
  ]}
  tail={[{ label: 'Préparer sortie boucle', note: 'La requête, associée à ce qu\'elle a retourné' }]}
  loop="Préparer sortie boucle retourne vers Boucle sur les éléments. Une fois toutes les requêtes traitées, la sortie finale de la boucle va vers un nœud Agrégé et l’agent reçoit un champ unique : Récupération base de connaissances."
/>

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

| Step | What happens | Cost |
| :--- | :--- | ---: |
| L'utilisateur pose une question complexe | Deux termes et leur interaction |  |
| L'agent rédige trois requêtes | Définir le premier terme, définir le second, établir le lien | 1 appel d'outil |
| Le sous-workflow exécute les trois | Une boucle et un score pour chaque résultat |  |
| Un élément agrégé est retourné | Groupé par requête |  |

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

**Déclarez le paramètre en JSON, sinon il arrivera sous forme de chaîne**

Lorsque le modèle remplit un paramètre d’outil via `$fromAI`, le type par défaut est `string`, et un tableau à un seul élément revient sous une forme impossible à analyser. Définissez le type sur `json` et précisez-le dans la description : utilisez un tableau même s’il n’y a qu’une seule question. Ma description se termine par un exemple de tableau, car c’est ce que le modèle copie.

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

**0,4 est mon chiffre, pas une norme universelle**

C’est ce qui a fonctionné sur cette base de connaissances avec ces tailles de chunks et ce modèle d’embedding. Modifiez l’un de ces trois éléments et ce seuil évoluera. Testez quelques questions réelles et quelques questions délibérément hors sujet, observez les scores obtenus et fixez la limite entre les deux.

### 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

- [n8n: Call n8n Workflow tool](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolworkflow/)
- [n8n: Supabase vector store node](https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoresupabase/)
- [n8n: `$fromAI` in tool parameters](https://docs.n8n.io/advanced-ai/examples/using-the-fromai-function/)
- [Supabase: pgvector and similarity search](https://supabase.com/docs/guides/ai/vector-columns)