Startseite / Artikel

Ein RAG-Projekt besteht nicht aus einer einzigen riesigen Python-Datei: Eine skalierbare Ordnerstruktur

Ein RAG-Projekt sollte nicht aus einer einzigen riesigen Python-Datei bestehen. Eine Ordnerstruktur für die Eingabe, Aufteilung in Blöcke, Embeddings, Vektorlager, Abfrage, Prompts, API und Tests.

Verlag: REACTAPP.TOP9 Min. Lesezeit2182 Wörter

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

Eine RAG-Demo erfordert einen Nachmittag: Ein PDF wird geladen, in Teile aufgeteilt, die Teile eingebettet, in eine Vektordatenbank eingefügt, eine Frage gestellt und die Antwort gelesen. Das reicht völlig aus für ein Wochenendexperiment. Als Grundlage für echte Arbeit bricht das System jedoch schnell zusammen – und der Grund liegt selten am Modell oder sogar am Vektorlagersystem. Der Grund ist die Struktur.

Anfängerprojekte beginnen meist mit einer einzigen Datei app.py, die das Laden von Dateien, das Aufteilen in Blöcke, die Erstellung von Embeddings, die Speicherung, das Abrufen, Prompt-Vorlagen, Modellaufrufe, API-Endpunkte, das Protokollieren sowie die Konfiguration enthält. Das Öffnen einer einzigen Datei erscheint praktisch, solange das Projekt noch klein ist. Dann taucht eine neue Dokumentenquelle auf, es muss eine andere Aufteilungsstrategie ausprobiert werden, ChromaDB wird durch Pinecone ersetzt, der Prompt ändert sich, ein FastAPI-Endpunkt entsteht, die Qualität des Abrufens stellt sich als schlecht heraus – und jemand möchte Tests. Zu diesem Zeitpunkt wird das Bearbeiten dieser einen Datei zur Qual.

Ein wartbares System weist jedem Aufgabengebiet einen eigenen Bereich zu. Die Eingabemaschinerie lädt Daten, das Chunking teilt Dokumente auf, Embeddings wandeln Text in Vektoren um, die Vektordatenbank speichert sie, die Abfragemaschinerie findet den nützlichen Kontext, Prompts formen die Anfrage, die LLM-Schicht führt Modellaufrufe durch und die API stellt alles den Nutzern zur Verfügung. Genau diese Trennung verwandelt eine Demo in ein ingenieurtechnisches System.

Ein mentales Modell: Pipeline gegen Anwendung

Stellen Sie es sich so vor: Die Pipeline bereitet das Wissen vor, und die Anwendung nutzt es. Halten Sie sie voneinander getrennt.

Die Pipeline- Seite umfasst das Laden von Dokumenten, die Reinigung von Texten, das Aufteilen von Inhalten, die Erstellung von Embeddings und deren Speicherung. Die Anwendungsseite umfasst das Empfangen von Fragen, das Abrufen von Teildokumenten, das Erstellen von Prompts, den Aufruf des Modells, die Rückgabe von Antworten über eine API sowie das Protokollieren des Verhaltens in der Produktion.

Im Zusammenhang betrachtet ist jede Änderung riskant. Das Ausprobieren einer neuen Blockgröße sollte nicht die Bearbeitung einer API-Route erfordern. Der Wechsel von einem Modellanbieter zum anderen sollte nicht zur Neuimplementierung der Abruflogik führen. Die Hinzufügung der Einbeziehung von Webseiten sollte die Prompt-Vorlagen nicht beeinflussen.

Eine Ausgangsstruktur

Hier ist eine Struktur, die als erster Entwurf dient. Sie ist nicht die einzige mögliche, doch jeder Ordner hat einen klaren Grund zur Existenz.

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

Die README-Datei ist die Eingangstür zum Projekt. Sie sollte erklären, wofür das System dient, wie es eingerichtet und ausgeführt wird sowie wofür jede wichtige Dateiordnung bestimmt ist. Anfänger verschieben dies oft, doch eine gute README hilft bereits einen Monat später dabei, das eigene Projekt zu verstehen. Mindestens sollten dort Zweck, Einrichtungsschritte, erforderliche Umgebungsvariablen, die Möglichkeit zum Einlesen von Dokumenten, die Verwendung der API, Beispielfragen sowie bekannte Einschränkungen aufgeführt sein. Für ein Portfolio-Projekt ist das umso wichtiger, da ein Prüfer sie möglicherweise kurz durchblättern könnte, bevor er entscheidet, ob er etwas ausprobiert.

requirements.txt

In dieser Datei werden die Python-Pakete aufgelistet, die das Projekt benötigt. Ein einfacher RAG-Dienst könnte beispielsweise Folgendes enthalten.

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

