Pewien człowiek miał na dysku trzysta PDF-ów — faktury, notatki ze spotkań, instrukcje do sprzętu, o którym już zdążył zapomnieć, że go w ogóle kupił — więc zrobił rzecz z pozoru oczywistą: zapytał lokalny model „ej, ile płaciłem za tę kartę w zeszłym roku?". Chciał tylko odpowiedzi, lokalnie, bez wysyłania własnego życia do cudzej chmury. Model odpowiedział z pełną powagą — podał kwotę, sklep i datę, wszystkie trzy wyssane z palca. Bo skąd niby miałby to wiedzieć? Twoich faktur nie było w danych treningowych. Model zna świat do dnia, w którym zamknięto mu trening, i ani tokena dalej.

I tu pojawia się pytanie, które w dyskusjach o lokalnych modelach wraca co tydzień jak bumerang: „to jak zrobić, żeby model gadał o MOICH danych?". Pierwszy odruch połowy internetu to „dotrenuj go". Zły odruch — o tym za chwilę. Odruch drugi, ten dobry, to trzy litery: RAG.

Co to właściwie jest ten RAG

RAG — retrieval-augmented generation, czyli generowanie wspomagane wyszukiwaniem — to pomysł starszy, niż się wydaje. Opisali go Lewis i spółka w pracy z 2020 roku, jeszcze zanim „chatbot" wszedł pod strzechy.1 Idea jest sprytnie prosta: zamiast upychać całą wiedzę świata w wagach modelu (to autorzy nazywają pamięcią parametryczną — wszystko, co model „umie z głowy"), doklejasz mu drugą pamięć — nieparametryczną. U Lewisa był to gęsty indeks wektorowy całej Wikipedii, przeszukiwany przez osobny model-wyszukiwarkę. Model najpierw pobiera pasujące fragmenty, a dopiero potem generuje odpowiedź, patrząc na to, co znalazł.

Przełóż to na twój folder z PDF-ami. Nie douczasz modelu, że kartę kupiłeś za tyle a tyle. Ty w momencie zadawania pytania podsuwasz mu odpowiedni kawałek faktury i mówisz: „odpowiedz na podstawie tego". Model dostaje ściągę tuż przed odpowiedzią. Cała sztuka w tym, żeby z trzystu PDF-ów wyłuskać akurat ten jeden akapit, który pasuje — i zrobić to w ułamku sekundy.

Jak to się spina, krok po kroku

No dobra, „wyłuskać pasujący akapit" brzmi ładnie, ale jak maszyna ma wiedzieć, co pasuje do „ile płaciłem za kartę"? Rozłóżmy łańcuch na części, bo on ma dokładnie cztery ogniwa i każde robi jedną robotę.

Raz — chunking. Dzielisz dokumenty na kawałki (chunki). Nie wrzucasz całego 40-stronicowego PDF-a jako jednej bryły, tylko tniesz na fragmenty po parę zdań. I tu pierwsza pułapka, w którą wpada każdy na starcie: cięcie na sztywno co X znaków. Utniesz w połowie zdania, w połowie tabelki z cenami — i fragment traci sens, a razem z nim traci sens jego reprezentacja. Podział semantyczny (po akapitach, sekcjach, zdaniach) jest nudniejszy w implementacji, ale retrieval potem dziękuje.

Dwa — embeddingi. Każdy chunk przepuszczasz przez model embeddingowy — osobny, mały model, który zamienia tekst w wektor liczb, czyli osadzenie (embedding). Sęk w tym, że te wektory układają się w przestrzeni tak, że teksty o podobnym znaczeniu lądują blisko siebie. „Zapłaciłem 1400 zł za RTX-a" i „koszt karty graficznej wyniósł" wylądują obok, mimo że nie mają wspólnego słowa. To jest serce całej zabawy.

Dwa modele, które na lokalnej scenie widać najczęściej:

ModelWymiar wektoraKontekstJęzykiLicencja
nomic-embed-text-v1.5768 (skalowalny w dół)8192 tokenówgłównie angielskiApache 2.0
bge-m310248192 tokenów100+MIT

