Strona główna / Artykuły

RAG od zera: dzielenie na fragmenty, wgrzanie, przechowywanie, odzyskiwanie, odpowiadanie z kontekstem

Krok po kroku przegląd pipeline RAG z użyciem LangChain i FAISS: dzielenie na fragmenty, embeddingi, wyszukiwanie, powiązanie z promptem, częste błędy oraz przyszłe ulepszenia.

Wydawca: REACTAPP.TOP8 min czytania1915 słów

Źródła: RAG, Explained the Way I Wish Someone Had Explained It to Me

Zadaj modelowi do rozmów pytanie o zasady zwrotu pieniędzy określonej firmy, a rzadko przyzna on, że nic o tym nie wie. Częściej generuje płynny, wiarygodny akapit, który nie ma nic wspólnego z rzeczywistymi zasadami. Przyczyną nie jest wada – model językowy jest trenowany raz, a następnie „zamrażany”. Nigdy nie czytał prywatnych plików PDF, wewnętrznych wiki ani przestrzeni roboczych Notion i nie wie nic o treściach napisanych po zakończeniu jego treningu.

Metoda generacji wzbogaconej pobieraniem danych, zwykle skracana do RAG, jest standardowym rozwiązaniem, a jej idea jest niemal żenująco prosta. Zamiast oczekiwać, że model zapamięta odpowiedź, system znajduje odpowiedni tekst i umieszcza go bezpośrednio w instrukcji, a następnie prosi model o jego przeczytanie i udzielenie odpowiedzi. Wszystko inne to elementy inżynierii służące do zapewnienia niezawodności tego wyszukiwania. Poniższy przykład to bot obsługi klienta, który odpowiada na pytania z pliku PDF zawierającego zasady firmy.

Dwóch studentów, jeden egzamin

Wyobraźmy sobie dwie osoby przystępujące do tego samego testu. Pierwsza zapamiętała podręcznik kilka miesięcy temu i odpowiada na podstawie pamięci. Jeśli podręcznik był przestarzały lub jakiś rozdział był częściowo zapomniany, odpowiedzi są pewne, ale błędne. Druga osoba ma do dyspozycji otwarty podręcznik. Zanim zacznie pisać, odwraca się do odpowiedniej strony, ją czyta i łączy jej treść ze swoim ogólnym rozumowaniem.

Zwykle wygrywa drugi uczeń, i to nie dzięki wyższej inteligencji. Sprawdzanie informacji przed odpowiedzią to po prostu lepszy sposób na udzielenie odpowiedzi. W systemie RAG model językowy odgrywa rolę drugiego ucznia, dane firmy stanowią otwarty podręcznik, a baza wektorowa jest indeksem wskazującym, do której strony się zwrócić.

Dwa procesy, nie jeden

Każdy system RAG, bez względu na stopień złożoności, składa się z dwóch oddzielnych procesów połączonych w środku.

  • Indeksowanie odbywa się offline, raz na start, a ponownie za każdym razem, gdy zmieniają się dokumenty. Pliki surowe przekształcają się w indeks nadający się do wyszukiwania.
  • Wyszukiwanie i generowanie odbywają się przy każdym pytaniu. Wszukuje się w indeksie, wybiera najlepsze fragmenty i przekazuje je modelowi.

Początkujący często próbują wyobrazić sobie jeden długi proces, co sprawia zamieszanie. Podział na dwa etapy pomaga, ponieważ każdy z nich przebiega w różnym momencie i rozwiązuje inne problemy.

Budowanie indeksu

Ładowanie dokumentów

Najpierw należy przenieść treść surową do zwykłego tekstu. PDF-y, pliki Word, strony internetowe oraz eksportowane notatki wymagają narzędzia do ładowania, które zwraca tekst.

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("company_policy.pdf")
documents = loader.load()

print(f"Loaded {len(documents)} pages")

Dzielenie tekstu na fragmenty

