
Wstęp
Nowoczesne systemy AI coraz częściej łączą modele językowe z zewnętrznymi źródłami wiedzy. Jednym z najpopularniejszych podejść jest stosowanie RAG — Retrieval-Augmented Generation, czyli generowanie odpowiedzi w oparciu o kontekst pobrany z bazy danych.
W poniższym artykule przyjrzymy się usprawnionemu podejściu, tzw. RAG 2.0.
RAG (Retrieval-Augmented Generation)
To architektura systemu AI, w której LLM nie odpowiada „z pamięci”, lecz korzysta z informacji pobranej z zewnętrznej bazy. Pipeline może wyglądać następująco:

RAG 2.0 (RetrievalAugmented Generation 2.0)
Jest to rozwinięcie klasycznego systemu RAG. Wersja 2.0 dodaje elementy takie jak reranking (CrossEncoder), multihop retrieval, iteracyjne zapytania oraz agentowe planowanie (ReAct). Dzięki temu stworzony system lepiej rozumie kontekst, redukuje halucynacje i łączy informacje z wielu źródeł, co czyni go bardziej precyzyjnym i użytecznym w złożonych zastosowaniach analitycznych.
W tym artykule skupimy się na zbudowaniu RAGa oraz RAGa 2.0 w oparciu o wektorową bazę danych Qdrant (https://qdrant.tech/cloud/).
Wektorowa baza danych, to baza która przechowuje dokumenty w postaci wektorów (embeddingów). Umożliwia wyszukiwanie semantyczne — nie po słowach, lecz po ich znaczeniu.
Qdrant jest jedną z wielu istniejących providerów wektorowych baz danych, jej zdecydowaną zaletą jest możliwość stworzenia za darmo klastra, w którym będziemy przechowywać wektory. Alterantywy dla tej bazy to m. in. ChromaDB, Pinecone, Weaviate, Milvus.
Poniżej znajduje się krótka instrukcja jak stworzyć swój pierwszy klaster w Qdrant 😊

Po zalogowaniu do Qdrant możemy utworzyć klaster


Następnie pojawi się okno, które pokaże nam klucz API (API Key) oraz Cluster Endpoint:Obie te wartości zapiszmy na boku w pliku .txt, żeby móc z nich skorzystać przy tworzeniu kodu 😉.

Kodowanie 😀
Część wspólna konieczna do stworzenia zarówno systemu RAG 1.0 i RAG 2.0 (punkty 1–5). Jako zbiór danych, dla których stworzę embeddingi wykorzystam fragment „Pana Tadeusza”, a mianowicie pierwsze 22 wiersze:
text = """
Litwo! Ojczyzno moja! ty jesteś jak zdrowie.
Ile cię trzeba cenić, ten tylko się dowie,
Kto cię stracił. Dziś piękność twą w całej ozdobie
Widzę i opisuję, bo tęsknię po tobie.
..........
A wszystko przepasane, jakby wstęgą, miedzą
Zieloną, na niej z rzadka ciche grusze siedzą.
"""
1. Połączenie z Qdrant Cloud
Tworzymy klienta, który umożliwi nam połączenie się z instancją Qdrant Cloud. Od tego momentu możemy tworzyć kolekcje, dodawać punkty i wykonywać zapytania wektorowe.
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, VectorParams, Distance
client = QdrantClient(
url="https://...qdrant.io",
api_key="TWÓJ-API-KEY",
)
2. Chunkowanie dokumentu
Następnie podzielimy poemat na linijki. Każda linia staje się osobnym „chunkiem”, który później będzie embedowany i zapisany w bazie.
def chunk_text(text):
lines = text.split('\n')
return [line for line in lines if len(line) > 0]
chunks = chunk_text(text)
3. Tworzenie embeddingów
Tutaj skorzystamy z modelu multilingual-e5-large, który wystarczająco dobrze działa dla języka polskiego. Każdy chunk zostanie zamieniony na wektor — reprezentację semantyczną.
embedder = SentenceTransformer("intfloat/multilingual-e5-large")
vectors = embedder.encode(chunks)
4. Tworzenie kolekcji w Qdrant
Teraz Sprawdźmy, czy kolekcja istnieje. Jeśli nie stwórzmy ją, definiując rozmiar wektora i metrykę. My skorzystamy z metryki kosinusowej.
collections = client.get_collections().collections
collection_names = [c.name for c in collections]
COLLECTION_NAME = 'pan_tadeusz'
if COLLECTION_NAME not in collection_names:
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(
size=vectors.shape[1],
distance=Distance.COSINE
)
)
5. Upsert danych
Każdy chunk załadujemy do Qdrant jako unikalny punkt z embeddingiem i payloadem.
points = [
PointStruct(
id=i,
vector=vectors[i].tolist(),
payload={"text": chunks[i]}
)
for i in range(len(chunks))
]
client.upsert(collection_name=COLLECTION_NAME, points=points)
W tym miejscu zaczniemy tworzyć RAG. Na początek stwórzmy wersję podstawową
6. RAG 1.0
6.1 Wektorowe wyszukiwanie
Zbudujemy tutaj embedding pytania, i wyślemy je w postaci wektora do Qdrant, aby pobierać top5 najbardziej podobnych chunków.
query = "gdzie lezy Litwa?"
query_vec = embedder.encode(query).tolist()
results = client.query_points(
collection_name=COLLECTION_NAME,
query=query_vec,
limit=5,
with_payload=True,
)
6.2 Składanie kontekstu
Łączymy znalezione fragmenty w jeden kontekst i wyświetlamy wyniki
context = "\n".join([point.payload['text'] for point in results.points])
print("Kontekst znaleziony w Qdrant:\n", context)
6.3 Budowa promptu
W ostatnim punkcie RAG budujemy zapytanie, które możemy wysłać do wybranego LLM-a aby uzyskać odpowiedź w formie akceptowalnej dla użytkownika-człowieka:
prompt = f"""
Użyj poniższego kontekstu, aby odpowiedzieć na pytanie.
Kontekst:
{context}
Pytanie:
{query}
Odpowiedź (maks 150 słów):
"""
Wynik w konsoli:

