Tüm rehberler
RehberYapay Zeka05 · Uygulama

RAG Sistemleri
Bilgini Modele Bağla.

Metin chunking'den embedding'e, vektör aramasından generation'a tam RAG pipeline. Hybrid search, reranking ve retrieval kalitesi ölçümü ile üretim hazır sistem.

00 RAG Nedir?

Bilgi kesme tarihi ve hallüsinasyon sorunlarına karşı dış bilgi kaynağı ile modeli buluşturan mimari.

Büyük dil modelleri eğitim verisi ile belirli bir tarihe kadar dünya bilgisine sahiptir — bu bilgi kesme tarihi (knowledge cutoff) sorunudur. GPT-4'ün Nisan 2023'te yaşanan bir olayı bilmemesi ya da şirketinizin iç dokümantasyonunu hiç görmemiş olması buna örnek gösterilebilir.

İkinci ve daha tehlikeli sorun ise hallüsinasyondur: model bilmediği şeyleri uydurmaz, icat eder ve bunu güvenle sunar. Kaynaksız, yanlış ama akıcı yanıtlar üretir.

RAG vs Fine-Tuning

KriterRAGFine-Tuning
Bilgi güncellemeVektör DB'yi güncelle → anındaModeli yeniden eğit → pahalı/yavaş
Kaynak gösterimiDoğal (hangi belgeden geldiği belli)Zor (model bilgiyi içselleştirir)
MaliyetDüşük başlangıç maliyetiYüksek GPU maliyeti
Uzun belgeÇalışır (chunk + retrieve)Context limit sorunu
Davranış değişimiSınırlıDerin (ton, format, görev)
Ne zaman seçmeliBilgi tabanı, Q&A, sık güncellenen içerikDomain dili, format tutarlılığı

RAG Pipeline'ın İki Bileşeni

RAG iki ana bileşenden oluşur: Retriever (kullanıcının sorusuna en ilgili belge parçalarını bulan sistem) ve Generator (bulunan parçaları bağlam olarak alıp yanıt üreten LLM).

01 Belge yükle (PDF, Word, web, kod...)
02 Chunk'lara böl (500 token, 50 token overlap)
03 Her chunk'ı embedding modeli ile vektöre çevir
04 Vektörleri vektör DB'ye kaydet
── — İndeksleme bitti, sorgu zamanı —
05 Kullanıcı sorusunu embedding'e çevir
06 Vektör DB'de benzerlik araması yap → top-k chunk
07 Soru + chunk'lar → LLM prompt'una ekle
08 LLM yanıt üret (kaynaklı, gerçeğe dayalı)
NOT

RAG sisteminizin kalitesi büyük ölçüde retrieval kalitesine bağlıdır. Yanlış chunk'lar getirilirse, en iyi LLM bile doğru yanıt üretemez. Bu yüzden bu rehberde retrieval optimizasyonuna özel ağırlık verilmiştir.

01 Metin Bölümleme (Chunking)

Belgelerinizi embedding için uygun boyutlara bölme stratejileri ve bunların retrieval kalitesine etkisi.

Embedding modelleri belirli bir maksimum token sayısını işleyebilir — örneğin all-MiniLM-L6-v2 için bu sınır 256 token, text-embedding-3-small için 8191 token'dır. Uzun belgeler için chunking kaçınılmazdır. Ama daha önemli bir sebep var: çok uzun bir metin gömüldüğünde, semantik odak dağılır ve retrieval hassasiyeti düşer.

Fixed-Size Chunking

fixed_chunking.py
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,       # her chunk'ta ~500 karakter
    chunk_overlap=50,    # komşu chunk'lar arasında 50 karakter örtüşme
    length_function=len,
    separators=["\n\n", "\n", ". ", " ", ""],
    # Bu sırayla önce paragraf sonu, sonra satır sonu, sonra cümle sonu...
)

text = """Transformer mimarisi 2017 yılında Google araştırmacıları tarafından
"Attention Is All You Need" makalesiyle tanıtıldı. Bu mimari, RNN ve
LSTM gibi tekrarlayan yapıların yerini almış ve modern NLP'nin temelini
oluşturmuştur. Self-attention mekanizması, dizideki tüm öğelerin birbirleriyle
doğrudan ilişki kurmasını sağlar..."""

chunks = splitter.split_text(text)
for i, chunk in enumerate(chunks):
    print(f"Chunk {i+1} ({len(chunk)} karakter): {chunk[:80]}...")

Document Chunking (Metadata ile)

doc_chunking.py
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# PDF yükle
loader = PyPDFLoader("./belge.pdf")
documents = loader.load()
print(f"Toplam sayfa: {len(documents)}")