Dzielenie tekstu na fragmenty zasługuje na większą uwagę, niż zwykle otrzymuje. Bardzo powszechnym pierwszym błędem jest umieszczanie całego dokumentu w bazie wektorowej jako jednej ogromnej jednostki. Model embeddingu przekształca fragment tekstu w wektor o stałej wielkości. Jeśli podasz mu dwadzieścia stron, rezultatem będzie rozmyta średnia wszystkich treści, która może wskazać, że plik dotyczy ogólnie polityki firmy, ale nie będzie w stanie oddać konkretnego akapitu o oknie zwrotu pieniędzy trwającym trzydzieści dni.

Rozwiązaniem jest dzielenie dokumentów na małe, nakładające się na siebie fragmenty. Nakładanie się jest ważne, ponieważ zdanie przecięte dokładnie na granicy traci kontekst po obu stronach, a margines wynoszący około dziesięć do dwudziestu procent wielkości fragmentu służy jako zabezpieczenie.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # characters per chunk
    chunk_overlap=80,    # overlap between consecutive chunks
    separators=["\n\n", "\n", ". ", " "]
)

chunks = splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")

Jako wskazówkę: zbyt duże fragmenty tekstowe sprawiają, że ich wydobycie jest niejasne, natomiast zbyt małe pozbawiają kontekstu. Rozsądnym początkowym zakresem jest około 300 do 800 znaków, który można następnie dostosować po przejrzeniu rzeczywistych odpowiedzi.

Wbuduj każdy fragment

Model embeddingu przekształca tekst w listę liczb, która odzwierciedla jego znaczenie, a nie tylko sformułowanie. Dwie zdania mówiące to samo kończą się wektorami znajdującymi się blisko siebie, nawet jeśli w ogóle nie mają wspólnych słów.

from langchain_openai import OpenAIEmbeddings

embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# under the hood, this is what happens per chunk:
vector = embedding_model.embed_query("30-day refund window for annual plans")
print(len(vector))   # e.g. 1536 numbers representing this sentence's meaning

Jednym ze sposobów na to wyobrażenie jest płaska mapa, na której powiązane pomysły gromadzą się w klastrach.

        "refund policy" •
                          \
                           • "money-back guarantee"
   "vacation days" •
                     \
                      • "paid time off"

Wyrażenie takie jak „polityka zwrotów” pojawia się obok „gwarancji zwrotu pieniędzy”, mimo że słowa są różne, i to właśnie jest sedno wyszukiwania semantycznego. To wyjaśnia, dlaczego RAG przewyższa stary styl dopasowywania słów kluczowych oparty na Ctrl+F.

Zachowuj wektory

Każdy wektor trafia do bazy danych wektorowej wraz z oryginalnym tekstem. Ta baza stanowi indeks, który przegląda student korzystający z otwartej książki.

from langchain_community.vectorstores import FAISS

vectorstore = FAISS.from_documents(chunks, embedding_model)
vectorstore.save_local("faiss_index")

FAISS to wygodny punkt wyjścia, ponieważ jest darmowy i działa na lokalnym komputerze, a Chroma jest równie prosta w użyciu. Usługi zarządzane takie jak Pinecone lub Weaviate okazują się przydatne, gdy dochodzi do skalowania w warunkach produkcyjnych. Koncepcja pozostaje ta sama we wszystkich przypadkach, różnią się jedynie mechanizmy przechowywania i infrastruktura.

Indeksowanie zostało już zakończone. Rezultatem jest pamięć dostępna do wyszukiwania, utworzona na podstawie dokumentów, która musi być odbudowywana tylko wtedy, gdy te dokumenty ulegną zmianie, a nie przy każdym pytaniu użytkownika.

Odpowiadanie na pytanie

Ta część jest wykonywana za każdym razem, gdy ktoś wpisuje zapytanie.

Wbudowanie pytania za pomocą tego samego modelu

query = "Can I get a refund on my annual subscription?"