Teraz przejdziemy do RAG 2.0 (pamiętajmy, że ten workflow może rozpocząć się od razu po wykonaniu punktu 5-ego „upsert danych”)
7. RAG 2.0 – zastosujemy tutaj dodanie metody rerankingu, która poprawi trafność wyszukań.
7.1 CrossEncoder
Zastosujemy dodatkowy model, który oceni pary (query, chunk) pod kątem ich dopasowania jako para pytanie-odpowiedź.
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
7.2 Vector search
W tym momencie musimy pobrać więcej chunków (np. 10), aby mieć z czego wybierać.
top_k_retrieval = 10
results = client.query_points(
collection_name=COLLECTION_NAME,
query=query_vec,
limit=top_k_retrieval,
with_payload=True,
)
7.3 Reranking
Rerankowanie przy pomocy CrossEncoder oceni trafność każdego chunku względem pytania.
pairs = [[query, p.payload["text"]] for p in results.points]
scores = reranker.predict(pairs)
scored_points = list(zip(results.points, scores))
scored_points.sort(key=lambda x: x[1], reverse=True)
7.4 Wybór najlepszych chunków
Aby móc porównać wyniki z RAG 1.0 wybierzemy tutaj również 5 ostatecznych chunków.
top_k_rerank = 5
top_points = scored_points[:top_k_rerank]
context_chunks = [p.payload["text"] for p, s in top_points]
context = "\n".join(context_chunks)
7.5 Budowa promptu
Prompt ma teraz dostęp do chunków, które nieco lepiej zgrywają się pod kątem szukania odpowiedzi na zadane na samym początku pytanie „Gdzie leży Litwa?”.
prompt = f"""
Użyj poniższego kontekstu, aby odpowiedzieć na pytanie po polsku.
Nie zgaduj – jeśli odpowiedź nie wynika z kontekstu, powiedz to wprost.
Kontekst:
{context}
Pytanie:
{query}
Odpowiedź (maks 150 słów):
"""
Wynik w konsoli:

Podsumowanie
Systemy RAG stały się fundamentem nowoczesnych aplikacji AI — od chatbotów, przez wyszukiwanie semantyczne, po zaawansowane analizy danych. Klasyczny RAG 1.0 jest prosty i szybki, ale jego skuteczność ogranicza słabsza kontrola nad kontekstem. RAG 2.0 rozwiązuje te problemy, oferując znacznie większą precyzję, stabilność i odporność na halucynacje.
W ramach projektu zbudowaliśmy w pełni działający system RAG 2.0, wykorzystując Qdrant Cloud jako wektorową bazę danych, SentenceTransformers do generowania embeddingów oraz CrossEncoder do rerankingu wyników. Nasz pipeline — od chunkingu, przez embeddingi i wyszukiwanie wektorowe, aż po końcową odpowiedź LLM — pokazuje, jak połączenie tych narzędzi tworzy skalowalny, produkcyjny system zdolny do pracy zarówno z językiem polskim, jak i wielojęzycznymi zbiorami danych.
To praktyczny dowód, że RAG 2.0 nie jest tylko koncepcją, lecz realnym, efektywnym podejściem do budowy nowoczesnych aplikacji AI.