# Chunk'lara böl
splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=100,
)
chunks = splitter.split_documents(documents)

# Her chunk'ın metadatasına bakın
for chunk in chunks[:3]:
    print(f"Sayfa: {chunk.metadata['page']}")
    print(f"İçerik: {chunk.page_content[:100]}...")
    print("---")

print(f"Toplam chunk sayısı: {len(chunks)}")

Semantic Chunking

Sabit boyutlu bölme yerine, anlamsal birliği hedefleyen semantic chunking arka arkaya gelen cümlelerin embedding benzerliğini hesaplar ve benzerlik düştüğünde yeni chunk başlatır. Bu yöntem daha doğal bölme noktaları üretir ancak hesaplama maliyeti daha yüksektir.

semantic_chunking.py
# pip install langchain-experimental
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()
chunker = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",  # veya "standard_deviation"
    breakpoint_threshold_amount=95,           # 95. yüzdelik dilim üzerinde böl
)

chunks = chunker.split_text(long_text)
print(f"Semantic chunk sayısı: {len(chunks)}")
YöntemAvantajDezavantajNe zaman kullan
Fixed-sizeHızlı, basitCümle ortasında kesilebilirGenel amaç, prototip
Recursive characterDoğal sınırları tercih ederChunk boyutu değişkenÇoğu üretim senaryosu
Sentence-awareCümle bütünlüğü korunurKısa cümleler çok küçük chunkHaber, makale
SemanticEn doğal bölmeYavaş (embedding gerektirir)Uzun teknik belgeler

02 Embedding Modelleri

Metni anlamsal anlam taşıyan yoğun vektörlere dönüştüren embedding modelleri ve Türkçe için öneriler.

Embedding modelleri, bir metin parçasını sabit boyutlu bir vektöre dönüştürür. Anlamca benzer metinler uzayda birbirine yakın vektörler üretir. Bu özellik, bir soruyu vektöre çevirip veritabanındaki en yakın belge vektörlerini bulmanın temelini oluşturur.

Sentence Transformers

embeddings_basic.py
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("all-MiniLM-L6-v2")

# Tek cümle embedding
vec = model.encode("Transformer attention mekanizması kullanır.")
print(f"Vektör boyutu: {vec.shape}")   # (384,)

# Toplu embedding
sentences = [
    "Python öğrenmek istiyorum.",
    "Yılan balığı nasıl pişirilir?",
    "Makine öğrenmesi Python ile uygulanabilir.",
]
embeddings = model.encode(sentences, batch_size=32, show_progress_bar=True)

# Kosinüs benzerliği hesapla
from sklearn.metrics.pairwise import cosine_similarity

sims = cosine_similarity(embeddings)
print("\nBenzerlik matrisi:")
for i, s1 in enumerate(sentences):
    for j, s2 in enumerate(sentences):
        if i < j:
            print(f"  [{i}] vs [{j}]: {sims[i][j]:.3f}")
# [0] vs [1]: 0.021  → alakasız
# [0] vs [2]: 0.621  → ilgili (Python)

Popüler Modeller

ModelBoyutVektör BoyutuMax TokenNotlar
all-MiniLM-L6-v280 MB384256Hızlı, İngilizce odaklı
all-mpnet-base-v2420 MB768384İngilizce için güçlü baseline
BAAI/bge-m3570 MB10248192Çok dilli, uzun bağlam, önerilir
intfloat/multilingual-e5-large560 MB1024512Türkçe dahil çok dilli
text-embedding-3-small (OpenAI)15368191API, ücretli, yüksek kalite
text-embedding-3-large (OpenAI)30728191API, ücretli, en yüksek kalite
NOT

Türkçe belgeler için BAAI/bge-m3 veya intfloat/multilingual-e5-large tercih edilmelidir. Bu modeller çok dilli eğitim gördüğünden Türkçe semantik benzerliği daha iyi yakalar. İngilizce odaklı modeller Türkçe için zayıf kalabilir.

Embedding'leri Normalize Etmek

normalize_embeddings.py
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("BAAI/bge-m3")

# normalize_embeddings=True → kosinüs benzerliği için dot product yeterli
embeddings = model.encode(
    ["Makine öğrenmesi nedir?", "Derin öğrenme nasıl çalışır?"],
    normalize_embeddings=True,
)

# Normalize edilmişse dot product = cosine similarity
dot_sim = np.dot(embeddings[0], embeddings[1])
print(f"Benzerlik: {dot_sim:.4f}")

03 Vektör Veritabanları

