Accueil / Articles

Un projet RAG n’est pas un seul fichier Python géant : une structure de dossiers qui s’adapte

Un projet RAG ne devrait pas être un seul et unique fichier Python géant. Il faut une structure de dossiers pour l’ingestion, le découpage en blocs, les embeddings, le stockage vectoriel, la récupération, les prompts, l’API et les tests.

Éditeur : REACTAPP.TOP9 min de lecture2182 mots

Sources: **Your RAG Project Should Not Be One Giant Python File**

Une démonstration RAG prend une après-midi : charger un PDF, le diviser, intégrer ses parties, les placer dans une base de données vectorielle, poser une question, lire la réponse. Cela suffit amplement pour une expérience de week-end. En tant que base pour un travail réel, ce système s’effondre rapidement, et la raison n’est rarement le modèle ou même le stockage vectoriel. La cause réside dans la structure.

Les projets pour débutants commencent généralement par un seul fichier app.py qui gère le chargement des fichiers, le découpage en parties, les embeddings, le stockage, la récupération, les modèles de prompts, les appels au modèle, les routes API, le journalisation et la configuration. Ouvrir un seul fichier semble pratique tant que le projet reste petit. Un nouveau type de document arrive, une nouvelle stratégie de découpage doit être testée, ChromaDB est remplacé par Pinecone, le prompt change, un point d’entrée FastAPI apparaît, la qualité de la récupération s’avère médiocre et quelqu’un veut des tests. À ce stade, toucher à ce seul fichier devient difficile.

Un système facile à maintenir attribue à chaque composante sa propre fonction. L’ingestion charge les données, le découpage divise les documents, les embeddings transforment le texte en vecteurs, la base de données vectorielle les stocke, la recherche trouve le contexte utile, les prompts structurent la demande, la couche LLM effectue les appels au modèle et l’API met tout cela à disposition des utilisateurs. C’est cette séparation qui transforme une démonstration en un système d’ingénierie.

Un modèle mental : pipeline versus application

Pensez-y de cette manière : le pipeline prépare les connaissances, tandis que l’application les utilise. Gardez-les séparés.

Le côté pipeline s’occupe du chargement des documents, du nettoyage du texte, du découpage du contenu, de la création des embeddings et de leur stockage. Le côté application gère la réception des questions, la récupération des fragments, la construction des prompts, l’appel au modèle, la restitution des réponses via une API et le journalisation du comportement en production.

Lorsqu’elles sont combinées, chaque modification présente des risques. Essayer une nouvelle taille de bloc ne devrait pas nécessiter de modifier une route API. Passer d’un fournisseur de modèles à un autre ne devrait pas signifier avoir à réécrire la logique de récupération des données. Ajouter la capacité d’ingérer des sites web ne devrait pas affecter les modèles de prompts.

Un plan de départ

Voici un plan qui peut servir de première ébauche. Ce n’est pas le seul plan valable, mais chaque dossier a une raison claire d’exister.