Pytanie musi być podawane przy użyciu dokładnie tego modelu embeddingu, który został zastosowany podczas tworzenia indeksu. Ludzie ciągle na to wpadają. Jeśli indeks został stworzony za pomocą jednego modelu embeddingu, a zapytanie jest wysyłane z innego, wektory znajdują się w różnych przestrzeniach semantycznych, przez co wyszukiwanie podobieństw przestaje działać. Nie pojawia się żaden błąd, tylko słabe wyniki, dlatego zasada brzmi: jeden model przez cały proces.

Pobierz najbliższe fragmenty

retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
relevant_chunks = retriever.invoke(query)

for chunk in relevant_chunks:
    print(chunk.page_content[:100], "...")

Wewnętrznie FAISS przeprowadza wyszukiwanie podobieństwa. Mierzy odległość, zwykle podobieństwo kosinowe, pomiędzy wektorem pytania a wektorem każdego przechowywanego fragmentu, a następnie zwraca te najbliższe. To ponownie koncepcja klastrów na mapie, zastosowana w odwrotnej kolejności, aby znaleźć klaster, do którego należy pytanie.

Wklej fragmenty do promptu

To właśnie ta operacja sprawia, że RAG funkcjonuje: pobrany tekst jest dosłownie wstawiany do promptu.

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("""
Answer the question using ONLY the context below.
If the answer isn't in the context, say "I don't have that information."

Context:
{context}

Question: {question}
""")

Instrukcja polegająca na odpowiadaniu wyłącznie na podstawie dostarczonego kontekstu sprawia, że model pozostaje przyziemny i nie wymyśla rzeczy. Dzieli to egzamin z otwartą książką od egzaminu z zamkniętą książką, w którym dozwolona jest improwizacja.

Połącz je w łańcuch

Teraz elementy są łączone w łańcuch RAG, o którym mówi się, gdy chodzi o budowę takiego systemu. W LangChain zazwyczaj jest to reprezentowane za pomocą operatora rury, który przekazuje wynik z lewej strony do kolejnego kroku po prawej stronie.

from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini")

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

answer = rag_chain.invoke("Can I get a refund on my annual subscription?")
print(answer)

Czytając od lewej do prawej, łańcuch sam się opowiada. Pytanie jest wprowadzane, pobierane są fragmenty, które są formatowane jako kontekst, podczas gdy pytanie przechodzi bez zmian, wypełniana jest szablonowa instrukcja, wywoływany jest model, a z jego odpowiedzi parsowany jest tekst prosty.

Gdy to uruchomimy, zamiast fałszywej domysłu pojawia się coś w rodzaju: tak, plany roczne podlegają pełnemu zwrotowi pieniędzy w ciągu trzydziestu dni od zakupu, zgodnie z polityką zwrotów. Odpowiedź jest uzasadniona i konkretna, a jej źródło można odnaleźć w rzeczywistym akapicie w pliku PDF.

Dlaczego nie wkleić wszystkiego?

Sprawiedliwym zarzutem jest to, że okna kontekstowe są obecnie ogromne. Dlaczego nie umieścić całego dokumentu w każdej instrukcji i pominąć wyszukiwanie wektorowe?

  • Koszt i opóźnienie. Każdy wysłany token kosztuje pieniądze i zajmuje czas. W przypadku bazy wiedzy liczącej 500 stron wysyłanie wszystkiego przy każdym pytaniu jest marnotrawstwem, gdy odpowiedź mieści się w jednym akapicie.
  • Obniżona dokładność odzyskiwania informacji. Nawet przy dużych oknach modeli informacje umieszczone w środku zapytania są wykorzystywane znacznie gorzej niż te przekazywane bezpośrednio i krótko.
  • Skalowalność. Wklejanie wszystkiego zawodzi całkowicie, gdy baza wiedzy przewyższa rozmiar danego okna – np. przy tysiącach plików PDF, pełnej witrynie firmowej lub latach zgłoszeń klientów.

RAG umożliwia systemowi rozwój od jednego dokumentu do całej bazy wiedzy, przy tym rozmiar zapytania pozostaje niezmieniony. Tylko krok odzyskiwania informacji dotyka całego zbioru danych, a zapytanie otrzymuje zaledwie kilka istotnych fragmentów.