Embedding vektörlerini saklayan ve benzerlik araması yapan vektör veritabanları — FAISS'ten managed servislere.

Vektör veritabanları, yüksek boyutlu vektörleri indexler ve milyonlarca vektör arasında milisaniyeler içinde en yakın komşuları bulur. Geleneksel veritabanlarının koşul tabanlı sorgularının aksine, bu sistemler anlamsal yakınlık sorgularına optimize edilmiştir.

FAISS — Lokal ve Hızlı

faiss_basic.py
# pip install faiss-cpu  (GPU için: faiss-gpu)
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-m3")
dim = 1024    # bge-m3 vektör boyutu

# Belge corpus'u
documents = [
    "Transformer mimarisi 2017'de tanıtıldı.",
    "BERT çift yönlü encoder modelidir.",
    "GPT otoregressif dil modelidir.",
    "LLaMA Meta tarafından açık kaynak olarak yayınlandı.",
    "Attention mekanizması tüm pozisyonlar arasında ilişki kurar.",
]

# Embedding oluştur
embeddings = model.encode(documents, normalize_embeddings=True)
embeddings = embeddings.astype(np.float32)

# FAISS index oluştur (L2 mesafesi)
index = faiss.IndexFlatL2(dim)
index.add(embeddings)
print(f"Index'teki vektör sayısı: {index.ntotal}")

# Sorgula
query = "BERT nasıl çalışır?"
query_vec = model.encode([query], normalize_embeddings=True).astype(np.float32)

k = 3   # en yakın 3 sonuç
distances, indices = index.search(query_vec, k)

print(f"\nSorgu: '{query}'")
for rank, (dist, idx) in enumerate(zip(distances[0], indices[0])):
    print(f"  {rank+1}. (mesafe={dist:.4f}): {documents[idx]}")

Chroma — Persist Desteği ile

chroma_basic.py
# pip install chromadb sentence-transformers
import chromadb
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction

# Disk'e kalıcı kayıt
client = chromadb.PersistentClient(path="./chroma_db")

embed_fn = SentenceTransformerEmbeddingFunction(
    model_name="BAAI/bge-m3"
)

collection = client.get_or_create_collection(
    name="belgelerim",
    embedding_function=embed_fn,
    metadata={"hnsw:space": "cosine"},
)

# Belge ekle
collection.add(
    documents=[
        "Transformer attention mekanizması kullanır.",
        "BERT çift yönlü encoder'dır.",
        "GPT decoder-only mimaridir.",
    ],
    ids=["doc1", "doc2", "doc3"],
    metadatas=[
        {"kaynak": "transformer_makalesi.pdf", "sayfa": 1},
        {"kaynak": "bert_makalesi.pdf", "sayfa": 3},
        {"kaynak": "gpt_makalesi.pdf", "sayfa": 2},
    ],
)

# Sorgula
results = collection.query(
    query_texts=["encoder nasıl çalışır?"],
    n_results=2,
)
for doc, meta in zip(results["documents"][0], results["metadatas"][0]):
    print(f"Kaynak: {meta['kaynak']} | İçerik: {doc}")

Vektör DB Karşılaştırması

VeritabanıTürÖlçekMetadata FiltreÖne Çıkan Özellik
FAISSKütüphaneMilyonlarHayırHızlı, Facebook, çok indeks tipi
ChromaGömülü/SunucuMilyonlarEvetKolay API, persist, LangChain desteği
pgvectorPostgreSQL eklentisiYüzler binlerEvet (SQL)Mevcut Postgres altyapısına entegre
WeaviateManaged/Self-hostedMilyarlarEvetHybrid search built-in
PineconeManaged (cloud)MilyarlarEvetYönetimden muaf, yüksek erişilebilirlik
QdrantSelf-hosted/CloudMilyarlarEvetRust tabanlı, yüksek performans

04 Similarity Search

L2, cosine similarity, FAISS index tipleri ve approximate nearest neighbor araması.

Vektör araması, sorgu vektörüne en yakın k vektörü bulmakla başlar. "Yakınlık" birkaç farklı metrikle ölçülebilir:

L2 (Euclidean)İki vektör arasındaki Öklid mesafesi. Vektörler normalize edilmemişse tercih edilebilir. FAISS IndexFlatL2.
Cosine SimilarityVektörlerin açı kosinüsü. Büyüklük değil yön önemli. Normalize vektörlerde dot product'a eşdeğer.
Dot Product (IP)İç çarpım. Normalize edilmiş vektörlerde cosine similarity ile aynı. FAISS IndexFlatIP.

FAISS Index Tipleri

faiss_index_types.py
import faiss
import numpy as np

