Projekt RAG to nie jeden ogromny plik w Pythonie: struktura folderów, która się skaluje
Projekt typu RAG nie powinien składać się z jednego ogromnego pliku w Pythonie. Należy użyć struktury folderów do przetwarzania danych, dzielenia ich na fragmenty, tworzenia embeddingów, przechowywania wektorów, wyszukiwania, generowania promptów, obsługi API oraz testowania.
Źródła: **Your RAG Project Should Not Be One Giant Python File**
Demo z RAG zajmuje popołudnie: załadowanie PDF, jego podział, wstawienie fragmentów, umieszczenie ich w bazie danych wektorowej, zadanie pytania i odczytanie odpowiedzi. To wystarcza na eksperyment na weekend. Jako podstawa do rzeczywistej pracy szybko się rozpada, a przyczyną rzadko jest model czy nawet magazyn wektorów – przyczyną jest struktura.
Projekty dla początkujących zwykle zaczynają się jako jeden plik app.py, który zawiera załadunek plików, dzielenie ich na fragmenty, generowanie wektorów, przechowywanie, wyszukiwanie, szablony zapytań, wywołania modeli, ścieżki API, logowanie i konfigurację. Otwieranie jednego pliku wydaje się wygodne, dopóki projekt się nie rozrasta. Pojawia się nowy źródło dokumentów, trzeba wypróbować inną strategię dzielenia na fragmenty, ChromaDB zostaje zastąpiony Pinecone, zmieniają się zapytania, pojawia się punkt końcowy FastAPI, jakość wyszukiwania okazuje się słaba i ktoś chce testy. Do tego momentu dotknięcie tego jednego pliku staje się uciążliwe.
System łatwy w utrzymaniu zapewnia każdej funkcji swoje własne miejsce. Proces pobierania danych ładuje informacje, dzielenie na fragmenty służy do rozbierania dokumentów, embeddingi przekształcają tekst w wektory, baza danych wektorowych je przechowuje, funkcja wyszukiwania znajduje przydatny kontekst, prompty kształtują zapytania, warstwa LLM wykonywa połączenia z modelem, a API udostępnia wszystko użytkownikom. To właśnie to rozdzielenie przekształca demonstrację w system inżynieryjny.
Jeden model mentalny: pipeline versus aplikacja
Pomyśl o tym w ten sposób: pipeline przygotowuje wiedzę, a aplikacja ją wykorzystuje. Trzymaj je oddzielnie.
Część pipeline obejmuje ładowanie dokumentów, oczyszczanie tekstu, dzielenie treści, tworzenie embeddingów i ich przechowywanie. Część aplikacji obejmuje odbieranie pytań, pobieranie fragmentów, tworzenie promptów, wywoływanie modelu, zwracanie odpowiedzi za pośrednictwem API oraz rejestrowanie zachowań w środowisku produkcyjnym.
Kiedy wszystko jest ze sobą połączone, każda zmiana niesie ze sobą ryzyko. Próba użycia nowego rozmiaru fragmentu nie powinna wymagać edycji trasy API. Przejście z jednego dostawcy modeli na drugiego nie powinno oznaczać przepisywania mechanizmów pobierania danych. Dodawanie funkcji pobierania treści z witryn internetowych nie powinno wpływać na szablony zapytań.
Podstawowa struktura
Oto struktura, która może służyć jako wstępny projekt. Nie jest to jedyna możliwa, ale każda folder ma jasny powód swojego istnienia.
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
README to wejście główne do projektu. Powinno tam być opisane, czym zajmuje się system, jak go skonfigurować i uruchomić oraz do czego służy każda z głównych folderów. Początkujący często odkładają jego przygotowanie, ale dobry dokument pomaga zrozumieć własny projekt nawet miesiąc później. Koniecznie należy umieścić w nim cel projektu, kroki konfiguracji, wymagane zmienne środowiskowe, sposoby wprowadzania dokumentów, sposób uruchamiania API, przykładowe pytania oraz znane ograniczenia. Dla projektu z portfolio jest to jeszcze ważniejsze, ponieważ recenzent może go szybko przejrzeć, zanim zdecyduje się na cokolwiek przetestować.
requirements.txt
W tym pliku wymienione są pakietów Python, których potrzebuje projekt. Prosty serwis RAG może zawierać coś w rodzaju poniższego.
fastapi
uvicorn
python-dotenv
pydantic
langchain
chromadb
sentence-transformers
openai
pypdf
Twoja lista zależy od używanych narzędzi. Zasada brzmi, że zależności nie powinny pozostawać ukryte na twoim własnym komputerze. Każdy, kto sklonuje projekt, powinien móc zainstalować wszystko za pomocą jednego polecenia.
pip install -r requirements.txt
To sprawia, że projekt jest powtarzalny oraz łatwiejszy do wdrożenia i przeglądania.
.env i .gitignore
Plik .env zawiera poufne wartości, takie jak klucze API. Typowy przykład wygląda w ten sposób.
OPENAI_API_KEY=your_key_here
VECTOR_DB_URL=your_vector_db_url
Nigdy nie może trafić do GitHub, więc plik .gitignore powinien go wykluczyć wraz z logami i kodem bajtowym.
.env
logs/
__pycache__/
*.pyc
Hardkodowanie kluczy w plikach Pythona może działać lokalnie, ale jest niebezpieczne. Trzymaj sekrety poza kodem.
config.yaml
Ustawienia niesekretne powinny znajdować się w config.yaml, na przykład:
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
Systemy RAG wymagają dostrojenia – konieczne będzie zmianienie rozmiaru fragmentów, stopnia nakładania się, liczby wyników do pobrania, nazwy modelu, ustawień temperatury oraz opcji bazy danych wektorowych. Gdy te wartości są rozproszone w losowych plikach, dostrojenie staje się uciążliwe. Ustawienia powinny być łatwe do znalezienia i zmiany.
Katalogi z kodem
ingestion/
Ten katalog odczytuje dane z plików PDF, stron internetowych, plików CSV, API, baz danych, formatu Markdown, eksportów z Notion, folderów w Drive lub czegokolwiek innego, co masz. Jego zadaniem nie jest odpowiadanie na pytania, lecz przyjmowanie surowego treści w czystym i przewidywalnym formacie. Plik loader.py może zawierać funkcje takie jak load_pdf(path), load_website(url), load_csv(path) oraz load_database_table(connection, table_name), z których każda zwraca dokumenty w jednolitym standardowym formacie, zazwyczaj tekst wraz z metadanymi.
Metadane zasługują na większą uwagę, niż zwykle otrzymują. Fragment tekstu jest przydatny, ale fragment zawierający nazwę źródła, numer strony, adres URL, typ dokumentu, autora oraz datę i godzinę jest o wiele bardziej użyteczny, ponieważ te dane umożliwiają filtrowanie, cytowanie, debugowanie oraz budowanie zaufania.
Podział na fragmenty/
Podział na fragmenty wydaje się prosty, ale ma ogromny wpływ na jakość. Zbyt duże fragmenty dostarczają obszerne sekcje pełne nieistotnego tekstu, natomiast zbyt małe pozbawiają model niezbędnego kontekstu. Celem jest zachowanie znaczenia, więc zamiast ślepo dzielić tekst co 500 znaków, warto rozważyć podział według nagłówków, akapitów, sekcji, stron, bloków kodu, tabel lub granic semantycznych. Plik chunker.py przekształca pełne dokumenty w fragmenty gotowe do wykorzystania w modelach.
Izolacja tej logiki przynosi korzyści, ponieważ jest to jeden z najszybszych sposobów na ulepszenie systemu RAG. Dzięki oddzieleniu procesu dzielenia tekstu na fragmenty można testować nowe strategie bez ingerencji w inne elementy.
embeddings/
Embeddingi to liczbowe reprezentacje tekstu, które umożliwiają systemowi porównywanie znaczeń: pytanie jest przekształcane w embedding i porównywane z embeddingami fragmentów tekstu. Plik embedder.py powinien centralizować proces ładowania modelu, przekształcania pojedynczych danych we embeddingi oraz całych grup danych, zarządzać ograniczeniami szybkości, próbować ponownie po niepowodzeniach oraz śledzić, która wersja modelu wygenerowała dane wektorowe.
Oddzielenie tych funkcji jest ważne, ponieważ modele do generowania embeddingów się zmieniają. Można zacząć od lokalnego modelu sentence-transformer, później przenieść się na usługę API, a następnie zmienić dostawcę ze względu na koszty lub jakość. Gdy logika ta znajduje się w jednym miejscu, każda zmiana pozostaje łatwa do zarządzania.
vectordb/
Ta folder zawiera dane embeddingów oraz metadane. Typowymi opcjami są FAISS do eksperymentów lokalnych, ChromaDB do prostych konfiguracji lokalnych lub małych środowisk produkcyjnych, Pinecone do zarządzanego wyszukiwania w chmurze, a także Weaviate, Qdrant lub Milvus dla większych lub bardziej specjalistycznych potrzeb.
Plik vector_store.py powinien ukrywać szczegóły dotyczące bazy danych przed resztą projektu. Kod służący do wyszukiwania nie powinien znać szczegółów dotyczących Pinecone czy ChromaDB. Powinien wywoływać proste metody na poziomie projektu, takie jak add_documents(chunks), similarity_search(query, top_k), delete_collection(name) oraz get_document_by_id(id). Jeśli kiedykolwiek zamienisz bazę danych, wystarczy zmienić tę warstwę, a nic więcej.
retrieval/
Tutaj pojawia się pytanie, a najbardziej istotne fragmenty są wybierane. Podstawowe wyszukiwanie to zwykłe porównywanie podobieństwa, natomiast bardziej zaawansowane wersje dodają filtry metadanych, wyszukiwanie hybrydowe, ponowną klasyfikację, przepisywanie zapytań lub wyszukiwanie wielu zapytań jednocześnie. Jest to niezwykle ważne, ponieważ model może udzielać dobrych odpowiedzi tylko wtedy, gdy otrzymuje odpowiedni kontekst, a słabe wyszukiwanie oznacza słabe odpowiedzi.
Plik retriever.py może zawierać zapytanie, przeszukiwać bazę danych, stosować filtry, ponownie klasyfikować wyniki, usuwać duplikaty i zwracać czysty kontekst dla promptu. Oddzielenie tych funkcji znacznie ułatwia debugowanie, ponieważ można sprawdzić, czy model zawiodł, czy też wyszukiwanie przesłało niewłaściwy kontekst. Na to pytanie nie ma jednoznacznej odpowiedzi, gdy obie te kwestie są ze sobą powiązane.
prompts/
Prompty nie powinny być rozrzucone po całym kodzie. Plik prompt_templates.py może przechowywać takie szablony jak ten.
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:
Przechowywanie promptów w osobnym pliku to nawyk, który warto wyrobić już na wczesnym etapie. Często się zmieniają w miarę eksperymentowania z instrukcjami, formatami wyjściowymi, stylami cytowania, zasadami bezpieczeństwa czy zachowaniami w przypadku odmowy. Gdy są ukryte w środku tras lub wywołań modeli, trudno je jest wersjonować i testować. Traktuj je jako część projektu i przeznacz im dedykowany plik.
llm/
Wszystkie wywołania modeli językowych powinny znajdować się w jednym miejscu, niezależnie od tego, czy dostawcą jest OpenAI, Claude, Gemini, lokalny model Ollama, czy coś innego. Reszta aplikacji nie powinna się tym przejmować. Plik llm_client.py może zajmować się inicjalizacją, wywołaniami API, próbami ponownymi, czasem oczekiwania, ustawieniem temperatury, analizą odpowiedzi, rejestracją użycia tokenów oraz błędami specyficznymi dla danego dostawcy. Modele zmieniają się szybko: dziś GPT, jutro Claude, w przyszłym miesiącu mniejszy model do tanich zapytań i silniejszy do trudniejszych. Centralizowane wywołania ułatwiają zmianę dostawcy.
api/
Warstwa ta umożliwia innym osobom i aplikacjom korzystanie z systemu, zazwyczaj za pośrednictwem FastAPI lub Flask. Powinna przyjmować i weryfikować żądania, uruchamiać proces pobierania i generowania danych, zwracać ustrukturyzowane odpowiedzi oraz sprawnie radzić sobie z błędami. Nie powinna zawierać samej logiki. Trasa powinna być prosta: przyjąć żądanie, wezwać odpowiednią usługę i zwrócić wynik. Jeśli jeden koniec punktu dostępu obejmuje procesy pobierania danych, dzielenia ich na fragmenty, tworzenia embeddingów, wyszukiwania informacji, budowania promptów oraz wywoływania modeli, projekt staje się trudny do utrzymania. API powinno być jedynie wejściem, a nie całym budynkiem.
utils/
Małe narzędzia używane w całym projekcie znajdują się tutaj – np. do ładowania konfiguracji, formatowania logów, czyszczenia tekstu, weryfikacji ścieżek, mierzenia czasu trwania operacji oraz obsługi błędów. Bądź dyscyplinowany, ponieważ ta folder może łatwo stać się miejscem zbierania niepotrzebnego kodu. Narzędzie przeznaczone wyraźnie do procesu pobierania danych trafia do folderu ingestion, a to przeznaczone do ich wyszukiwania – do folderu retrieval. Używaj utils tylko wtedy, gdy rzeczywiście chodzi o elementy wspólne dla całego projektu.
tests/
Testy odróżniają poważne projekty od kruchych demonstracji, a systemy RAG składają się z wielu małych elementów, które można testować pojedynczo. Możesz sprawdzić, czy pliki PDF są poprawnie ładowane, czy rozmiary fragmentów danych są odpowiednie, czy metadane przetrwają przetwarzanie, czy są tworzone embeddingi, czy funkcja wyszukiwania zwraca istotne dokumenty, czy prompty zawierają zarówno kontekst, jak i pytanie, oraz czy API zwraca oczekiwany format danych.
Nie jest konieczne doskonałe pokrycie od pierwszego dnia, ale potrzebujesz wystarczających danych, aby wiedzieć, gdy coś się zepsuje. Testy pobierania są szczególnie cenne: przechowuj małą kolekcję dokumentów i pytań przykładowych, określ, który źródło powinno zostać przywrócone dla każdego z nich, i uruchamiaj je ponownie po jakiejkolwiek zmianie w ustawieniach dzielenia na fragmenty lub embedowania. Dzięki temu uzyskujesz podstawowy sygnał jakościowy, zanim użytkownicy odkryją problemy.
logs/
Logi z produkcji mogą trafiać do narzędzia monitoringu, ale prosty plik logów jest przydatny podczas rozwoju. Logi powinny odpowiadać na to, o co pytał użytkownik, które fragmenty zostały przywrócone, które źródła zostały użyte, jak długo trwało pobieranie oraz który model został wywołany. Gdy użytkownik twierdzi, że odpowiedź była błędna, musisz zobaczyć, co faktycznie zostało pobrałe i jaki prompt został wysłany.
Utrzymywanie krótkiego pliku main.py
Punkt wejścia powinien pozostać prosty. Uruchamia on serwer API, ładuje konfigurację lub wywołuje główną funkcję przetwarzania, i to wszystko. Prosty punkt wejścia jest oznaką, że prawdziwa praca znajduje się we właściwych modułach.
Co to przynosi
- Debugowanie: izolacja problemu. Jeśli wynik jest błędny, sprawdź proces pobierania danych, następnie dzielenie ich na fragmenty i tworzenie embeddingów. Jeśli API nie działa, sprawdź warstwę tras.
- Testowanie: sprawdzaj każdy komponent osobno, dzięki czemu weryfikacja ładowarki PDF nie wymaga uruchamiania całego aplikacji.
- Mas skalowania: ulepsz jedną warstwę bez przepisywania reszty, czy to poprzez dodanie masowego pobierania danych, zadań w tle, cache’owania, ponownego sortowania lub nowej bazy danych wektorowej.
- Współpraca: jedna osoba poprawia proces pobierania danych, podczas gdy druga pracuje nad promptami lub trasami.
- Rozwój: konfiguracja, tajne dane, zależności oraz punkty wejścia są jasno określone.
Przede wszystkim sprawia to, że projekt jest zrozumiały, a projekt łatwy do zrozumienia jest łatwiejszy do zaufania.
Zasada dla pierwszych projektów
Używaj jednej folderu na każdą funkcję, a nie dąż do idealnej architektury. Po prostu unikaj jednego ogromnego pliku. Zacznij od pobierania danych, dzielenia ich na fragmenty, tworzenia embeddingów, używania bazy danych wektorowej, wyszukiwania, promptów, wywołań modeli LLM, API, testów oraz logów. To wystarczy do większości projektów początkujących i projektów portfolio. Dodatkowe warstwy, takie jak ponowne sortowanie, ocena, cacheowanie, autoryzacja, zadania w tle, wersjonowanie dokumentów i panele kontrolne, można dodać w miarę pojawiania się potrzeb, przy czym podstawowa zasada pozostaje niezmienna: trzymaj pipeline oddzielnie od aplikacji. Mały nawyk, który pomaga, to zapisywanie w pliku README, który folder należy edytować przy każdym rodzaju zmiany, ponieważ jeśli odpowiedź jest niejasna, struktura prawdopodobnie zaczęła się zamazywać.
Demo pokazuje, że dana idea może funkcjonować. Projekt nadający się do utrzymania dowodzi, że system może się rozwijać. Katalogi same w sobie nie są celem – kluczowe jest przydzielenie każdej odpowiedzialności jasnego miejsca.