Lokalna implementacja RAG: asystent dokumentów oparty na Qdrant, LangChain i Ollama

implementacje

Retrieval-Augmented Generation (RAG) to architektura, która rozwiązuje jeden z podstawowych problemów dużych modeli językowych: odpowiadanie wyłącznie na bazie własnej wiedzy treningowej, bez dostępu do konkretnych dokumentów użytkownika. Zamiast polegać na “pamięci” modelu, RAG w czasie każdego zapytania wyszukuje trafne fragmenty tekstu z zewnętrznej bazy wektorowej i dopiero te fragmenty przekazuje do modelu jako kontekst. Omawiana implementacja – złożona z dwóch plików: ingest.py i app.py – realizuje ten wzorzec w całości lokalnie, bez wysyłania danych do zewnętrznych API.

Architektura systemu

System składa się z dwóch niezależnych komponentów, które współdzielą jedną lokalną bazę wektorową:

Reklama
  • ingest.py – skrypt uruchamiany z linii poleceń; jego zadaniem jest jednorazowe (lub inkrementalne) przetworzenie plików PDF i zapisanie ich reprezentacji wektorowych do bazy Qdrant.
  • app.py – interfejs webowy zbudowany w Streamlit; odpowiada za przyjmowanie pytań od użytkownika, wyszukiwanie odpowiednich fragmentów i generowanie odpowiedzi przez lokalny model językowy.

Oba skrypty korzystają z tego samego modelu embeddingowego, co jest warunkiem koniecznym poprawnego działania: wektory muszą żyć w tej samej przestrzeni, w której są potem wyszukiwane. Dane trafiają lokalnie na dysk (domyślna ścieżka: ./qdrant_db), a cały przepływ jest w pełni offline – jedynym wyjątkiem jest pierwsze pobranie modelu z Hugging Face.

Etap pierwszy: ingestowanie dokumentów (ingest.py)

Skrypt przyjmuje trzy argumenty: katalog z plikami PDF (--source), nazwę kolekcji (--collection) oraz opcjonalnie ścieżkę do bazy (--db_path). Architektura tego etapu obejmuje cztery sekwencyjne kroki:

  • Ładowanie PDF: pliki są wczytywane przez PyMuPDFLoader, który zachowuje metadane strony. Do każdego dokumentu dopisywana jest oryginalna ścieżka pliku jako source_file – informacja ta jest później wyświetlana użytkownikowi jako źródło odpowiedzi.
  • Podział na chunki: tekst jest dzielony przez RecursiveCharacterTextSplitter z rozmiarem okna 1000 znaków i nakładką 200 znaków. Nakładka gwarantuje, że zdania na granicy chunków nie tracą kontekstu.
  • Generowanie embeddingów: każdy chunk jest zamieniany na wektor 384-wymiarowy przez model sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2. Model ten obsługuje ponad 50 języków, w tym polski, co czyni go adekwatnym wyborem dla wielojęzycznych zbiorów dokumentów.
  • Zapis do Qdrant: wektory są zapisywane do lokalnej instancji Qdrant z metryką kosinusową. Jeśli dana kolekcja jeszcze nie istnieje, zostaje automatycznie utworzona z właściwą konfiguracją (size=384, distance=Distance.COSINE).

Ważna cecha implementacji: skrypt śledzi już przetworzone pliki w pliku stanu JSON ({collection}_state.json). Kolejne uruchomienie ingest.py na tym samym katalogu przetworzy wyłącznie nowe dokumenty, pomijając już zaindeksowane. Pozwala to na stopniowe rozbudowywanie bazy bez konieczności przetwarzania całego zbioru od nowa.

ParametrWartośćUzasadnienie
Model embeddingówparaphrase-multilingual-MiniLM-L12-v2Wielojęzyczny, lekki (384 dim), dobra jakość na polskim tekście
Rozmiar chunka1000 znakówKompromis między granularnością a kontekstem
Nakładka (overlap)200 znakówZapobiega utracie sensu na granicy chunków
Metryka podobieństwaCosineStandard dla modeli sentence-transformers
Baza wektorowaQdrant (lokalna)Brak zależności od zewnętrznych serwerów

Etap drugi: interfejs i generowanie odpowiedzi (app.py)

image
image

Plik app.py to aplikacja Streamlit realizująca drugą fazę RAG – retrieval i generation. Po uruchomieniu (streamlit run app.py) użytkownik widzi interfejs czatu z paskiem bocznym, w którym wybiera tematyczną kolekcję i model Ollama.

Na każde pytanie użytkownika system wykonuje następującą sekwencję:

  • Pytanie jest zamieniane na wektor tą samą metodą embeddingową co dokumenty.
  • Retriever przeszukuje kolekcję Qdrant i zwraca 5 najbliższych fragmentów tekstu (k=5).
  • Fragmenty są łączone w jeden blok kontekstowy przez łańcuch create_stuff_documents_chain.
  • Kontekst wraz z pytaniem trafia do lokalnego modelu językowego przez create_retrieval_chain.
  • Model generuje odpowiedź, a aplikacja wyświetla listę unikalnych plików źródłowych w rozwijanej sekcji.

System prompt jest zaprojektowany rygorystycznie: model ma odpowiadać wyłącznie na podstawie dostarczonego kontekstu i wprost przyznawać się do braku wiedzy, jeśli fragmenty nie zawierają odpowiedzi. Eliminuje to problem halucynacji, który jest typowy dla modeli odpowiadających “z pamięci”.