dim = 1024
n = 100_000  # 100k vektör
data = np.random.randn(n, dim).astype(np.float32)
query = np.random.randn(1, dim).astype(np.float32)

# 1. Exact search — küçük corpus için
index_flat = faiss.IndexFlatL2(dim)
index_flat.add(data)

# 2. IVF — büyük corpus için (yaklaşık)
#    nlist: Voronoi hücre sayısı (~sqrt(n) tavsiye)
nlist = 100
quantizer = faiss.IndexFlatL2(dim)
index_ivf = faiss.IndexIVFFlat(quantizer, dim, nlist)
index_ivf.train(data)   # IVF eğitim gerektirir
index_ivf.add(data)
index_ivf.nprobe = 10   # kaç hücreye bak (daha yüksek = daha doğru, yavaş)

# 3. HNSW — çok hızlı, iyi recall (Hierarchical NSW)
index_hnsw = faiss.IndexHNSWFlat(dim, 32)  # 32: komşu sayısı
index_hnsw.add(data)

# Arama hızı karşılaştırması
import time

for name, idx in [("Flat", index_flat), ("IVF", index_ivf), ("HNSW", index_hnsw)]:
    start = time.time()
    D, I = idx.search(query, 10)
    elapsed = (time.time() - start) * 1000
    print(f"{name:6s}: {elapsed:.2f}ms — top-1 idx={I[0][0]}")

FAISS ile Cosine Similarity

faiss_cosine.py
import faiss
import numpy as np

# Cosine similarity için vektörleri normalize et, sonra L2 kullan
# (normalize vektörlerde L2 = √(2 - 2·cosine))
def build_cosine_index(embeddings: np.ndarray) -> faiss.Index:
    emb = embeddings.copy().astype(np.float32)
    faiss.normalize_L2(emb)
    index = faiss.IndexFlatIP(emb.shape[1])   # Inner Product = cosine when normalized
    index.add(emb)
    return index

def cosine_search(index, query_vec: np.ndarray, k: int = 5):
    q = query_vec.copy().astype(np.float32)
    faiss.normalize_L2(q)
    scores, indices = index.search(q, k)
    return scores, indices  # scores: cosine similarity (yüksek = daha benzer)

05 Temel RAG Pipeline

Tüm bileşenleri bir araya getiren, LangChain ile sıfırdan yazılmış tam RAG pipeline.

Bu bölümde şimdiye kadar öğrendiğimiz tüm parçaları birleştirip çalışan bir RAG sistemi kuruyoruz: PDF yükleme, chunking, embedding, FAISS index, sorgu ve LLM yanıt üretimi.

rag_pipeline.py
# pip install langchain langchain-openai langchain-community faiss-cpu pypdf
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

os.environ["OPENAI_API_KEY"] = "sk-..."

# ── 1. Belge yükle ──────────────────────────────────────────
loader = PyPDFLoader("./teknik_dokuman.pdf")
documents = loader.load()
print(f"Yüklendi: {len(documents)} sayfa")

# ── 2. Chunk'lara böl ───────────────────────────────────────
splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=100,
    length_function=len,
)
chunks = splitter.split_documents(documents)
print(f"Chunk sayısı: {len(chunks)}")

# ── 3. Embedding ve FAISS index ─────────────────────────────
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(chunks, embeddings)

# Lokal diske kaydet
vectorstore.save_local("./faiss_index")
print("Index kaydedildi.")

# ── 4. Retriever oluştur ─────────────────────────────────────
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4},   # en benzer 4 chunk
)

# ── 5. Prompt şablonu ────────────────────────────────────────
prompt_template = """Aşağıdaki bağlam bilgisini kullanarak soruyu Türkçe yanıtla.
Bağlamda cevap yoksa "Bu bilgi belgelerimde yer almıyor." de.

Bağlam:
{context}

Soru: {question}

Yanıt:"""

prompt = PromptTemplate(
    template=prompt_template,
    input_variables=["context", "question"]
)

# ── 6. RAG zinciri ──────────────────────────────────────────
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",   # tüm chunk'ları tek prompt'a doldur
    retriever=retriever,
    chain_type_kwargs={"prompt": prompt},
    return_source_documents=True,
)

# ── 7. Sorgula ───────────────────────────────────────────────
result = qa_chain.invoke({"query": "Ürün kurulum adımları nelerdir?"})
print("\n=== YANIT ===")
print(result["result"])
print("\n=== KAYNAKLAR ===")
for doc in result["source_documents"]:
    print(f"  Sayfa {doc.metadata.get('page', '?')}: {doc.page_content[:80]}...")