Ihre Liste hängt von Ihrer Technologieauswahl ab. Die Regel ist, Abhängigkeiten nicht auf Ihrem eigenen Rechner zu verbergen. Jeder, der das Projekt kloniert, sollte in der Lage sein, alles mit einem einzigen Befehl zu installieren.

pip install -r requirements.txt

Dadurch wird das Projekt reproduzierbar sowie einfacher zu deployen und zu überprüfen.

.env und .gitignore

Die .env-Datei enthält sensible Werte wie API-Schlüssel. Eine typische Version sieht so aus.

OPENAI_API_KEY=your_key_here
VECTOR_DB_URL=your_vector_db_url

Es darf auf keinen Fall bei GitHub landen, daher sollte Ihr .gitignore es zusammen mit Logs und Bytecode ausschließen.

.env
logs/
__pycache__/
*.pyc

Das Härtecodieren von Schlüsseln in Python-Dateien funktioniert zwar lokal, ist aber unsicher. Bewahren Sie Geheimnisse außerhalb des Codes auf.

config.yaml

Nicht-geheime Einstellungen gehören in config.yaml, zum Beispiel:

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

RAG-Systeme benötigen Anpassungen, und Sie werden den Chunk-Größe, die Überlappung, die Anzahl der zu abrufenden Ergebnisse, den Modellnamen, die Temperatur sowie die Optionen der Vektordatenbank ändern wollen. Wenn diese Werte in zufälligen Dateien verteilt sind, wird die Anpassung lästig. Die Einstellungen sollten leicht auffindbar und änderbar sein.

Die Codeordner

ingestion/

Dieser Ordner liest Inhalte aus PDFs, Webseiten, CSV-Dateien, APIs, Datenbanken, Markdown-Dateien, Notion-Exporten, Google Drive-Ordnern oder allem anderen, was Sie haben. Seine Aufgabe besteht nicht darin, Fragen zu beantworten, sondern Rohinhalte in einem sauberen und vorhersehbaren Format einzuspeisen. Ein loader.py kann Funktionen wie load_pdf(path), load_website(url), load_csv(path) und load_database_table(connection, table_name) bereitstellen, wobei jede Funktion Dokumente in einer standardisierten Form zurückgibt – in der Regel Text zusammen mit Metadaten.

Metadaten verdienen mehr Aufmerksamkeit, als sie normalerweise erhalten. Ein Textabschnitt ist nützlich, doch ein Abschnitt mit seinem Quellnamen, Seitennummer, URL, Dokumenttyp, Autor und Zeitstempel ist weitaus nützlicher, denn diese Daten ermöglichen das Filtern, Zitieren, Debuggen sowie das Aufbauen von Vertrauen.

Chunking/

Chunking scheint trivial zu sein, hat aber einen enormen Einfluss auf die Qualität. Zu große Chunking-Einheiten liefern umfangreiche Abschnitte voller irrelevanter Texte, während zu kleine Einheiten den Kontext entfernen, den das Modell benötigt. Das Ziel ist es, die Bedeutung beizubehalten; daher sollte man anstelle eines blinden Abschneidens alle 500 Zeichen in Betracht ziehen, den Text nach Überschriften, Absätzen, Abschnitten, Seiten, Codeblöcken, Tabellen oder semantischen Grenzen aufzuteilen. Ein chunker.py wandelt vollständige Dokumente in Teile um, die für das Einbetten bereit sind.

Die Isolierung dieser Logik lohnt sich, da sie zu den schnellsten Mitteln gehört, um ein RAG-System zu verbessern. Durch die Trennung durch Chunking können neue Strategien getestet werden, ohne etwas anderes anzufassen.

Embeddings/

Embeddings sind numerische Repräsentationen von Texten, die es dem System ermöglichen, Bedeutungen zu vergleichen: Die Frage wird in ein Embedding umgewandelt und mit den entsprechenden Embeddings der Chunks abgeglichen. Ein embedder.py sollte das Laden des Modells, das Einbetten einzelner Eingaben und von Batch-Daten, das Handhaben von Rate Limits, das Wiederholen bei Fehlern sowie das Erfassen der Modellversion, die die Vektoren erzeugt hat, zentralisieren.

Die Trennung ist wichtig, weil sich Embedding-Modelle ändern können. Man beginnt möglicherweise mit einem lokalen Sentence-Transformer, wechselt später zu einer gehosteten API und tauscht den Anbieter aus Kostengründen oder wegen der Qualität aus. Durch eine zentrale Stelle für diese Logik bleibt jede Änderung übersichtlich.

vectordb/

Diese Ordner enthält die Speicherung von Embeddings und Metadaten. Typische Optionen sind FAISS für lokale Experimente, ChromaDB für einfache lokale oder kleine Produktionsumgebungen, Pinecone für verwaltete Cloud-Suchfunktionen sowie Weaviate, Qdrant oder Milvus für größere oder speziellere Anforderungen.