rag-project/
|-- README.md
|-- requirements.txt
|-- .env
|-- .gitignore
|-- config.yaml
|-- main.py
|-- src/
|   |-- ingestion/
|   |   |-- __init__.py
|   |   `-- loader.py
|   |-- chunking/
|   |   |-- __init__.py
|   |   `-- chunker.py
|   |-- embeddings/
|   |   |-- __init__.py
|   |   `-- embedder.py
|   |-- vectordb/
|   |   |-- __init__.py
|   |   `-- vector_store.py
|   |-- retrieval/
|   |   |-- __init__.py
|   |   `-- retriever.py
|   |-- prompts/
|   |   |-- __init__.py
|   |   `-- prompt_templates.py
|   |-- llm/
|   |   |-- __init__.py
|   |   `-- llm_client.py
|   |-- api/
|   |   |-- __init__.py
|   |   `-- routes.py
|   `-- utils/
|       |-- __init__.py
|       `-- helpers.py
|-- tests/
|   `-- test_app.py
`-- logs/
    `-- app.log

README.md

Le README est l’entrée principale du projet. Il doit indiquer ce que fait le système, comment le configurer et le lancer, ainsi à quoi sert chaque dossier majeur. Les débutants ont souvent tendance à le reporter, mais un bon README vous aide à comprendre votre propre projet même un mois plus tard. Incluez au minimum l’objectif du projet, les étapes de configuration, les variables d’environnement nécessaires, la manière d’ingérer des documents, la façon de faire fonctionner l’API, des exemples de questions et les limites connues. Cela est d’autant plus important pour un projet présenté dans un portfolio, car un examinateur peut en prendre connaissance rapidement avant de décider s’il va tester quoi que ce soit.

requirements.txt

Ce fichier liste les paquets Python nécessaires au projet. Un service RAG simple peut contenir des éléments similaires à ceux ci-dessous.

fastapi
uvicorn
python-dotenv
pydantic
langchain
chromadb
sentence-transformers
openai
pypdf

Votre liste dépend de votre stack technique. La règle est de ne pas laisser les dépendances cachées sur votre propre machine. Toute personne qui clone le projet doit pouvoir installer tout le nécessaire avec une seule commande.

pip install -r requirements.txt

Cela rend le projet reproductible et plus facile à déployer et à examiner.

.env et .gitignore

Le fichier .env contient des valeurs sensibles telles que les clés API. Un exemple typique ressemble à ceci.

OPENAI_API_KEY=your_key_here
VECTOR_DB_URL=your_vector_db_url

Il ne doit en aucun cas parvenir à GitHub, donc votre .gitignore doit l’exclure, tout comme les journaux et le bytecode.

.env
logs/
__pycache__/
*.pyc

Le codage statique des clés dans les fichiers Python peut fonctionner localement, mais c’est peu sûr. Conservez les informations secrètes en dehors du code.

config.yaml

Les paramètres non secrets doivent se trouver dans config.yaml, par exemple :

chunking:
  chunk_size: 800
  chunk_overlap: 120

retrieval:
  top_k: 5

models:
  embedding_model: text-embedding-3-small
  llm_model: gpt-4.1-mini

vector_db:
  provider: chromadb
  collection_name: company_docs

Les systèmes RAG nécessitent des ajustements, et vous voudrez modifier la taille des blocs, le degré de chevauchement, le nombre de résultats à récupérer, le nom du modèle, la température ou les options de la base de données vectorielle. Lorsque ces valeurs sont dispersées dans des fichiers aléatoires, l’ajustement devient fastidieux. Les paramètres doivent être faciles à trouver et à modifier.

Les dossiers de code

ingestion/

Ce dossier lit des PDF, des sites web, des fichiers CSV, des API, des bases de données, du Markdown, des exportations Notion, des dossiers Drive ou tout autre contenu que vous possédez. Son rôle n’est pas de répondre aux questions, mais d’importer du contenu brut sous un format propre et prévisible. Un loader.py peut exposer des fonctions telles que load_pdf(path), load_website(url), load_csv(path) et load_database_table(connection, table_name), chacune renvoyant des documents sous une forme standard, généralement du texte accompagné de métadonnées.

Les métadonnées méritent plus d’attention qu’elles n’en reçoivent généralement. Un morceau de texte est utile, mais un morceau accompagné de son nom de source, de son numéro de page, de son URL, de son type de document, de son auteur et de sa date de création est bien plus utile, car ces données permettent le filtrage, les citations, le débogage et de renforcer la confiance.

Chunking/

Le chunking semble simple, mais il a un impact énorme sur la qualité. Des chunks trop gros fournissent des sections larges remplies de texte irrelevant, tandis que des chunks trop petits enlèvent le contexte dont le modèle a besoin. L’objectif est de préserver le sens ; donc, au lieu de couper aveuglément tous les 500 caractères, envisagez de diviser le texte en fonction des titres, des paragraphes, des sections, des pages, des blocs de code, des tableaux ou des frontières sémantiques. Un chunker.py transforme les documents complets en fragments prêts à être intégrés.

Isoler cette logique s’avère payant, car c’est l’un des moyens les plus rapides d’améliorer un système RAG. En la séparant, de nouvelles stratégies peuvent être testées sans toucher au reste du système.

embeddings/

Les embeddings sont des représentations numériques de texte qui permettent au système de comparer les significations : la question est transformée en embedding et comparée aux chunks correspondants. Un fichier embedder.py doit gérer centralisément le chargement du modèle, l’embedding des entrées individuelles et des lots, le respect des limites de vitesse, les tentatives de récupération en cas d’échec, ainsi que le suivi de la version du modèle ayant généré les vecteurs.

Cette séparation est importante car les modèles d’embedding évoluent. Vous pouvez commencer avec un sentence-transformer local, passer ensuite à une API hébergée, et changer à nouveau de fournisseur en fonction des coûts ou de la qualité. Avec une logique centralisée, chaque modification reste gérable.

vectordb/

Cette boîte à fichiers contient les données d’incrustation et les métadonnées. Les options courantes sont FAISS pour les expériences locales, ChromaDB pour des configurations locales simples ou de petite envergure, Pinecone pour une recherche cloud gérée, ainsi que Weaviate, Qdrant ou Milvus pour des besoins plus importants ou spécialisés.

Un vector_store.py doit cacher les spécificités de la base de données au reste du projet. Le code de récupération ne doit pas connaître les détails de Pinecone ou de ChromaDB ; il doit plutôt appeler des méthodes au niveau du projet telles que add_documents(chunks), similarity_search(query, top_k), delete_collection(name) et get_document_by_id(id). Si vous remplacez un jour la base de données, il vous suffit de modifier cette couche et rien d’autre.

retrieval/

Ici, une question est reçue et les fragments les plus pertinents sont sélectionnés. La récupération de base consiste en une simple recherche par similarité, tandis que les versions plus avancées intègrent des filtres de métadonnées, des recherches hybrides, un réclassement, une réécriture des requêtes ou une récupération multi-requêtes. Cela est extrêmement important, car le modèle ne peut donner de bonnes réponses que s’il reçoit le contexte adéquat, et une récupération insuffisante entraîne des réponses de mauvaise qualité.

Un fichier retriever.py peut intégrer la requête, rechercher dans la base de données, appliquer des filtres, réclasser les résultats, éliminer les doublons et retourner un contexte propre pour la prompt. Le séparer rend le débogage bien plus facile, car on peut déterminer si c’est le modèle qui a échoué ou si la récupération a fourni le mauvais contexte. Cette question n’a pas de réponse claire lorsque les deux éléments sont mélangés.

prompts/

Les prompts ne devraient pas être dispersés dans le code. Un fichier prompt_templates.py peut contenir des modèles comme celui-ci.

You are a helpful assistant answering questions using the provided context.

Use only the context below. If the answer is not in the context, say you do not know.

Context:
{context}

Question:
{question}

Answer:

Il est judicieux de développer tôt l’habitude de stocker les prompts dans un fichier dédié. Ils évoluent fréquemment au fil des expérimentations concernant les instructions, les formats de sortie, les styles de citation, les règles de sécurité ou le comportement en cas de refus. Enfermés à l’intérieur de chemins ou d’appels de modèles, ils deviennent difficiles à versionner et à tester. Considérez-les comme faisant partie de la conception et attribuez-leur un fichier spécifique.

llm/

Tous les appels aux modèles de langage doivent être regroupés en un seul endroit, que le fournisseur soit OpenAI, Claude, Gemini, un modèle local Ollama ou un autre. Le reste de l’application ne doit pas s’en soucier. Un llm_client.py peut gérer l’initialisation, les appels API, les tentatives de réessai, les délais d’attente, la température, l’analyse des réponses, le suivi de l’utilisation des tokens ainsi que les erreurs propres au fournisseur. Les modèles évoluent rapidement : GPT aujourd’hui, Claude demain, un modèle plus petit pour les requêtes simples et un modèle plus puissant pour les cas complexes le mois prochain. Des appels centralisés facilitent le changement de modèle.

api/

Cette couche permet à d’autres personnes et applications d’utiliser le système, généralement via FastAPI ou Flask. Elle doit recevoir et valider les requêtes, appeler le pipeline de récupération et de génération, retourner des réponses structurées et gérer les erreurs de manière propre. Elle ne doit pas contenir la logique elle-même. Une route doit être simple : accepter la requête, appeler le service approprié, retourner le résultat. Si un seul point d’entrée comprend des opérations de saisie, de segmentation, d’incorporation, de récupération, de construction de prompts et d’appels au modèle, le projet devient difficile à maintenir. L’API doit être l’entrée, et non toute la structure.

utils/

De petits outils partagés à l’échelle du projet se trouvent ici, tels que le chargement des configurations, le formatage des journaux, le nettoyage du texte, la validation des chemins, la mesure du temps et la gestion des erreurs courantes. Soyez discipliné, car ce dossier risque facilement de devenir un dépotoir. Un outil qui relève clairement de l’ingestion doit être placé dans le dossier ingestion, et un outil destiné à la récupération doit être placé dans le dossier retrieval. Utilisez utils uniquement pour ce qui est véritablement partagé.

tests/

Les tests permettent de distinguer les projets sérieux des démos fragiles, et les systèmes RAG sont composés de nombreuses petites parties qui peuvent être testées une par une. Vous pouvez vérifier que les PDF s’chargent correctement, que la taille des blocs est adéquate, que les métadonnées sont conservées, que des embeddings sont créés, que la récupération renvoie des documents pertinents, que les prompts incluent à la fois le contexte et la question, et que l’API renvoie la structure attendue.

Une couverture parfaite dès le premier jour n’est pas indispensable, mais il faut en avoir suffisamment pour savoir quand quelque chose ne fonctionne pas. Les tests de récupération sont particulièrement utiles : conservez un petit ensemble de documents et de questions d’exemple, déterminez quelle source doit être retournée pour chacune d’elles, et réexécutez-les après toute modification des paramètres de segmentation ou d’incorporation. Cela fournit un indicateur de qualité de base avant que les utilisateurs ne découvrent des problèmes.

logs/

Les journaux de production peuvent être acheminés vers un outil de surveillance, mais un simple fichier de journal est utile pendant le développement. Les journaux doivent indiquer ce que l’utilisateur a demandé, quels fragments ont été retournés, quelles sources ont été utilisées, combien de temps a duré la récupération et quel modèle a été appelé. Lorsqu’un utilisateur affirme que la réponse était incorrecte, il est nécessaire de voir ce qui a réellement été récupéré et quel prompt a été envoyé.

Conserver main.py court

Le point d’entrée doit rester simple. Il lance le serveur API, charge la configuration ou appelle une fonction principale de traitement, et rien de plus. Un point d’entrée simple indique que le travail réel se trouve dans les bons modules.

Avantages pour vous

  • Débogage : isoler la cause du problème. Si le résultat est incorrect, vérifier d’abord la récupération des données, puis le découpage et les embeddings. Si l’API échoue, examiner la couche des routes.
  • Tests : vérifier chaque composant séparément, ce qui permet de s’assurer que le chargeur de PDF n’a pas besoin que toute l’application soit en cours d’exécution.
  • Échelle : améliorer une couche sans réécrire le reste, que ce soit en ajoutant une ingestion par lots, des tâches en arrière-plan, du cache, un réclassement ou une nouvelle base de données vectorielle.
  • Collaboration : une personne peut améliorer l’ingestion tandis qu’une autre travaille sur les prompts ou les routes.
  • Déploiement : la configuration, les secrets, les dépendances et les points d’entrée sont clairement définis.
  • Surtout, cela rend le projet compréhensible, et un projet facile à comprendre est plus facile à faire confiance.

    Une règle pour les premiers projets

    Utilisez un dossier par responsabilité, et ne cherchez pas à avoir une architecture parfaite. Évitez simplement un seul fichier géant. Commencez par l’ingestion, le découpage en blocs, les embeddings, une base de données vectorielle, la récupération d’informations, les prompts, les appels aux LLM, une API, les tests et les journaux. Cela suffit pour la plupart des projets débutants et de portfolio. Des couches supplémentaires telles que le réclassement, l’évaluation, le cache, l’authentification, les tâches en arrière-plan, la versionnement des documents et les tableaux de bord peuvent être ajoutées au fur et à mesure des besoins, tandis que l’idée de base reste inchangée : garder le pipeline séparé de l’application. Une petite habitude utile est d’indiquer dans le README quel dossier vous modifierez pour chaque type de changement, car si la réponse devient jamais claire, la structure a probablement commencé à devenir floue.

    Une démonstration montre qu’une idée peut fonctionner. Un projet facile à maintenir démontre que le système peut se développer. Les dossiers en eux-mêmes ne sont pas l’objectif ; ce qui compte, c’est d’attribuer à chaque responsabilité un emplacement clair.

    Lectures complémentaires

  • Spring AI dans les entreprises : Un agent d’assistance équipé d’outils, de RAG et de fonctionnalités de révision — Créer un assistant pour le tri des incidents avec Spring AI : prompts en tant que contrats, sortie structurée, appel d’outils, MCP, Qdrant RAG, capacités d’observation et boucles d’agent limitées.