„Jeśli dostarczony kontekst nie zawiera odpowiedzi na pytanie, powiedz wprost, że nie wiesz biorąc pod uwagę dostępne zasoby, zamiast zmyślać z własnej wiedzy treningowej.”

Obsługa modeli jest dynamiczna: aplikacja przy starcie odpytuje lokalny serwer Ollama przez REST API (http://localhost:11434/api/tags) i wypełnia listę wyboru dostępnymi modelami. Domyślnie skonfigurowany jest ministral3, ale użytkownik może przełączyć się na dowolny inny model zainstalowany lokalnie. Zmiana kolekcji automatycznie czyści historię czatu, aby uniknąć pomieszania kontekstu między różnymi tematykami.

Cachowanie i wydajność

Ważnym elementem app.py są dekoratory @st.cache_resource użyte przy inicjalizacji modelu embeddingów, klienta Qdrant i modelu LLM. Dzięki temu zasoby są tworzone tylko raz na czas działania aplikacji – kolejne zapytania nie przeładowują modeli. Lista dostępnych modeli Ollama jest cache’owana przez 60 sekund (@st.cache_data(ttl=60)), co pozwala na wykrycie nowo zainstalowanych modeli bez pełnego restartu.

Praktyczne zastosowania

Ta architektura jest przydatna wszędzie tam, gdzie użytkownik potrzebuje przeszukiwać prywatny lub firmowy zbiór dokumentów bez wysyłania ich do zewnętrznych API. Kilka konkretnych przypadków:

  • Baza wiedzy technicznej: zaingestowanie dokumentacji produktów, instrukcji lub RFC pozwala na zadawanie pytań w języku naturalnym zamiast przeszukiwania PDF ręcznie.
  • Asystent e-booków i książek: użytkownik może zbudować osobne kolekcje tematyczne (np. “programowanie”, “prawo”, “medycyna”) i przełączać się między nimi.
  • Poufne dokumenty firmowe: całkowita lokalizacja – Qdrant na dysku, Ollama lokalnie – oznacza, że żadne dane nie opuszczają maszyny użytkownika.
  • Prototypowanie RAG: prosta dwuplikowa struktura ułatwia eksperymentowanie z różnymi modelami embeddingów, rozmiarami chunków czy liczbą zwracanych fragmentów.

Ograniczenia i możliwe rozszerzenia

Obecna implementacja ma kilka technicznych ograniczeń, które warto uwzględnić przy planowaniu rozbudowy. Retriever używa prostego wyszukiwania wektorowego bez reranking ani filtrowania metadatowego, co może obniżać precyzję przy bardzo dużych kolekcjach. Rozmiar kontekstu (k=5 fragmentów po 1000 znaków to ok. 5000 znaków) jest stały – przy modelach z małym oknem kontekstowym może to powodować problemy. Warto też rozważyć zamianę create_stuff_documents_chain na create_map_reduce_documents_chain przy dużych zbiorach danych, gdzie kontekst nie mieści się w oknie modelu.

Możliwe kierunki rozbudowy obejmują m.in. dodanie filtrów metadatowych (np. wyszukiwanie tylko w konkretnym pliku), implementację hybrydowego wyszukiwania (wektor + keyword BM25), dodanie reranking modelu oraz eksport historii czatu do pliku. Qdrant obsługuje wszystkie te funkcjonalności na poziomie bazy danych, więc rozszerzenie dotyczyłoby głównie warstwy LangChain i interfejsu.

Repozytorium

Implementacja która została omówiona znajduje się na oficjalnym repozytorium lowcyai.

Źródła

🧠 Utrwal wiedzę z tego artykułu!

Kliknij pojęcie, by przypomnieć sobie definicję.

Metryka podobieństwa kosinusowa (Cosine Distance)
?
Metryka podobieństwa kosinusowego to miara matematyczna służąca do określania zbieżności dwóch wektorów poprzez obliczenie kosinusa kąta między nimi w przestrzeni...
Czytaj pełną definicję
Podział tekstu na fragmenty (chunks) (Chunking)
?
Podział tekstu na fragmenty, znany jako chunking, to proces dzielenia obszernych dokumentów na mniejsze, dające się zarządzać jednostki zwane chunkami....
Czytaj pełną definicję
Framework do budowy interfejsów aplikacji webowych (Streamlit)
?
Streamlit to otwartoźródłowy framework napisany w języku Python, który umożliwia szybkie tworzenie interaktywnych aplikacji webowych i dashboardów bez konieczności znajomości...
Czytaj pełną definicję
Lokalna baza wektorowa Qdrant (Qdrant)
?
Qdrant to otwartoźródłowa baza danych wektorowych, która umożliwia wydajne przechowywanie, przeszukiwanie i zarządzanie osadzeniami (embeddings). Jako lokalna baza wektorowa pozwala...
Czytaj pełną definicję
Lokalny serwer modeli językowych (Ollama)
?
Ollama to oprogramowanie open-source służące do lokalnego uruchamiania i zarządzania dużymi modelami językowymi (LLM) bezpośrednio na komputerze użytkownika. Narzędzie to...
Czytaj pełną definicję
Wyszukiwanie fragmentów tekstu na podstawie wektorów (Retrieval)
?
Wyszukiwanie fragmentów tekstu na podstawie wektorów, znane jako Retrieval, to proces odnajdywania najbardziej istotnych danych w bazie wiedzy poprzez porównywanie...
Czytaj pełną definicję
Reklama

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Powiązane posty

Zacznij wpisywać wyszukiwane hasło powyżej i naciśnij Enter, aby wyszukać. Naciśnij ESC, aby anulować.

Powrót do góry