Kurs Python · Moduł 11: RAG i systemy wieloagentowe
RAG - Rozszerzona Pamięć AI
W tej lekcji6
Wyobraź sobie przewodnika, który zna sawannę tylko z książek wydanych trzy lata temu. Pytasz go o nowy szlak do wodopoju, a on z pełnym przekonaniem opisuje drogę, której już nie ma. Tak zachowuje się model językowy bez dostępu do aktualnej wiedzy. Witaj w przedostatnim etapie Python Safari! W tej lokacji nauczymy model zaglądać do notatnika terenowego, zanim cokolwiek powie.
Tak jak wielkie ssaki ewoluowały, rozwijając większe mózgi i lepszą pamięć, tak modele językowe ewoluują dzięki technologii RAG (Retrieval-Augmented Generation). To technika, która daje AI dostęp do zewnętrznej wiedzy bez ponownego trenowania modelu.
Czego się nauczysz
- rozpoznawać ograniczenia LLM, które rozwiązuje RAG,
- opisać trzy kroki pipeline'u: retrieve, augment, generate,
- zbudować najprostszy RAG w czystym Pythonie z OpenAI i NumPy,
- dzielić długie dokumenty na fragmenty (chunking),
- mierzyć jakość wyszukiwania metrykami precision i recall.
Problem z tradycyjnymi LLM
Modele językowe mają fundamentalne ograniczenia, podobnie jak zwierzęta przystosowane tylko do jednego środowiska:
Ograniczenia bazowych LLM:
- Knowledge cutoff - wiedza zamrożona w czasie treningu
- Halucynacje - generowanie nieprawdziwych informacji
- Brak kontekstu firmowego - nie znają Twoich dokumentów
- Wysokie koszty fine-tuningu - dostosowanie modelu jest drogie
Najważniejsze jest pierwsze ograniczenie: model nie wie nic o tym, co wydarzyło się po zakończeniu treningu, i nie zna Twoich prywatnych dokumentów. RAG nie zmienia samego modelu. Zmienia to, co model dostaje do przeczytania przed odpowiedzią.
Czym jest RAG?
RAG to architektura, która łączy wyszukiwanie informacji z generowaniem tekstu. Poniższy schemat pokazuje drogę pytania od użytkownika aż do gotowej odpowiedzi:
1┌─────────────────────────────────────────────────────────┐
2│ RAG Pipeline │
3├─────────────────────────────────────────────────────────┤
4│ │
5│ Pytanie użytkownika │
6│ │ │
7│ ▼ │
8│ ┌─────────────────┐ │
9│ │ Embedding │ ← Zamiana na wektor │
10│ └────────┬────────┘ │
11│ │ │
12│ ▼ │
13│ ┌─────────────────┐ ┌─────────────────┐ │
14│ │ Vector Search │────▶│ Vector Database │ │
15│ └────────┬────────┘ └─────────────────┘ │
16│ │ │
17│ ▼ │
18│ ┌─────────────────┐ │
19│ │ Retrieved Docs │ ← Top-K podobnych dokumentów │
20│ └────────┬────────┘ │
21│ │ │
22│ ▼ │
23│ ┌─────────────────┐ │
24│ │ LLM + Context │ ← Generowanie z kontekstem │
25│ └────────┬────────┘ │
26│ │ │
27│ ▼ │
28│ Odpowiedź │
29└─────────────────────────────────────────────────────────┘Czytaj go od góry. Pytanie zamieniamy na wektor liczb (embedding), szukamy w bazie wektorowej fragmentów o podobnym znaczeniu, bierzemy kilka najlepszych (Top-K) i doklejamy je do promptu. Dopiero wtedy LLM generuje odpowiedź. Model się nie zmienia, zmienia się tylko jego "ściąga" na czas jednego pytania.
Podstawowa implementacja RAG
Zbudujmy cały pipeline w kilkudziesięciu linijkach. Zaczynamy od klienta OpenAI, NumPy i malutkiej bazy wiedzy, czyli listy zdań, które pełnią rolę notatnika terenowego:
1from openai import OpenAI
2import numpy as np
3
4client = OpenAI()
5
6# Prosta baza wiedzy
7knowledge_base = [
8 "Python Safari to kurs programowania w Pythonie z 12 modułami.",
9 "RAG łączy wyszukiwanie dokumentów z generowaniem tekstu przez LLM.",
10 "Embeddings to wektorowe reprezentacje tekstu w przestrzeni semantycznej.",
11 "Vector databases przechowują i wyszukują embeddings efektywnie.",
12 "LlamaIndex to framework do budowania aplikacji RAG.",
13 "CrewAI umożliwia tworzenie systemów multi-agentowych."
14]OpenAI() czyta klucz API ze zmiennej środowiskowej OPENAI_API_KEY, więc nie wpisujemy go w kod. Sama baza wiedzy to zwykła lista stringów, nic magicznego.
Teraz dwie funkcje pomocnicze. Pierwsza wysyła tekst do modelu text-embedding-3-small i zwraca wektor, druga liczy podobieństwo kosinusowe, czyli jak bardzo dwa wektory "patrzą" w tę samą stronę:
1def get_embedding(text: str) -> list[float]:
2 """Generuje embedding dla tekstu."""
3 response = client.embeddings.create(
4 model="text-embedding-3-small",
5 input=text
6 )
7 return response.data[0].embedding
8
9def cosine_similarity(vec1: list, vec2: list) -> float:
10 """Oblicza podobieństwo kosinusowe dwóch wektorów."""
11 vec1, vec2 = np.array(vec1), np.array(vec2)
12 return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))Podobieństwo kosinusowe przyjmuje wartości od -1 do 1, a im bliżej 1, tym bliższe znaczenie tekstów. Dokładniej rozłożymy je na części w następnej lekcji.
Trzeci krok to wyszukiwarka. Dla każdego dokumentu liczymy podobieństwo do pytania, sortujemy malejąco i zwracamy top_k najlepszych:
1def retrieve_relevant_docs(query: str, docs: list, top_k: int = 3) -> list[str]:
2 """Wyszukuje najbardziej podobne dokumenty."""
3 query_embedding = get_embedding(query)
4
5 similarities = []
6 for doc in docs:
7 doc_embedding = get_embedding(doc)
8 similarity = cosine_similarity(query_embedding, doc_embedding)
9 similarities.append((doc, similarity))
10
11 # Sortuj po podobieństwie
12 similarities.sort(key=lambda x: x[1], reverse=True)
13
14 return [doc for doc, _ in similarities[:top_k]]Zwróć uwagę na jedną słabość: ta funkcja liczy embedding każdego dokumentu przy każdym pytaniu. Przy sześciu zdaniach to nie boli, przy tysiącach dokumentów byłoby drogie i wolne. Dlatego w kolejnych lekcjach embeddingi dokumentów liczymy raz i przechowujemy w bazie wektorowej.
Na koniec składamy wszystko w funkcję rag_query, w której widać trzy litery skrótu: Retrieve, Augment, Generate:
1def rag_query(question: str) -> str:
2 """Pełny pipeline RAG."""
3 # 1. Retrieve - znajdź relevantne dokumenty
4 relevant_docs = retrieve_relevant_docs(question, knowledge_base)
5
6 # 2. Augment - zbuduj prompt z kontekstem
7 context = "\n".join(relevant_docs)
8
9 prompt = f"""Odpowiedz na pytanie używając TYLKO poniższego kontekstu.
10Jeśli nie możesz odpowiedzieć na podstawie kontekstu, powiedz "Nie wiem".
11
12Kontekst:
13{context}
14
15Pytanie: {question}
16"""
17
18 # 3. Generate - wygeneruj odpowiedź
19 response = client.chat.completions.create(
20 model="gpt-4o-mini",
21 messages=[{"role": "user", "content": prompt}]
22 )
23
24 return response.choices[0].message.content
25
26# Test
27answer = rag_query("Co to jest RAG?")
28print(answer)Kluczowa jest instrukcja w prompcie: "używając TYLKO poniższego kontekstu" oraz prawo do odpowiedzi "Nie wiem". To właśnie ogranicza halucynacje. Sam model gpt-4o-mini pozostał dokładnie taki sam jak przed chwilą, zmienił się tylko prompt.
Chunking - dzielenie dokumentów
Długie dokumenty trzeba podzielić na mniejsze fragmenty (chunks), bo embedding całej książki rozmywa znaczenie, a prompt ma ograniczoną długość. Najprostsza metoda tnie tekst co określoną liczbę znaków, z zakładką (overlap):
1from typing import Generator
2
3def simple_chunker(text: str, chunk_size: int = 500, overlap: int = 100) -> Generator[str, None, None]:
4 """Prosta funkcja do dzielenia tekstu na chunki."""
5 start = 0
6 while start < len(text):
7 end = start + chunk_size
8 chunk = text[start:end]
9 yield chunk
10 start = end - overlap # Overlap dla zachowania kontekstuZakładka sprawia, że zdanie przecięte na granicy trafi w całości przynajmniej do jednego fragmentu. Pilnuj tylko, żeby overlap był mniejszy niż chunk_size, inaczej pętla nigdy się nie skończy. Ostatni fragment bywa też bardzo krótki, bo to końcówka tekstu.
Lepiej zachowuje sens podział po zdaniach. Wyrażenie regularne rozcina tekst po kropce, wykrzykniku lub pytajniku, a potem sklejamy po kilka zdań:
1def sentence_chunker(text: str, sentences_per_chunk: int = 5) -> list[str]:
2 """Dzieli tekst na chunki po zdaniach."""
3 import re
4
5 # Prosta segmentacja zdań
6 sentences = re.split(r'(?<=[.!?])\s+', text)
7
8 chunks = []
9 for i in range(0, len(sentences), sentences_per_chunk):
10 chunk = ' '.join(sentences[i:i + sentences_per_chunk])
11 chunks.append(chunk)
12
13 return chunks
14
15# Przykład użycia
16document = """
17Python to język programowania wysokiego poziomu. Został stworzony przez
18Guido van Rossuma. Jest bardzo popularny w Data Science i Machine Learning.
19RAG to technika łącząca retrieval z generowaniem. Pozwala modelom LLM
20korzystać z zewnętrznych źródeł wiedzy. Jest kluczowa dla enterprise AI.
21"""
22
23chunks = sentence_chunker(document, sentences_per_chunk=2)
24for i, chunk in enumerate(chunks):
25 print(f"Chunk {i}: {chunk[:50]}...")Każdy wypisany chunk zawiera teraz pełne zdania, a treść dokumentu się nie zmieniła, zmieniło się tylko jej opakowanie. Polecam zaczynać od podziału po zdaniach, bo fragmenty ucięte w połowie myśli dają słabsze embeddingi.
Metryki jakości RAG
Jak sprawdzić, czy RAG działa dobrze? Oceniamy dwa etapy osobno: czy wyszukiwanie znalazło właściwe fragmenty i czy odpowiedź trzyma się kontekstu. Oto szkielet klasy z trzema typowymi metrykami:
1from dataclasses import dataclass
2
3@dataclass
4class RAGMetrics:
5 """Metryki do ewaluacji systemu RAG."""
6
7 def relevance_score(self, query: str, retrieved_doc: str) -> float:
8 """Czy dokument jest relevantny do pytania?"""
9 # W praktyce używamy LLM do oceny
10 pass
11
12 def faithfulness_score(self, answer: str, context: str) -> float:
13 """Czy odpowiedź opiera się na kontekście?"""
14 # Sprawdza halucynacje
15 pass
16
17 def answer_correctness(self, answer: str, ground_truth: str) -> float:
18 """Czy odpowiedź jest poprawna?"""
19 # Porównanie z ground truth
20 passMetody mają tylko pass, bo w praktyce ocenę trafności (relevance), wierności kontekstowi (faithfulness) i poprawności odpowiedzi robi zwykle drugi model w roli sędziego. To szkielet do uzupełnienia, nie gotowe narzędzie.
Wyszukiwanie da się za to zmierzyć zwykłą arytmetyką zbiorów. Precision mówi, jaka część znalezionych dokumentów jest trafna, recall, jaką część trafnych dokumentów znaleźliśmy:
1# Przykład prostej ewaluacji
2def evaluate_retrieval(query: str, retrieved: list[str], relevant: list[str]) -> dict:
3 """Oblicza precision i recall dla retrieval."""
4 retrieved_set = set(retrieved)
5 relevant_set = set(relevant)
6
7 true_positives = len(retrieved_set & relevant_set)
8
9 precision = true_positives / len(retrieved_set) if retrieved_set else 0
10 recall = true_positives / len(relevant_set) if relevant_set else 0
11
12 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
13
14 return {
15 "precision": precision,
16 "recall": recall,
17 "f1": f1
18 }Przykład: jeśli system zwrócił ["a", "b", "c"], a trafne były ["a", "d"], to precision wynosi 1/3, recall 1/2, a F1, czyli średnia harmoniczna obu, 0.4.
RAG to fundament nowoczesnych aplikacji AI enterprise. W następnej lekcji poznasz embeddings - serce całego systemu wyszukiwania - i zobaczysz, dlaczego podobne znaczenia lądują blisko siebie.
Zapamiętaj z tej wyprawy: RAG to przewodnik, który przed każdą odpowiedzią zagląda do swojego notatnika terenowego.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza skrót RAG w kontekście AI?
2. Które ograniczenie tradycyjnych LLM RAG pomaga rozwiązać?
Zadania praktyczne w grze
- Edytor kodu
Zaimplementuj podstawowy pipeline RAG
- Układanie w pionie
Uporządkuj kroki:
- Klikanie w kolejności
Ułóż kroki pipeline RAG:
- Układanie w pionie
Ułóż kroki pipeline RAG w prawidłowej kolejności: