RAG od podstaw: jak AI czyta Twoje dokumenty

Michał Feliksik
3 min read
RAG od podstaw: jak AI czyta Twoje dokumenty

Wprowadzenie

Zaczynamy trzyczęściową serię, na końcu której zbudujemy działający, najprostszy możliwy RAG z własnym API: wskażesz mu plik Markdown z dysku, a potem zadasz pytanie o jego treść — i dostaniesz odpowiedź opartą na tym dokumencie, nie na domysłach modelu. Zanim jednak dotkniemy kodu, w tym wpisie układamy teorię: czym właściwie jest RAG, jak działa „wyszukiwanie po znaczeniu" i jakie elementy Pythona będą nam potrzebne. Świadomie omawiamy tylko to, co faktycznie użyjemy w części trzeciej — bez zaawansowanych strategii i akademickich dygresji.

Problem: model językowy nie zna Twoich dokumentów

Model językowy (LLM) świetnie odpowiada na pytania ogólne, ale nie ma pojęcia o treści Twojej dokumentacji projektowej, notatek czy wewnętrznych procedur — nie było ich w danych treningowych. Zapytany o nie, w najlepszym razie odpowie „nie wiem", a w gorszym — zmyśli coś wiarygodnie brzmiącego.

Najprostsze obejście brzmi: wklej cały dokument do pytania. Działa przy jednej stronie tekstu, ale nie przy stu plikach — okno kontekstu modelu jest ograniczone, a wysyłanie wszystkiego za każdym razem jest wolne i drogie.

Rozwiązanie: RAG

RAG (Retrieval-Augmented Generation) rozwiązuje to w dwóch krokach:

  1. Retrieval (wyszukanie) — spośród wszystkich dokumentów znajdź tylko te fragmenty, które są związane z pytaniem.
  2. Generation (generowanie) — wyślij do modelu pytanie plus znalezione fragmenty i poproś o odpowiedź opartą na nich.

Model nie musi znać dokumentów z góry — dostaje właściwy kontekst dokładnie w momencie, gdy jest potrzebny. Cała „magia" RAG sprowadza się więc do jednego problemu: jak szybko znaleźć fragmenty pasujące do pytania?

Embeddingi — tekst jako punkt w przestrzeni

Komputer nie rozumie tekstu, ale świetnie porównuje liczby. Embedding to zamiana fragmentu tekstu na długi wektor liczb (kilkaset wartości), który reprezentuje jego znaczenie. Kluczowa własność: teksty o podobnym znaczeniu dostają podobne wektory — nawet jeśli nie mają żadnego wspólnego słowa. „Jak zresetować hasło?" i „procedura odzyskiwania dostępu do konta" wylądują blisko siebie.

Tu przydaje się intuicja z wpisu o macierzach: embedding to po prostu lista liczb, a zbiór embeddingów wszystkich fragmentów — lista list, czyli macierz. Porównanie pytania z dokumentami to policzenie, który wiersz tej macierzy leży najbliżej wektora pytania.

Indeks i chunki — dokument pocięty na kawałki

Całego dokumentu nie zamienia się na jeden embedding — zbyt długi tekst „rozmywa" znaczenie. Zamiast tego dokument tnie się na chunki (fragmenty po kilkaset słów), a każdy chunk dostaje własny embedding. Wszystkie te pary (fragment tekstu + jego wektor) trzymane razem to indeks.

Cały przepływ RAG wygląda więc tak:

  1. wczytaj dokument → potnij na chunki → policz embeddingi → zapisz w indeksie (robione raz, przy dodawaniu dokumentu),
  2. pytanie użytkownika → policz jego embedding → znajdź najbliższe chunki → wyślij je z pytaniem do LLM → zwróć odpowiedź (robione przy każdym pytaniu).

W naszej wersji indeks będzie żył w pamięci procesu — po restarcie aplikacji trzeba zaindeksować dokumenty ponownie. To celowe uproszczenie: do nauki i prototypu w zupełności wystarczy, a unikamy tematu baz wektorowych.

Co z Pythona będzie potrzebne?

Zbudowanie tego w części trzeciej wymaga zaskakująco niewiele — i wszystko to omawialiśmy już w serii o podstawach Pythona:

  • słowniki i JSON — API przyjmuje i zwraca dane w formacie JSON, który w Pythonie jest po prostu słownikiem (część o słownikach),
  • funkcje — każdy endpoint API to zwykła funkcja Pythona,
  • listy — indeksowane dokumenty i znalezione fragmenty to listy,
  • środowisko wirtualne i pip — biblioteki (LlamaIndex, FastAPI) zainstalujemy w izolowanym venv, żeby nie zaśmiecać systemowego Pythona,
  • zmienne środowiskowe — klucz API do modelu językowego nigdy nie trafia do kodu; program odczyta go ze zmiennej środowiskowej.

Podsumowanie

RAG to prosty pomysł: zamiast uczyć model Twoich dokumentów, znajdź właściwe fragmenty i podaj mu je razem z pytaniem. Fundament to embeddingi (tekst jako wektor liczb), chunki (dokument pocięty na kawałki) i indeks (wszystko razem, u nas — w pamięci). W następnej części zobaczymy, jak te pojęcia mapują się na konkretne klasy biblioteki LlamaIndex, a w trzeciej — złożymy z nich kompletne API.