Błędy, które pojawiają się wcześnie

  • Słabe dzielenie tekstu na fragmenty psuje proces wydobywania informacji, niezależnie od jakości modelu. Jeśli tabela zostanie przecięta na pół lub zdanie zostanie przerwane w trakcie myśli, nic późniejsze nie może tego naprawić. Sprawdź jakość wydobywania informacji, zanim obwiniasz model.
  • Niepasujące modele embeddingów sprawiają, że wyniki pomiarów podobieństwa tracą sens. Indeks utworzony za pomocą text-embedding-3-small nie może być wyszukiwany za pomocą innego modelu.
  • Zbyt mała lub zbyt duża liczba fragmentów również szkodzi. Przy k=1 może zostać przeoczona prawdziwa odpowiedź, a przy k=20 model tonie w szumie i wzrośnie koszt. Zacznij od trzech do pięciu fragmentów i dostosuj wartość na podstawie uzyskanych wyników.
  • Bez planu awaryjnego na wypadek braku informacji model i tak będzie wymyślał odpowiedź, nawet gdy istotny kontekst znajduje się przed nim. Określ dokładnie, co robić, gdy kontekst nie zawiera odpowiedzi.

Kolejne kroki

Gdy podstawowy mechanizm funkcjonuje poprawnie, trzy ulepszenia stanowią naturalne następne kroki.

  • Poszukiwanie hybrydowe łączy podobieństwo wektorowe z dopasowywaniem słów kluczowych, ponieważ niektóre zapytania, takie jak dokładne kody produktów lub nazwy, lepiej radzą sobie dzięki słowom kluczowym.
  • Przegrupowanie wyników najpierw pobiera szerszy zbiór kandydatów, a następnie pozwala drugiemu, bardziej precyzyjnemu modelowi na ich ponowne uporządkowanie, zanim model językowy zobaczy jakiekolwiek z nich.
  • Filtrowanie metadanych przypisuje do każdego fragmentu informacje takie jak data, źródło lub dział, dzięki czemu możliwe jest zawężenie zakresu wyników jeszcze przed uruchomieniem poszukiwania na podstawie podobieństwa.

Żadne z tych elementów nie ma znaczenia, dopóki podstawowy cykl składający się z przetwarzania fragmentów, ich embedowania, przechowywania, wyszukiwania i generowania odpowiedzi nie działa poprawnie. Naj szybszym sposobem na rzeczywiste zrozumienie tego procesu jest załadowanie ważnego pliku PDF, celowe uszkodzenie jego struktury, na przykład poprzez nieodpowiednie modele embedowania lub fragmenty bez nakładania się, i obserwacja pogorszenia jakości uzyskiwanych odpowiedzi. Dzięki technologii RAG sposoby awarii uczą więcej niż tylko scenariusz prawidłowego funkcjonowania.

Literatura pokrewna

  • Notatki praktyczne: Trzy poziomy Google Cloud RAG: wyszukiwanie wektorowe, silnik RAG oraz odzyskiwanie informacji przez agenta — Szczegółowy przewodnik po notatkach praktycznych: Trzy poziomy Google Cloud RAG: wyszukiwanie wektorowe, silnik RAG oraz odzyskiwanie informacji przez agenta: umowy, sprawdzenia oraz miejsca na kod do wstawienia.
  • RAG z pamięcią stanową w Databricks: pamięć wieloetapowa z Lakebase i LangGraph — Nadaj agentowi RAG w Databricks prawdziwą pamięć rozmowy: analizuj pliki PDF, twórz system wyszukiwania wektorowego, przechowuj rozmowy w Lakebase Postgres, a następnie wdroż go za pomocą MLflow i Model Serving.
  • Jak zostać inżynierem AI w 2026 roku bez dyplomu z informatyki — Plan od zera do zatrudnienia, aby zostać inżynierem AI w 2026 roku bez wykształcenia z informatyki: siedem obszarów umiejętności, małe projekty, doświadczenie w praktyce oraz mądre nawiązywanie kontaktów.