DİKKAT

chain_type="stuff" tüm chunk'ları tek prompt'a doldurur. 4 chunk × 800 karakter ≈ 3200 token olur. Çok fazla chunk veya büyük belgeler için context window limitine dikkat edin. Uzun belgeler için map_reduce veya refine chain tiplerini değerlendirin.

06 Hybrid Search

Semantik vektör aramasını BM25 keyword aramasıyla birleştiren hybrid search, her iki yöntemin güçlü yanlarını kullanır.

Tek başına vektör araması her zaman yeterli değildir. "Python 3.11 changelog" gibi spesifik anahtar kelime sorgularında, semantik benzerlik sürüm numarası ve teknik terimleri kaçırabilir. BM25 (Best Match 25), TF-IDF tabanlı klasik keyword arama algoritmasıdır — bu tür sorgularda güçlüdür.

Hybrid search, her iki yöntemi birleştirerek her iki yaklaşımın güçlü yönlerinden yararlanır.

RRF (Reciprocal Rank Fusion)

İki farklı ranking listesini birleştirmenin en yaygın yolu Reciprocal Rank Fusion'dır. Her listenin i. sırasındaki eleman için 1 / (k + rank) skoru hesaplanır ve her eleman için bu skorlar toplanır.

hybrid_search.py
# pip install langchain langchain-community rank-bm25
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# Belgeler (gerçekte chunk'larınız olur)
from langchain.schema import Document
docs = [
    Document(page_content="Python 3.11 önemli performans iyileştirmeleri içerir."),
    Document(page_content="Transformer attention mekanizması Q,K,V matrislerini kullanır."),
    Document(page_content="Python 3.12 tip ipuçlarını iyileştirdi."),
    Document(page_content="BERT encoder tabanlı bir dil modelidir."),
    Document(page_content="Python sürüm 3.11 yüzde 25 daha hızlı çalışır."),
]

# BM25 Retriever (keyword arama)
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 3

# FAISS Retriever (semantik arama)
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(docs, embeddings)
faiss_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# Ensemble: RRF ile birleştir
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever],
    weights=[0.4, 0.6],   # BM25: 40%, Vektör: 60%
)

# Sorgula
query = "Python 3.11 performans"
results = ensemble_retriever.invoke(query)

print(f"Hybrid search sonuçları ({len(results)} belge):")
for i, doc in enumerate(results):
    print(f"  {i+1}. {doc.page_content}")

Elle RRF Implementasyonu

rrf.py
from collections import defaultdict
from typing import List

def reciprocal_rank_fusion(
    results_lists: List[List[str]],
    k: int = 60
) -> List[tuple]:
    """
    results_lists: Her retriever'dan gelen belge ID listesi
    k: RRF sabit parametresi (tipik: 60)
    Döndürür: (doc_id, skor) çiftleri, skora göre sıralı
    """
    scores = defaultdict(float)
    for results in results_lists:
        for rank, doc_id in enumerate(results):
            scores[doc_id] += 1.0 / (k + rank + 1)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

# Örnek
bm25_results = ["doc_0", "doc_4", "doc_2"]
vec_results  = ["doc_4", "doc_0", "doc_1"]

fused = reciprocal_rank_fusion([bm25_results, vec_results])
for doc_id, score in fused:
    print(f"{doc_id}: {score:.4f}")

07 Reranking

Cross-encoder reranker ile ilk retrieval sonuçlarını yeniden sıralayarak precision'ı artırma.

İlk retrieval aşaması (bi-encoder: sorgu ve belgeyi ayrı ayrı encode et) hızlıdır ama hassasiyeti sınırlıdır. Reranking, alınan ilk N sonucu (örneğin 20) bir cross-encoder ile yeniden puanlandırır. Cross-encoder sorgu ve belgeyi birlikte encode eder — daha doğru ama yavaş. Reranker yalnızca küçük bir küme üzerinde çalıştığından pratiktir.

01 Bi-encoder ile hızlı retrieval → top-20 belge
02 Cross-encoder reranker → her (sorgu, belge) çifti yeniden puanla
03 Yeni sıraya göre top-4 seç → LLM'e gönder
reranking.py
# pip install sentence-transformers flashrank
from sentence_transformers import CrossEncoder
import numpy as np

# Cross-encoder modeli yükle
reranker = CrossEncoder(
    "cross-encoder/ms-marco-MiniLM-L-6-v2",
    max_length=512,
)

