Duże modele językowe (LLM), takie jak GPT-4 i Claude, są niezwykle potężne, ale cierpią na fundamentalne ograniczenie: ich wiedza jest zamrożona w momencie treningu. Nie mogą uzyskać dostępu do Twoich wewnętrznych dokumentów, bazy danych ani informacji w czasie rzeczywistym. Generowanie wspomagane wyszukiwaniem (RAG) rozwiązuje dokładnie ten problem, łącząc generatywną moc LLM ze zdolnością do pobierania informacji ze źródeł zewnętrznych.
Problem: ograniczenia LLM
Zanim porozmawiamy o RAG, ważne jest zrozumienie, dlaczego go potrzebujemy.
- Statyczna wiedza: LLM zna tylko to, co widział podczas treningu. Jeśli zapytasz o wydarzenie, które miało miejsce po jego odcięciu, nie będzie w stanie odpowiedzieć.
- Halucynacje: Gdy LLM nie zna odpowiedzi, ma tendencję do jej wymyślania, generując wiarygodne, ale całkowicie fałszywe informacje.
- Brak dostępu do prywatnych danych: Ogólny LLM nie ma dostępu do wewnętrznej dokumentacji Twojej firmy, zgłoszeń ani bazy kodu.
RAG rozwiązuje wszystkie trzy te problemy, dostarczając modelowi odpowiedni kontekst pobrany ze źródeł zewnętrznych w momencie zapytania.
Czym jest RAG?
Generowanie wspomagane wyszukiwaniem to architektura, która wzbogaca prompt wysyłany do LLM informacjami pobranymi z zewnętrznej bazy wiedzy. Zamiast polegać wyłącznie na parametrycznej wiedzy modelu, RAG najpierw wyszukuje odpowiednie informacje, a następnie wstrzykuje je do promptu, umożliwiając modelowi generowanie dokładnych, ugruntowanych odpowiedzi.
Jak RAG działa w szczegółach
Architektura RAG składa się z dwóch głównych faz: Indeksowanie (offline) i Wyszukiwanie + Generowanie (online).
Faza 1: Indeksowanie (przetwarzanie dokumentów)
Faza indeksowania przygotowuje Twoje dokumenty do wyszukiwania semantycznego. Składa się z czterech kroków.
1. Ładowanie dokumentów
Dokumenty mogą pochodzić z dowolnego źródła: pliki PDF, strony internetowe, bazy danych, pliki Markdown, API. Document Loader odczytuje te dokumenty i konwertuje je na ustrukturyzowany tekst.
2. Dzielenie tekstu (Chunking)
LLM mają ograniczone okno kontekstowe, a dokumenty mogą być bardzo długie. Text Splitter dzieli dokumenty na mniejsze fragmenty zwane chunks. Jakość chunkingu jest kluczowa: zbyt małe chunks tracą kontekst, podczas gdy zbyt duże rozmywają trafność.
Najczęstsze strategie to:
- Rekurencyjne dzielenie znaków: Rekurencyjnie dzieli tekst za pomocą separatorów takich jak
\n\n,\n,., respektując strukturę dokumentu. - Dzielenie semantyczne: Wykorzystuje embeddings do znajdowania naturalnych punktów podziału w tekście.
- Nakładanie się chunks: Zawiera nakładanie się między kolejnymi chunks, aby zachować kontekst na granicach.
3. Embedding
Każdy chunk jest przekształcany w wektor numeryczny (embedding) za pomocą modelu embedding (takiego jak text-embedding-3-small od OpenAI). Te wektory oddają semantyczne znaczenie tekstu: zdania o podobnym znaczeniu będą miały bliskie wektory w przestrzeni wielowymiarowej.
4. Vector Store
Wektory są zapisywane w Vector Store (lub bazie danych wektorowej), takiej jak ChromaDB, Pinecone, Weaviate lub FAISS. Ta baza danych jest zoptymalizowana pod kątem wyszukiwania podobieństwa: dla danego zapytania znajduje najbardziej podobne wektory (a tym samym najbardziej trafne fragmenty tekstu).
Faza 2: Wyszukiwanie + Generowanie
Gdy użytkownik zadaje pytanie:
- Pytanie jest przekształcane w embedding za pomocą tego samego modelu embedding.
- Vector Store znajduje najbardziej podobne chunks poprzez wyszukiwanie podobieństwa (zwykle cosine similarity lub odległość euklidesowa).
- Pobrane chunks są wstawiane do promptu jako kontekst.
- LLM generuje odpowiedź na podstawie dostarczonego kontekstu.
Budowanie pipeline RAG z LangChain
LangChain to najpopularniejszy framework Python (i JavaScript) do budowania aplikacji opartych na LLM. Zapewnia abstrakcje wysokiego poziomu dla każdego komponentu pipeline RAG.
Instalacja
pip install langchain langchain-openai langchain-community chromadb
Krok 1: Załaduj dokumenty
LangChain udostępnia dziesiątki Document Loaderów dla różnych źródeł danych.
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # Load a PDF pdf_loader = PyPDFLoader("docs/manual.pdf") pdf_docs = pdf_loader.load() # Load a web page web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # Load all .md files from a directory dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
Krok 2: Podziel dokumenty na chunks
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"Original documents: {len(all_docs)}, Chunks: {len(chunks)}")
Parametr chunk_overlap jest kluczowy: tworzy nakładanie się między kolejnymi chunks, aby kontekst nie był tracony na granicach.
Krok 3: Utwórz Embeddings i Vector Store
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
Krok 4: Utwórz Retriever
Retriever to komponent, który dla danego zapytania pobiera najbardziej trafne chunks z vector store.
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("How does authentication work?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
Krok 5: Zbuduj łańcuch RAG
Teraz połączmy wszystko razem z LLM i szablonem promptu.
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" Answer the question based only on the provided context. If the context does not contain enough information, say you don't know. Context: {context} Question: {question} Answer: """) 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() ) response = rag_chain.invoke("How does authentication work in the system?") print(response)
Zaawansowane techniki RAG
Podstawowy pipeline działa dobrze, ale istnieje kilka technik znacząco poprawiających jakość odpowiedzi.
Wyszukiwanie wielozapytaniowe
Czasami zapytanie użytkownika jest niejednoznaczne lub nie jest dopasowane do języka użytego w dokumentach. Multi-Query Retriever automatycznie generuje warianty oryginalnego pytania, aby uchwycić różne perspektywy.
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("What are the security best practices?")
Kompresja kontekstowa
Nie cała zawartość chunka jest trafna dla zapytania. Contextual Compression Retriever wykorzystuje LLM do wyodrębnienia tylko istotnych części z każdego pobranego chunka.
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
Wyszukiwanie hybrydowe
Czysto semantyczne wyszukiwanie nie zawsze jest optymalne. Wyszukiwanie hybrydowe łączy wyszukiwanie semantyczne (embeddings) z wyszukiwaniem leksykalnym (BM25, dopasowanie słów kluczowych) w celu osiągnięcia lepszych wyników.
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
Konwersacyjny RAG (z pamięcią)
Aby zbudować chatbot RAG zapamiętujący kontekst rozmowy, należy dodać pamięć, która przeformułowuje pytania użytkownika z uwzględnieniem historii konwersacji.
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "Given the chat history and the user's latest question, " "reformulate the question so it is understandable without the history."), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
Najlepsze praktyki
- Wybierz odpowiedni rozmiar chunka: Eksperymentuj z różnymi rozmiarami (500-1500 tokenów). Mniejsze chunks dla precyzyjnych odpowiedzi, większe dla szerszego kontekstu.
- Używaj metadanych dokumentów: Dodawaj źródło, datę i kategorię jako metadane do chunks. Umożliwia to filtrowanie wyników podczas wyszukiwania.
- Oceniaj jakość: Używaj frameworków takich jak RAGAS do mierzenia metryk takich jak faithfulness, relevancy i context precision.
- Zarządzaj aktualizacjami dokumentów: Zaimplementuj pipeline ponownego przetwarzania, aby utrzymać synchronizację vector store ze źródłami danych.
- Dodaj re-ranker: Po początkowym wyszukiwaniu użyj modelu re-rankingu (takiego jak Cohere Rerank), aby uporządkować wyniki na podstawie rzeczywistej trafności.
Podsumowanie
RAG stał się standardową architekturą do budowania aplikacji AI wymagających dostępu do konkretnej, aktualnej wiedzy. LangChain znacznie upraszcza implementację, dostarczając abstrakcje dla każdego komponentu pipeline.
Kolejne kroki:
- Eksperymentuj lokalnie: Zacznij od ChromaDB i kilku dokumentów, aby zapoznać się z pipeline.
- Odkryj LangSmith: Użyj LangSmith do monitorowania i debugowania łańcuchów w produkcji.
- Wypróbuj różne modele embedding: Porównaj modele takie jak
text-embedding-3-small,text-embedding-3-largei modele open-source z Sentence Transformers. - Sprawdź dokumentację: Dokumentacja LangChain to doskonałe i stale aktualizowane źródło.