fbpx
AIArtykułPython

Architektura nowoczesnych systemów RAG: od podstaw do RAG 2.0 na przykładzie Qdrant Cloud

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:

Diagram przepływu danych w potoku RAG: chunking, embedding, wyszukiwanie wektorowe, wybór top-K, budowa promptu i generowanie odpowiedzi przez LLM

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 😊

Ekran logowania do Qdrant Cloud z opcjami kontynuacji przez Google, GitHub lub adres e-mail

Po zalogowaniu do Qdrant możemy utworzyć klaster

Panel Qdrant Cloud z sekcją Clusters i zachętą do utworzenia pierwszego klastra
Formularz tworzenia klastra w Qdrant Cloud: wybór wariantu Free Cluster, nazwa klastra, wersja, dostawca chmury i region

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 😉.

Okno Qdrant Cloud z nowo utworzonym kluczem API i adresem klastra; obie wartości są zamazane

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:

Konsola z promptem zbudowanym przez RAG 1.0: kontekst złożony z fragmentów Pana Tadeusza oraz pytanie o położenie Litwy

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:

Konsola z promptem RAG 2.0 po rerankingu: te same fragmenty kontekstu ustawione w innej kolejności i instrukcja, by nie zgadywać

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.

Wiktor Filipiuk

Wiktor Filipiuk [in]

Machine Learning Engineer. Od 2014 roku tworzy i wdraża modele uczenia maszynowego w obszarach finansów, energetyki, robotyki i medycyny. Największą przyjemność sprawia mu przekazywanie wiedzy i wspólne odkrywanie tajników programowania w języku Python.

Back to top button