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
| Kriter | RAG | Fine-Tuning |
|---|---|---|
| Bilgi güncelleme | Vektör DB'yi güncelle → anında | Modeli yeniden eğit → pahalı/yavaş |
| Kaynak gösterimi | Doğal (hangi belgeden geldiği belli) | Zor (model bilgiyi içselleştirir) |
| Maliyet | Düşük başlangıç maliyeti | Yüksek GPU maliyeti |
| Uzun belge | Çalışır (chunk + retrieve) | Context limit sorunu |
| Davranış değişimi | Sınırlı | Derin (ton, format, görev) |
| Ne zaman seçmeli | Bilgi tabanı, Q&A, sık güncellenen içerik | Domain 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ı)
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
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)
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.
# 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öntem | Avantaj | Dezavantaj | Ne zaman kullan |
|---|---|---|---|
| Fixed-size | Hızlı, basit | Cümle ortasında kesilebilir | Genel amaç, prototip |
| Recursive character | Doğal sınırları tercih eder | Chunk boyutu değişken | Çoğu üretim senaryosu |
| Sentence-aware | Cümle bütünlüğü korunur | Kısa cümleler çok küçük chunk | Haber, makale |
| Semantic | En doğal bölme | Yavaş (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
# 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
| Model | Boyut | Vektör Boyutu | Max Token | Notlar |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | 80 MB | 384 | 256 | Hızlı, İngilizce odaklı |
| all-mpnet-base-v2 | 420 MB | 768 | 384 | İngilizce için güçlü baseline |
| BAAI/bge-m3 | 570 MB | 1024 | 8192 | Çok dilli, uzun bağlam, önerilir |
| intfloat/multilingual-e5-large | 560 MB | 1024 | 512 | Türkçe dahil çok dilli |
| text-embedding-3-small (OpenAI) | — | 1536 | 8191 | API, ücretli, yüksek kalite |
| text-embedding-3-large (OpenAI) | — | 3072 | 8191 | API, ücretli, en yüksek kalite |
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
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ı
# 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
# 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çek | Metadata Filtre | Öne Çıkan Özellik |
|---|---|---|---|---|
| FAISS | Kütüphane | Milyonlar | Hayır | Hızlı, Facebook, çok indeks tipi |
| Chroma | Gömülü/Sunucu | Milyonlar | Evet | Kolay API, persist, LangChain desteği |
| pgvector | PostgreSQL eklentisi | Yüzler binler | Evet (SQL) | Mevcut Postgres altyapısına entegre |
| Weaviate | Managed/Self-hosted | Milyarlar | Evet | Hybrid search built-in |
| Pinecone | Managed (cloud) | Milyarlar | Evet | Yönetimden muaf, yüksek erişilebilirlik |
| Qdrant | Self-hosted/Cloud | Milyarlar | Evet | Rust 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:
FAISS Index Tipleri
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
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.
# 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]}...")
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.
# 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
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
# 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
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]}")
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.
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öntem | Retrieval Precision@5 | Notlar |
|---|---|---|
| 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 |
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
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
# 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)
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
# 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
Ü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