nomic-embed-text-v1.5 ma jedną fajną sztuczkę — Matryoshka Representation Learning: natywnie robi wektor 768-wymiarowy, ale możesz go przyciąć w dół, choćby do 64 wymiarów, tracąc na jakości zaskakująco mało.2 Mniejszy wektor to mniejsza baza i szybsze wyszukiwanie. bge-m3 gra w inną grę — ogarnia ponad sto języków (czyli i polski), a przy okazji jeden model daje ci trzy tryby wyszukiwania naraz: gęsty, leksykalny i wielowektorowy w stylu ColBERT.3 Do tego drugiego zaraz wrócimy.

Dlaczego to ważne dla lokalnego LLM

Najczęstszy babol na starcie z nomikiem: model wymaga prefiksów instrukcji. Dokument, który indeksujesz do bazy, poprzedzasz search_document: , a zapytanie użytkownika — search_query: . To nie kosmetyka. Wrzucisz zapytanie z prefiksem dokumentu (albo w ogóle bez prefiksu) i trafność retrievalu leci na łeb, bo model liczy embedding „nie w tym trybie", w którym powinien.2 Embedding dokumentu i embedding pytania to u nomika DWIE różne operacje, nie jedna.

Trzy — baza wektorowa. Te wszystkie wektory trzeba gdzieś trzymać i umieć szybko przeszukać. Tu masz dwa światy. Z jednej strony FAISS od Meta — biblioteka do wyszukiwania podobieństwa w gęstych wektorach, z całym menu typów indeksu: IndexFlatL2 (dokładne przeszukanie wszystkiego), IVF (partycjonowanie na kubełki), HNSW (grafy nawigacyjne) czy Product Quantization (stratna kompresja wektorów).4 Szybka i potężna, ale sama z siebie nie trzyma metadanych ani nie martwi się trwałością — to silnik, nie baza. Z drugiej strony Chroma — pełna baza wektorowa open source, która obok embeddingów przechowuje metadane, filtruje po nich, wspiera wyszukiwanie gęste, rzadkie i hybrydowe, i którą odpalisz lokalnie jednym importem.5 Mylenie tych dwóch to klasyk: FAISS da ci surową prędkość, Chroma da ci wygodę i „ten dokument jest z folderu faktury, rok 2025".

Cztery — retrieval i wstrzyknięcie kontekstu. Użytkownik pyta. Ty liczysz embedding pytania (z prefiksem search_query: , pamiętasz?), baza zwraca ci top-k najbliższych chunków — powiedzmy trzy albo pięć najbardziej pasujących — a ty wklejasz je do promptu tuż przed pytaniem. To jest to całe context injection: „Oto fragmenty z dokumentów: […]. Na ich podstawie odpowiedz: ile płaciłem za kartę?". Model generuje odpowiedź warunkowaną na tym, co mu podsunąłeś. Koniec łańcucha.

I tu druga pułapka, na którą łapie się nawet ogarnięty ludek: „skoro więcej kontekstu = lepiej, to wezmę top-k równe 20". Nie. Nadmiar fragmentów rozmywa uwagę modelu, zapycha okno kontekstu i topi ten jeden trafny akapit w morzu luźno pasujących. Mniej, ale celniej. Stąd zresztą pomysł na re-ranking — najpierw baza wyławia dwadzieścia kandydatów, a osobny model-sędzia przestawia je i oddaje ci trzy najlepsze. Twórcy bge-m3 wprost zalecają łączyć jego tryby hybrydowo i dokładać re-ranker, jak zależy ci na maksymalnej jakości.3

Czego RAG NIE robi (i dlaczego to ważne)

Wróćmy do tego „złego odruchu" z początku. RAG nie douczy modelu. Nie dotyka jego wag, nie zaszywa wiedzy na stałe. Podaje kontekst w czasie inferencji — i tyle. Wywal dokument z bazy wektorowej, a model „zapomina" go w tej samej sekundzie, bo nigdy naprawdę go nie „zapamiętał". To zupełnie inna bajka niż fine-tuning, gdzie modyfikujesz parametry i żeby zaktualizować wiedzę, musisz przepuścić model przez kolejny trening — techniki pokroju QLoRA rozbieraliśmy tutaj. Prosta zasada na później: wiedza, która się zmienia albo jest prywatna → RAG. Zachowanie, styl, format odpowiedzi → fine-tuning.