Ein vector_store.py sollte die Besonderheiten der Datenbank vor dem Rest des Projekts verbergen. Der Abrufcode sollte keine Details zu Pinecone oder ChromaDB kennen. Er sollte stattdessen einfache methoden auf Projektebene aufrufen wie add_documents(chunks), similarity_search(query, top_k), delete_collection(name) und get_document_by_id(id). Wenn Sie die Datenbank austauschen, ändern Sie nur diese Schicht und nichts anderes.

retrieval/

Hier kommt eine Frage herein und die relevantesten Teile werden ausgewählt. Die grundlegende Abfrage erfolgt durch eine einfache Ähnlichkeitssuche, während fortgeschrittene Versionen Metadatenfilter, hybride Suchverfahren, Neuranking, Umformulierung der Abfrage oder Mehrfachabfragen hinzufügen. Das ist äußerst wichtig, denn das Modell kann nur gut antworten, wenn es den richtigen Kontext erhält – eine schwache Abfrage führt zu schwachen Antworten.

Ein retriever.py kann die Abfrage einbetten, im Speicher suchen, Filter anwenden, Neuranking durchführen, Duplikate entfernen und einen sauberen Kontext für die Anfrage zurückgeben. Die Trennung dieser Komponenten erleichtert das Debuggen erheblich, da man prüfen kann, ob das Modell versagt hat oder die Abfrage den falschen Kontext übermittelt hat. Diese Frage lässt sich nicht eindeutig beantworten, wenn beides miteinander vermischt ist.

prompts/

Prompts sollten nicht über den gesamten Codebase verstreut sein. Ein prompt_templates.py kann Vorlagen wie diese enthalten.

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:

Die Prompte in eine eigene Datei zu speichern, ist eine Gewohnheit, die man früh entwickeln sollte. Sie ändern sich häufig, je nachdem, wie man mit Anweisungen, Ausgabeformaten, Zitierstilen, Sicherheitsregeln oder Ablehnungsverhalten experimentiert. Wenn sie innerhalb von Routen oder Modellaufrufen versteckt sind, wird es schwierig, sie zu versionieren und zu testen. Behandeln Sie sie als Teil der Architektur und weisen Sie ihnen eine eigene Datei zu.

llm/

Sämtliche Aufrufe von Sprachmodellen sollten an einem Ort zusammengefasst werden – unabhängig davon, ob es sich um OpenAI, Claude, Gemini, ein lokales Ollama-Modell oder etwas anderes handelt. Der Rest der Anwendung sollte sich darum nicht kümmern. Ein llm_client.py kann für die Initialisierung, API-Aufrufe, Wiederholungsversuche, Zeitlimits, Temperatureinstellungen, die Analyse der Antworten, das Protokollieren der Tokenverwendung sowie fehlerbezogene Meldungen spezifisch für den Anbieter sorgen. Modelle ändern sich schnell: Heute GPT, morgen Claude, im nächsten Monat ein kleineres Modell für einfache Anfragen und ein leistungsstärkeres für schwierige Fälle. Zentralisierte Aufrufe erleichtern den Wechsel zwischen Modellen.

api/

Diese Schicht ermöglicht es anderen Personen und Anwendungen, das System zu nutzen – in der Regel über FastAPI oder Flask. Sie sollte Anfragen entgegennehmen und validieren, den Abruf- und Generierungsprozess aufrufen, strukturierte Antworten zurückgeben und Fehler sauber handhaben. Sie sollte nicht selbst die Logik enthalten. Eine Route sollte einfach sein: Die Anfrage entgegennehmen, den richtigen Dienst aufrufen und das Ergebnis zurückgeben. Wenn ein Endpunkt Ingestion, Chunking, Embedding, Abruf, Erstellung von Prompts sowie Aufrufe des Modells enthält, wird das Projekt unverwaltbar. Die API sollte der Eingang sein, nicht das ganze System.

utils/

Kleine Hilfsfunktionen, die im gesamten Projekt verwendet werden, sind hier aufgelistet – beispielsweise das Laden von Konfigurationen, das Formatieren von Protokollen, die Reinigung von Texten, die Überprüfung von Pfaden, die Zeitmessung sowie die Behandlung häufiger Fehler. Seien Sie diszipliniert, denn dieser Ordner kann leicht zu einem Ablageort für alles Mögliche werden. Eine Hilfsfunktion, die eindeutig zur Dateneingabe gehört, kommt in den Ordner für Dateneingabe, und eine, die zur Datenabfrage gehört, in den Ordner für Datenabfrage. Verwenden Sie utils nur für das, was tatsächlich gemeinsam genutzt wird.

tests/

