RAG (Retrieval-Augmented Generation) to technika, która pozwala modelowi językowemu odpowiadać na podstawie Twoich własnych danych, zamiast tylko wiedzy, na której był trenowany — a baza wektorowa to miejsce, gdzie te dane przechowuje się jako punkty w przestrzeni liczbowej (embeddingi), żeby błyskawicznie znajdować te najbardziej podobne znaczeniowo do pytania użytkownika. W Pythonie typowy pipeline RAG łączy model embeddingowy, bazę wektorową (np. Qdrant) i model językowy, który na końcu generuje odpowiedź na podstawie znalezionych fragmentów.
Model językowy, nawet najlepszy, zna tylko to, na czym był trenowany — nie wie nic o Twojej dokumentacji wewnętrznej, bazie wiedzy firmy czy danych sprzed tygodnia. RAG to sposób, żeby to obejść bez kosztownego douczania modelu od zera: zamiast tego, przy każdym pytaniu, najpierw wyszukuje się najbardziej trafne fragmenty Twoich własnych danych, a dopiero potem prosi się model językowy o odpowiedź na ich podstawie.
Czym jest RAG i baza wektorowa?
RAG (Retrieval-Augmented Generation, czyli "generowanie wzbogacone o wyszukiwanie") to architektura łącząca dwa kroki: najpierw wyszukanie (retrieval) odpowiednich informacji, a potem wygenerowanie (generation) odpowiedzi przez model językowy na ich podstawie. Kluczowy jest ten pierwszy krok — żeby wyszukiwanie działało dobrze, potrzebny jest sposób na znalezienie fragmentów tekstu podobnych znaczeniowo do pytania, a nie tylko takich, które zawierają te same słowa.
Właśnie do tego służy baza wektorowa. Zamiast przechowywać tekst jako zwykłe zdania, zamienia go na wektory liczbowe (embeddingi) — a potem pozwala błyskawicznie znaleźć, które wektory leżą najbliżej siebie w przestrzeni. "Blisko" w tej przestrzeni oznacza "podobne znaczeniowo", nie "zawiera te same litery" — to fundamentalna różnica względem tradycyjnego wyszukiwania pełnotekstowego.
Typowy pipeline RAG w Pythonie wygląda tak:
# 1. Zamiana tekstu na wektor (embedding)
embedding = embedding_model.encode("Jak zresetować hasło?")
# 2. Wyszukanie najbardziej podobnych fragmentów w bazie wektorowej
results = vector_db.search(embedding, limit=5)
# 3. Przekazanie znalezionych fragmentów do modelu językowego
answer = llm.generate(question="Jak zresetować hasło?", context=results)
Matematyka przestrzeni latentnych — dlaczego to w ogóle działa
Embedding to po prostu lista liczb (np. 1536 liczb dla jednego z modeli OpenAI) reprezentująca znaczenie fragmentu tekstu. Model embeddingowy został wytrenowany tak, by fragmenty o podobnym znaczeniu trafiały w blisko siebie położone punkty tej wielowymiarowej przestrzeni — nazywanej przestrzenią latentną (ukrytą), bo te wymiary nie odpowiadają niczemu, co człowiek mógłby nazwać wprost (to nie jest "wymiar emocji" czy "wymiar tematu"), tylko wzorcom wyuczonym statystycznie z ogromnej ilości tekstu.
Żeby zmierzyć, jak blisko leżą dwa wektory, najczęściej liczy się podobieństwo kosinusowe — kąt między dwoma wektorami, a nie ich odległość w linii prostej. Dwa teksty o identycznym znaczeniu, ale różnej długości, mogą mieć bardzo różną "długość" wektora, ale niemal identyczny kierunek — dlatego to kąt, nie dystans, jest tu właściwą miarą podobieństwa.
To wyjaśnia też jeden z najczęstszych błędów, na jaki trafiają osoby budujące swój pierwszy system RAG: błąd niezgodności wymiarów (np. "embedding dimension 384 does not match collection dimensionality 1536"). Każdy model embeddingowy generuje wektory o ściśle określonej, stałej liczbie wymiarów — zmieszanie wektorów z dwóch różnych modeli (np. zaindeksowanie danych jednym modelem, a wyszukiwanie zapytania innym) jest matematycznie bez sensu, bo te liczby opisują zupełnie różne przestrzenie. To jeden z realnych, bardzo często zgłaszanych problemów przy pierwszym kontakcie z tematem — rozwiązanie jest proste (konsekwentnie używać tego samego modelu embeddingowego do indeksowania i wyszukiwania), ale dopóki nie rozumie się dlaczego wymiary muszą się zgadzać, błąd bywa mylący.
Indeksowanie w Qdrant
Qdrant to jedna z popularniejszych otwartoźródłowych baz wektorowych, z natywnym klientem Pythona. Podstawowy przykład indeksowania danych:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="dokumenty",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
client.upsert(
collection_name="dokumenty",
points=[
PointStruct(
id=1,
vector=embedding_model.encode("Jak zresetować hasło?"),
payload={"tekst": "Jak zresetować hasło?", "zrodlo": "faq.md"},
)
],
)
wyniki = client.search(
collection_name="dokumenty",
query_vector=embedding_model.encode("zapomniałem hasła"),
limit=3,
)
Kolekcja (collection_name) to odpowiednik tabeli w bazie relacyjnej —
grupuje wektory o tym samym rozmiarze i metryce podobieństwa (tu:
Distance.COSINE, zgodnie z poprzednią sekcją). payload to zwykłe,
ustrukturyzowane dane dołączone do każdego wektora (oryginalny tekst,
źródło, metadane) — zwracane razem z wynikiem wyszukiwania, żeby wiedzieć,
skąd faktycznie pochodzi dopasowany fragment.
Co dalej, gdy podstawy już działają
Powyższy pipeline wystarczy na pierwszy, działający system RAG — ale produkcyjne wdrożenia zwykle idą dalej w kilku kierunkach: **wyszukiwanie hybrydowe** (łączenie wyszukiwania wektorowego z klasycznym, słowo-kluczowym, bo czasem dokładne dopasowanie frazy jest ważniejsze niż podobieństwo znaczeniowe) razem z rerankingiem (dogrywaniem kolejności wyników drugim, dokładniejszym modelem po wstępnym wyszukiwaniu), staranny podział dłuższych, nieustrukturyzowanych dokumentów (PDF-y, strony HTML) na fragmenty, które niosą pełny sens bez urywania w połowie myśli, oraz kontrolę dostępu (RBAC) na poziomie samej bazy wektorowej, gdy różni użytkownicy systemu powinni mieć dostęp tylko do części zaindeksowanych danych. To tematy, które dokładniej omawiamy w ramach mentoringu z Pythona i AI — jeśli wdrażasz RAG komercyjnie, to naturalny kolejny krok po opanowaniu podstaw opisanych wyżej.