I jeszcze jedno, żebyś nie wyszedł z fałszywym poczuciem bezpieczeństwa: RAG nie gwarantuje braku halucynacji. Podsuniesz modelowi trafny akapit, a on i tak potrafi go zignorować albo przekręcić. Redukujesz ryzyko, nie kasujesz go do zera. No i pamiętaj, że te embeddery mają okno 8192 tokenów — wrzucisz dłuższy dokument bez dzielenia, a końcówka po prostu zostanie obcięta i nigdy nie trafi do bazy.

Zrób to sam

Najfajniejsze w lokalnym RAG-u jest to, że embeddingi policzysz tym samym llama.cpp, którym odpalasz zwykły model — flagi rozbieraliśmy osobno. Ściągasz GGUF-a z nomikiem i odpalasz serwer z jedną dodatkową flagą:

llama-server -m nomic-embed-text-v1.5.Q8_0.gguf \
  --embeddings \
  --host 0.0.0.0 --port 8080

--embeddings przełącza llama-server w tryb liczenia wektorów.6 Potem strzelasz do niego jak do zwykłego API — i tu drobiazg, o który łatwo się potknąć: endpoint /embeddings obsługuje wszystkie tryby poolingu, łącznie z --pooling none (wektor per token), natomiast zgodny z OpenAI /v1/embeddings wymaga poolingu innego niż none.6

curl http://localhost:8080/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{"input": "search_document: RTX 4070, faktura, 2799 zł"}'

Widzisz ten search_document: wklejony w treść? To nie ozdoba — to ten prefiks, bez którego nomic liczy nie to, co trzeba. Dostajesz z powrotem wektor 768 liczb, wsadzasz go do Chromy albo FAISS-a razem z oryginalnym tekstem i metadanymi, i powtarzasz dla każdego chunka. Zapytanie liczysz identycznie, tylko z prefiksem search_query: . Cały lokalny RAG to te cztery ogniwa spięte skryptem na sto linijek — reszta to strojenie.

Kończąc, wyobraź to sobie tak: fine-tuning jest jak wysłanie modelu z powrotem do szkoły, żeby na nowo wykuł materiał. RAG to podanie mu otwartej książki tuż przed egzaminem — z zakładką na właściwej stronie. Model dalej jest tym samym modelem, wagi ani drgną. Zmienia się tylko to, że tym razem nie musi zgadywać, ile płaciłeś za tę kartę. Ma to czarno na białym, w akapicie, który sam mu podsunąłeś.

Przypisy · źródła


  1. Lewis et al., „Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks", NeurIPS 2020 — arxiv.org/abs/2005.11401. Definicja RAG oraz podział na pamięć parametryczną (model seq2seq) i nieparametryczną (gęsty indeks wektorowy Wikipedii przeszukiwany przez neuronowy retriever). ↩︎

  2. Karta modelu nomic-ai/nomic-embed-text-v1.5, Hugging Face — huggingface.co/nomic-ai/nomic-embed-text-v1.5. Wymiar 768 z Matryoshka Representation Learning (skalowanie w dół, np. do 64), kontekst 8192 tokenów, wymagane prefiksy search_document: / search_query: , licencja Apache 2.0. ↩︎ ↩︎

  3. Karta modelu BAAI/bge-m3, Hugging Face — huggingface.co/BAAI/bge-m3. Wymiar 1024, kontekst 8192, ponad 100 języków (baza XLM-RoBERTa), trzy tryby retrievalu naraz (dense / sparse / multi-vector w stylu ColBERT), zalecenie hybrydowego łączenia z re-rankerem, licencja MIT. ↩︎ ↩︎

  4. FAISS (Meta AI Research) — faiss.ai. Biblioteka do similarity search i klastrowania gęstych wektorów; typy indeksów IndexFlatL2, IVF, HNSW, Product Quantization, wsparcie GPU. ↩︎

  5. Dokumentacja Chroma, „Introduction" — docs.trychroma.com/docs/overview/introduction. Open-source’owa baza wektorowa (Apache 2.0): przechowywanie embeddingów z metadanymi, wyszukiwanie dense/sparse/hybrydowe, filtrowanie po metadanych, tryby lokalny / self-hosted / Chroma Cloud. ↩︎

  6. README serwera llama.cpp (ggml-org) — github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md. Flaga --embeddings do llama-server; endpoint /embeddings wspiera wszystkie tryby poolingu (w tym --pooling none), a zgodny z OpenAI /v1/embeddings wymaga poolingu innego niż none↩︎ ↩︎