Tests unterscheiden ernsthafte Projekte von fragilen Demos, und RAG-Systeme bestehen aus vielen kleinen Komponenten, die nacheinander getestet werden können. Sie können überprüfen, ob PDFs korrekt geladen werden, ob die Größen der Datenblöcke in Ordnung sind, ob Metadaten erhalten bleiben, ob Embeddings erstellt werden, ob die Datenabfrage relevante Dokumente zurückgibt, ob Anfragen sowohl Kontext als auch Frage enthalten und ob die API die erwartete Struktur liefert.

Eine perfekte Abdeckung bereits am ersten Tag ist nicht erforderlich, aber es muss ausreichen, um zu erkennen, wenn etwas nicht funktioniert. Abruftests sind besonders wertvoll: Halten Sie eine kleine Sammlung von Beispieldokumenten und -fragen bereit, definieren Sie, welche Quelle für jede Frage zurückgegeben werden soll, und führen Sie sie nach jeder Änderung der Einteilungs- oder Einbettungseinstellungen erneut durch. Dadurch erhalten Sie ein grundlegendes Qualitätssignal, bevor die Nutzer Probleme feststellen.

Logs/

Produktionslogs können in ein Überwachungstool fließen, doch eine einfache Log-Datei hilft bei der Entwicklung. Die Logs sollten beantworten, was der Nutzer gefragt hat, welche Teile zurückgegeben wurden, welche Quellen verwendet wurden, wie lange der Abruf gedauert hat und welches Modell aufgerufen wurde. Wenn ein Nutzer sagt, die Antwort sei falsch, müssen Sie sehen können, was tatsächlich abgerufen wurde und welche Anfrage gesendet wurde.

main.py kurz halten

Der Eingangspunkt sollte einfach und unkompliziert bleiben. Er startet den API-Server, lädt die Konfiguration oder ruft eine Hauptfunktion auf – und nichts weiter. Ein einfacher Eingangspunkt zeigt an, dass die eigentliche Arbeit in den richtigen Modulen stattfindet.

Was das für Sie bringt

  • Debugging: Man kann den Fehler isolieren. Wenn das Ergebnis falsch ist, prüft man zunächst den Abrufvorgang, anschließend das Aufteilen der Daten in Blöcke sowie die Erstellung der Embeddings. Fällt die API aus, wird die Route-Ebene überprüft.
  • Testing: Jedes Komponente kann einzeln getestet werden, sodass man sicherstellen kann, dass der PDF-Loader nicht das gesamte Programm laufen lassen muss.
  • Scaling: Man kann eine Ebene verbessern, ohne den Rest neu schreiben zu müssen – sei es durch Hinzufügen von Batch-Eingaben, Hintergrundaufgaben, Caching, Neuranking oder einer neuen Vektordatenbank.
  • Collaboration: Eine Person kann sich um die Dateneingabe kümmern, während eine andere an Prompts oder Routen arbeitet.
  • Implementierung: Konfiguration, Geheimnisse, Abhängigkeiten und Eingangspunkte sind klar definiert.
  • Vor allem macht das das Projekt verständlich, und ein leicht verständliches Projekt ist leichter zu vertrauen.

    Ein Regel für erste Projekte

    Nutzen Sie einen Ordner pro Aufgabe und streben Sie keine perfekte Architektur an. Vermeiden Sie einfach nur eine riesige Datei. Beginnen Sie mit der Eingabe, dem Aufteilen in Blöcke, den Embeddings, einer Vektordatenbank, der Abfrage, den Prompts, den Aufrufen von LLMs, einer API, Tests und Logs. Das reicht für die meisten Anfängerprojekte und Portfolio-Projekte aus. Zusätzliche Schichten wie Neubewertung, Evaluierung, Caching, Authentifizierung, Hintergrundaufgaben, Dokumentversionierung und Dashboards können nach Bedarf hinzugefügt werden, wobei die Grundidee unverändert bleibt: Halten Sie den Pipeline-Prozess getrennt von der Anwendung. Eine kleine Gewohnheit, die dabei hilft, ist es, im README aufzuschreiben, in welchem Ordner Sie für jede Art von Änderung vorgehen würden, denn wenn die Antwort je unklar wird, hat sich die Struktur wahrscheinlich bereits verschoben.

    Eine Demo zeigt, dass eine Idee funktionieren kann. Ein wartbares Projekt zeigt, dass das System wachsen kann. Ordnner allein sind nicht das Ziel – wichtig ist vielmehr, jeder Verantwortung einen klaren Platz zuzuweisen.

    Verwandte Artikel

  • Spring AI im Unternehmensumfeld: Ein Support-Agent mit Tools, RAG und Überprüfungsmöglichkeiten — Erstellen Sie einen Hilfsassistenten für die Priorisierung von Vorfällen mit Spring AI: Prompts als Verträge, strukturierte Ausgaben, Aufruf von Tools, MCP, Qdrant RAG, Überwachungsfunktionen sowie begrenzte Agentenschleifen.