query = "Transformer attention nasıl çalışır?"
candidate_docs = [
    "Attention mekanizması Q, K ve V matrislerini hesaplar.",
    "Recurrent neural network ardışık veri işler.",
    "Self-attention her pozisyonu diğerleriyle ilişkilendirir.",
    "Convolutional network görüntü işlemede kullanılır.",
    "Multi-head attention paralel attention katmanları çalıştırır.",
]

# (sorgu, belge) çiftleri oluştur
pairs = [(query, doc) for doc in candidate_docs]

# Puanla
scores = reranker.predict(pairs)

# Yeniden sırala
ranked = sorted(
    zip(candidate_docs, scores),
    key=lambda x: x[1],
    reverse=True
)

print(f"Sorgu: '{query}'\n")
for rank, (doc, score) in enumerate(ranked):
    print(f"{rank+1}. [{score:.3f}] {doc}")

LangChain ile Reranking Pipeline

rerank_pipeline.py
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# Reranker model
model = HuggingFaceCrossEncoder(
    model_name="cross-encoder/ms-marco-MiniLM-L-6-v2"
)
compressor = CrossEncoderReranker(model=model, top_n=4)

# Temel retriever (20 belge getir)
base_retriever = vectorstore.as_retriever(
    search_kwargs={"k": 20}
)

# Reranker ile sar: 20 → rerank → top-4
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=base_retriever,
)

docs = compression_retriever.invoke("Attention mekanizması nasıl çalışır?")
print(f"Reranked sonuçlar: {len(docs)} belge")
for doc in docs:
    print(f"  · {doc.page_content[:100]}")
NOT

Cohere Rerank API, FlashRank (CPU'da çok hızlı) ve Jina Reranker de popüler seçeneklerdir. Üretim sistemlerinde gecikmeyi ölçün: cross-encoder 20 belgeyi puanlamak ~50–200ms alabilir. Latency hassas uygulamalarda FlashRank gibi hafif alternatifleri tercih edin.

08 Contextual Retrieval

Anthropic'in önerdiği yöntem: her chunk'a belge bağlamı ekleyerek embedding kalitesini ve retrieval hassasiyetini artırma.

Anthropic'in 2024 yılında yayınladığı Contextual Retrieval yöntemi, basit ama etkili bir fikre dayanır: bir chunk kendi başına bağlamdan kopuk olabilir. "Bu bu yıl uygulandı." gibi bir cümle, hangi belgeden geldiğini bilmeden anlamsızdır. Çözüm: her chunk'ın başına, o chunk'ın ait olduğu belge hakkında özet bir bağlam eklemek.

contextual_retrieval.py
from anthropic import Anthropic
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
import os

client = Anthropic()

CONTEXTUALIZE_PROMPT = """Aşağıda tüm belge ve bu belgeden alınan bir chunk yer almaktadır.
Chunk'ın belge içindeki konumunu ve amacını anlatan, arama sırasında bu chunk'ın
bulunabilmesini iyileştirecek kısa (2-3 cümle) bir bağlam metni yaz.
Sadece bağlam metnini yaz, başka şey ekleme.

<document>
{document}
</document>

<chunk>
{chunk}
</chunk>

Bağlam:"""

def add_context_to_chunk(document_text: str, chunk_text: str) -> str:
    """Bir chunk'a belge bağlamı ekle."""
    response = client.messages.create(
        model="claude-3-haiku-20240307",   # hızlı ve ucuz
        max_tokens=200,
        messages=[{
            "role": "user",
            "content": CONTEXTUALIZE_PROMPT.format(
                document=document_text[:5000],  # belgenin ilk 5000 karakteri
                chunk=chunk_text
            )
        }]
    )
    context = response.content[0].text.strip()
    return f"{context}\n\n{chunk_text}"

# Pipeline
with open("./belge.txt", "r", encoding="utf-8") as f:
    full_document = f.read()

splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=60)
chunks = splitter.split_text(full_document)

# Her chunk'a bağlam ekle
contextual_chunks = []
for i, chunk in enumerate(chunks):
    print(f"Bağlam ekleniyor: {i+1}/{len(chunks)}", end="\r")
    contextual = add_context_to_chunk(full_document, chunk)
    contextual_chunks.append(Document(page_content=contextual))

print(f"\nToplam {len(contextual_chunks)} contextual chunk oluşturuldu.")

Contextual Embedding Etkisi

YöntemRetrieval Precision@5Notlar
Standart chunking~68%Baseline
Contextual chunking~76%+8 puan — bağlam ekleme etkisi
Contextual + reranking~82%+14 puan toplam
Contextual + hybrid + reranking~87%Tam optimizasyon
NOT

