Retrieval-Augmented Generation (RAG) Mimarisi: Sıfırdan Kurulum Rehberi
Büyük dil modelleri (LLM) inanılmaz yeteneklere sahip olsa da, kritik bir zayıflıkları var: halüsinasyon. Model, eğitim verisinde olmayan bir soruyla karşılaştığında, son derece ikna edici ama tamamen yanlış cevaplar üretebilir. İşte tam bu noktada Retrieval-Augmented Generation (RAG) devreye giriyor.
Bu yazıda, RAG mimarisinin ne olduğunu, neden bu kadar önemli olduğunu ve sıfırdan nasıl kurulacağını adım adım ele alacağız. Yazının sonunda kendi belgeleriniz üzerinde çalışan, akıllı bir soru-cevap sistemi inşa etmiş olacaksınız.
RAG Nedir ve Neden İhtiyacımız Var?
RAG, 2020 yılında Meta AI (eski adıyla Facebook AI Research) tarafından önerilen bir mimaridir. Temel fikir basit ama güçlüdür:
LLM'ye cevap üretmeden önce, ilgili bilgiyi bir bilgi kaynağından çekip (retrieve) bağlam olarak sun.
Geleneksel bir LLM çağrısında model sadece kendi parametrelerindeki bilgiyi kullanır. RAG mimarisinde ise süreç şöyle işler:
- Kullanıcı bir soru sorar
- Soru, vektör veritabanında aranır (Retrieval)
- En alakalı belgeler/parçalar bulunur
- Bu belgeler + soru birlikte LLM'ye gönderilir (Augmented Generation)
- LLM, sağlanan bağlamı kullanarak cevap üretir
RAG'ın Avantajları
- Güncel bilgi: Model eğitim tarihinden sonraki verilere erişebilir
- Doğruluk: Cevaplar gerçek belgelere dayanır, halüsinasyon azalır
- Kaynak gösterimi: Cevabın hangi belgeden geldiği gösterilebilir
- Maliyet etkinliği: Model fine-tuning yapmaya gerek kalmaz
- Gizlilik: Özel verileriniz model sağlayıcısına gönderilmez (yerel çalıştırıldığında)
Mimari Genel Bakış
Kodlamaya başlamadan önce, RAG pipeline'ının bileşenlerini anlayalım:
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Dokümanlar │────▶│ Chunking │────▶│ Embedding │
│ (PDF, TXT) │ │ (Parçalama) │ │ (Vektörleme) │
└─────────────┘ └──────────────┘ └────────┬────────┘
│
▼
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Cevap │◀────│ LLM │◀────│ Vektör DB │
│ │ │ (Üretim) │ │ (ChromaDB) │
└─────────────┘ └──────────────┘ └─────────────────┘
▲ ▲
│ │
┌──────┴───────┐ ┌───────┴────────┐
│ Prompt + │ │ Semantic │
│ Context │ │ Search │
└──────────────┘ └────────────────┘
▲
│
┌────────┴───────┐
│ Kullanıcı │
│ Sorusu │
└────────────────┘Adım 1: Ortamın Hazırlanması
Öncelikle gerekli bağımlılıkları yükleyelim. Bu projede Python, LangChain, ChromaDB ve OpenAI kullanacağız.
# Sanal ortam oluştur
python -m venv rag-env
source rag-env/bin/activate # Windows: rag-env\Scripts\activate
# Gerekli paketleri yükle
pip install langchain langchain-openai langchain-community
pip install chromadb
pip install pypdf
pip install python-dotenv
pip install tiktokenProje yapısını oluşturalım:
rag-project/
├── .env
├── data/
│ └── documents/ # PDF ve TXT dosyalarınız
├── src/
│ ├── __init__.py
│ ├── document_loader.py
│ ├── text_splitter.py
│ ├── embeddings.py
│ ├── vector_store.py
│ ├── rag_chain.py
│ └── main.py
└── requirements.txt.env dosyasına API anahtarınızı ekleyin:
OPENAI_API_KEY=sk-your-api-key-hereAdım 2: Doküman Yükleme (Document Loading)
RAG'ın ilk adımı, bilgi kaynaklarınızı sisteme yüklemektir. LangChain, farklı dosya formatları için hazır loader'lar sunar.
# src/document_loader.py
import os
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
DirectoryLoader,
)
class DocumentLoader:
"""Farklı formatlardaki dokümanları yükleyen sınıf."""
def __init__(self, data_dir: str = "data/documents"):
self.data_dir = data_dir
def load_pdf(self, file_path: str):
"""Tek bir PDF dosyasını yükler."""
loader = PyPDFLoader(file_path)
documents = loader.load()
print(f"✅ {file_path} yüklendi: {len(documents)} sayfa")
return documents
def load_text(self, file_path: str):
"""Tek bir TXT dosyasını yükler."""
loader = TextLoader(file_path, encoding="utf-8")
documents = loader.load()
print(f"✅ {file_path} yüklendi: {len(documents)} doküman")
return documents
def load_directory(self):
"""Bir dizindeki tüm PDF dosyalarını yükler."""
loader = DirectoryLoader(
self.data_dir,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
)
documents = loader.load()
print(f"✅ Toplam {len(documents)} doküman yüklendi")
return documents
def load_all(self):
"""Dizindeki tüm desteklenen dosyaları yükler."""
all_documents = []
for filename in os.listdir(self.data_dir):
file_path = os.path.join(self.data_dir, filename)
if filename.endswith(".pdf"):
all_documents.extend(self.load_pdf(file_path))
elif filename.endswith(".txt"):
all_documents.extend(self.load_text(file_path))
print(f"\n📚 Toplam yüklenen doküman sayısı: {len(all_documents)}")
return all_documentsAdım 3: Metin Parçalama (Text Splitting / Chunking)
Dokümanlar genellikle LLM'nin bağlam penceresi (context window) için çok uzundur. Bu nedenle, metinleri anlamlı parçalara (chunk) bölmemiz gerekir. Chunking stratejisi, RAG sisteminin performansını doğrudan etkileyen en kritik adımlardan biridir.
# src/text_splitter.py
from langchain.text_splitter import RecursiveCharacterTextSplitter
class TextSplitter:
"""Dokümanları anlamlı parçalara bölen sınıf."""
def __init__(
self,
chunk_size: int = 1000,
chunk_overlap: int = 200,
separators: list = None,
):
self.splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=separators or ["\n\n", "\n", ". ", " ", ""],
length_function=len,
is_separator_regex=False,
)
def split_documents(self, documents):
"""Dokümanları parçalara böler."""
chunks = self.splitter.split_documents(documents)
print(f"📄 {len(documents)} doküman → {len(chunks)} parçaya bölündü")
print(f"📏 Ortalama parça boyutu: "
f"{sum(len(c.page_content) for c in chunks) // len(chunks)} karakter")
return chunks
def preview_chunks(self, chunks, n: int = 3):
"""İlk n parçayı önizleme olarak gösterir."""
for i, chunk in enumerate(chunks[:n]):
print(f"\n--- Parça {i + 1} ---")
print(f"Boyut: {len(chunk.page_content)} karakter")
print(f"Metadata: {chunk.metadata}")
print(f"İçerik: {chunk.page_content[:200]}...")Chunk Size Seçimi Neden Önemli?
| Parametre | Küçük Chunk (200-500) | Büyük Chunk (1000-2000) |
|---|---|---|
| Kesinlik | Daha kesin eşleşme | Daha geniş bağlam |
| Bağlam | Bağlam kaybı riski | Daha zengin bağlam |
| Arama | Daha hızlı | Daha yavaş |
| Kullanım | Spesifik sorular | Genel sorular |
Genel kural: chunk_size=1000 ve chunk_overlap=200 çoğu senaryo için iyi bir başlangıç noktasıdır.
Adım 4: Embedding Oluşturma
Metin parçalarını vektörlere dönüştürmek, semantik aramanın temelini oluşturur. Her metin parçası, anlamını temsil eden yüksek boyutlu bir vektöre dönüştürülür.
# src/embeddings.py
from langchain_openai import OpenAIEmbeddings
from dotenv import load_dotenv
load_dotenv()
class EmbeddingManager:
"""Embedding model yönetim sınıfı."""
def __init__(self, model: str = "text-embedding-3-small"):
self.embeddings = OpenAIEmbeddings(
model=model,
# text-embedding-3-small: Hızlı, uygun maliyetli
# text-embedding-3-large: Daha yüksek doğruluk
)
self.model = model
print(f"🧠 Embedding modeli: {model}")
def embed_query(self, text: str) -> list[float]:
"""Tek bir metni vektöre dönüştürür."""
vector = self.embeddings.embed_query(text)
print(f"📐 Vektör boyutu: {len(vector)}")
return vector
def embed_documents(self, texts: list[str]) -> list[list[float]]:
"""Birden fazla metni vektöre dönüştürür."""
vectors = self.embeddings.embed_documents(texts)
print(f"📐 {len(vectors)} metin vektöre dönüştürüldü")
return vectors
def get_embeddings(self):
"""LangChain embedding nesnesini döndürür."""
return self.embeddingsYerel Alternatif: HuggingFace Embeddings
OpenAI API maliyetinden kaçınmak istiyorsanız, yerel embedding modelleri kullanabilirsiniz:
# Ücretsiz ve yerel alternatif
from langchain_community.embeddings import HuggingFaceEmbeddings
local_embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={"device": "cpu"}, # veya "cuda" GPU için
encode_kwargs={"normalize_embeddings": True},
)Adım 5: Vektör Veritabanı (Vector Store)
Vektörleri depolamak ve semantik arama yapmak için ChromaDB kullanacağız. ChromaDB, hafif, hızlı ve yerel çalışabilen bir vektör veritabanıdır.
# src/vector_store.py
import os
from langchain_community.vectorstores import Chroma
from src.embeddings import EmbeddingManager
class VectorStore:
"""ChromaDB tabanlı vektör veritabanı yönetim sınıfı."""
PERSIST_DIR = "data/chroma_db"
COLLECTION_NAME = "rag_documents"
def __init__(self, embedding_manager: EmbeddingManager = None):
self.embedding_manager = embedding_manager or EmbeddingManager()
self.embeddings = self.embedding_manager.get_embeddings()
self.vector_store = None
def create_from_documents(self, chunks):
"""Doküman parçalarından vektör veritabanı oluşturur."""
print(f"🔄 {len(