Bağlam ekleme her chunk için bir API çağrısı gerektirir. 1000 chunk'lık bir belge, 1000 Haiku çağrısı anlamına gelir. Maliyet açısından Haiku gibi ucuz modeller tercih edin ve sonuçları cache'leyin — belge değişmediği sürece bu işlemi tekrar yapmaya gerek yoktur.

09 Retrieval Kalitesi Ölçümü

Precision@k, Recall@k, MRR ve RAGAS framework ile RAG sisteminizin performansını nesnel olarak ölçme.

Sistem geliştirmek için neyin çalışıp neyin çalışmadığını bilmek gerekir. Retrieval kalitesini sezgisel olarak değil, metriklerle ölçün.

Temel Metrikler

Precision@kTop-k sonuç içindeki ilgili belge oranı. Yüksek = gereksiz belge yok.
Recall@kTüm ilgili belgeler içinde top-k'da bulunan oran. Yüksek = hiçbir şey kaçırılmıyor.
MRRMean Reciprocal Rank. İlk doğru sonucun ortalama konumu. 1/rank skoru.
NDCG@kNormalized Discounted Cumulative Gain. Daha üst sıradaki doğru sonuçlara daha fazla puan.
retrieval_metrics.py
from typing import List, Set

def precision_at_k(retrieved: List[str], relevant: Set[str], k: int) -> float:
    """Top-k içindeki ilgili belge oranı."""
    top_k = retrieved[:k]
    hits = sum(1 for doc in top_k if doc in relevant)
    return hits / k

def recall_at_k(retrieved: List[str], relevant: Set[str], k: int) -> float:
    """Top-k'da bulunan ilgili belgeler / toplam ilgili belge sayısı."""
    if not relevant:
        return 0.0
    top_k = retrieved[:k]
    hits = sum(1 for doc in top_k if doc in relevant)
    return hits / len(relevant)

def reciprocal_rank(retrieved: List[str], relevant: Set[str]) -> float:
    """İlk doğru sonucun 1/rank değeri."""
    for i, doc in enumerate(retrieved):
        if doc in relevant:
            return 1.0 / (i + 1)
    return 0.0

def evaluate_retriever(retriever, test_cases: list, k: int = 5) -> dict:
    """
    test_cases: [{"query": ..., "relevant_ids": [...]}]
    """
    precisions, recalls, rrs = [], [], []

    for case in test_cases:
        docs = retriever.invoke(case["query"])
        retrieved_ids = [d.metadata["id"] for d in docs]
        relevant = set(case["relevant_ids"])

        precisions.append(precision_at_k(retrieved_ids, relevant, k))
        recalls.append(recall_at_k(retrieved_ids, relevant, k))
        rrs.append(reciprocal_rank(retrieved_ids, relevant))

    import statistics
    return {
        f"precision@{k}": statistics.mean(precisions),
        f"recall@{k}": statistics.mean(recalls),
        "mrr": statistics.mean(rrs),
    }

# Örnek test
test_cases = [
    {"query": "Attention mekanizması nedir?", "relevant_ids": ["chunk_0", "chunk_4"]},
    {"query": "BERT ile GPT farkı", "relevant_ids": ["chunk_1", "chunk_2"]},
]
metrics = evaluate_retriever(retriever, test_cases, k=5)
for name, val in metrics.items():
    print(f"{name}: {val:.3f}")

RAGAS ile Uçtan Uca Değerlendirme

ragas_eval.py
# pip install ragas datasets
from ragas import evaluate
from ragas.metrics import (
    faithfulness,        # yanıt bağlama sadık mı?
    answer_relevancy,    # yanıt soruya uygun mu?
    context_precision,   # bağlam içinde ilgili olanlar ne kadar üste?
    context_recall,      # tüm ilgili bilgi bağlamda mevcut mu?
)
from datasets import Dataset

# Değerlendirme verisi
eval_data = {
    "question": [
        "Transformer mimarisi ne zaman tanıtıldı?",
        "Self-attention nasıl çalışır?",
    ],
    "answer": [
        "Transformer mimarisi 2017 yılında tanıtıldı.",
        "Self-attention her pozisyonun diğerleriyle ilişki kurmasını sağlar.",
    ],
    "contexts": [
        ["Attention Is All You Need makalesi 2017'de yayınlandı..."],
        ["Self-attention Q, K, V matrisleri ile çalışır..."],
    ],
    "ground_truth": [
        "2017 yılında Google araştırmacıları tarafından.",
        "Q, K, V vektörleri ile attention skoru hesaplanır.",
    ],
}

dataset = Dataset.from_dict(eval_data)
result = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(result)

10 Üretim RAG Sistemi

LCEL ile conversational RAG, streaming yanıt, kaynak gösterimi ve FastAPI endpoint'i ile production'a hazır sistem.

Tüm bileşenleri birleştiren, chat geçmişini destekleyen, kaynak belgelerini gösteren ve bir HTTP API olarak sunan tam üretim RAG sistemi.

Conversational RAG Chain (LCEL)

conversational_rag.py
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage

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

# Önceden hazırlanmış vectorstore
vectorstore = FAISS.load_local(
    "./faiss_index",
    OpenAIEmbeddings(),
    allow_dangerous_deserialization=True
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# ── 1. Geçmiş aware sorgu yeniden formülleme ─────────────────
contextualize_q_prompt = ChatPromptTemplate.from_messages([
    ("system", """Sohbet geçmişi ve son kullanıcı sorusunu göz önünde bulundurarak,
geçmişe referans vermeden anlaşılabilir bağımsız bir soru oluştur.
Soruyu yanıtlama, sadece gerekirse yeniden formüle et."""),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
])

history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

# ── 2. Soru-cevap prompt'u ───────────────────────────────────
qa_prompt = ChatPromptTemplate.from_messages([
    ("system", """Sen yardımcı bir asistansın. Aşağıdaki bağlam bilgisini kullanarak
soruları Türkçe yanıtla. Bağlamda cevap yoksa 'Bu konu belgelerimde yer almıyor.' de.

Bağlam:
{context}"""),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
])

question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)

# ── 3. Tam RAG zinciri ───────────────────────────────────────
rag_chain = create_retrieval_chain(
    history_aware_retriever, question_answer_chain
)

# ── 4. Sohbet oturumu ────────────────────────────────────────
chat_history = []

def chat(user_input: str) -> str:
    result = rag_chain.invoke({
        "input": user_input,
        "chat_history": chat_history,
    })

    chat_history.extend([
        HumanMessage(content=user_input),
        AIMessage(content=result["answer"]),
    ])

    print("\n=== YANIT ===")
    print(result["answer"])
    print("\n=== KAYNAKLAR ===")
    for doc in result["context"]:
        src = doc.metadata.get("source", "bilinmeyen")
        page = doc.metadata.get("page", "?")
        print(f"  [{src}, sayfa {page}]: {doc.page_content[:80]}...")

    return result["answer"]

# Kullanım
chat("Transformer mimarisi nedir?")
chat("Bunu kim geliştirdi?")  # geçmişi hatırlar

FastAPI Endpoint

api.py
# pip install fastapi uvicorn
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List, Optional
import json

app = FastAPI(title="RAG API")

class ChatMessage(BaseModel):
    role: str   # "user" veya "assistant"
    content: str

class ChatRequest(BaseModel):
    message: str
    history: Optional[List[ChatMessage]] = []

class ChatResponse(BaseModel):
    answer: str
    sources: List[dict]

@app.post("/chat", response_model=ChatResponse)
async def chat_endpoint(request: ChatRequest):
    # Geçmişi LangChain formatına çevir
    history = []
    for msg in request.history:
        if msg.role == "user":
            history.append(HumanMessage(content=msg.content))
        else:
            history.append(AIMessage(content=msg.content))

    result = rag_chain.invoke({
        "input": request.message,
        "chat_history": history,
    })

    sources = [
        {
            "content": doc.page_content[:200],
            "source": doc.metadata.get("source", ""),
            "page": doc.metadata.get("page", 0),
        }
        for doc in result["context"]
    ]

    return ChatResponse(answer=result["answer"], sources=sources)

@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    async def generate():
        async for chunk in rag_chain.astream({
            "input": request.message,
            "chat_history": [],
        }):
            if "answer" in chunk:
                data = json.dumps({"token": chunk["answer"]})
                yield f"data: {data}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

# Çalıştır: uvicorn api:app --host 0.0.0.0 --port 8080 --reload
NOT

Üretim ortamında chat geçmişini istemcide (frontend) ya da bir session store'da (Redis) tutun. Uzun sohbetlerde geçmişi tümüyle LLM'e göndermek yerine özetleyin veya son N mesajı tutun — context window sınırını ve maliyet artışını önlemek için bu kritiktir.

Sistem Mimarisi Özeti

01 Belge → Loader → Chunker → Contextual Chunk
02 Chunk → Embedding → Vektör DB (FAISS / Chroma / pgvector)
03 Sorgu → Embedding → Vektör DB arama → top-20
04 top-20 → BM25 ile Hybrid Fusion (RRF)
05 Hybrid sonuç → Cross-encoder Reranker → top-5
06 top-5 + soru + geçmiş → LLM → Kaynaklı Yanıt