Demo RAG ile üretim RAG arasındaki fark
LLM API'leriyle çalışmak: istemci, JSON ve araç çağrısı dersinde bir belgeyi cümlelere bölüp TF-IDF ile en ilgili cümleleri isteme (prompt) koydun; Embedding ve vektör arama: anlamsal arama ve RAG'in getirme (retrieval) katmanı dersinde getirme (retrieval) katmanını (embedding, BM25, RRF, recall@k) derinlemesine kurdun. Bu derste parçaları birleştirip uçtan uca bir RAG uygulaması (Retrieval-Augmented Generation, getirmeyle zenginleştirilmiş üretim) yazıyorsun: belge gelir, temizlenir, parçalanır, indekslenir; soru gelir, getirilir, bütçeye sığdırılır, kaynaklı cevaba dönüşür; belge değişince indeks güncellenir.
Senaryo: Pusula İK çalışanları izin, çalışma düzeni ve eğitim kurallarını soruyor. Kaynaklar kurgusal bir yönetmelik metni ile gerçek pusula_ik tablolarından üretilen istatistik belgeleri; ürün sorularını da sınamak için anadolu_market ürünlerinden kısa ürün kartları ekliyoruz.
| Hat | Adımlar | Ne zaman çalışır? |
|---|---|---|
| İndeksleme (çevrim dışı) | yükle → temizle → parçala → meta veri ekle → vektörleştir → indeksle | Belge eklenince, değişince, silinince |
| Sorgu (çevrim içi) | soru → getir (hibrit) → yeniden sırala → bağlam bütçesi → istem → LLM → atıf denetimi → cevap | Her soruda, milisaniyeler içinde |
1. Belge yükleme ve temizleme
Gerçek belgeler PDF, HTML, Word ya da wiki sayfası olarak gelir ve içinde gürültü taşır: HTML etiketleri, & gibi kaçışlar, sayfa numaraları, her sayfada tekrar eden "Kurum içi - Gizli" altbilgisi, çift boşluklar. Bu gürültü hem parçaları şişirir hem de aramayı bozar. Bir de görünmez tuzak var: aynı "ü" harfi tek karakter (NFC) ya da "u + iki nokta" (NFD) olarak gelebilir; normalleştirmezsen iki metin eşit görünür ama eşleşmez.
import re, html, unicodedata, hashlib
from dataclasses import dataclass, field
@dataclass
class Belge:
id: str
metin: str
meta: dict = field(default_factory=dict)
@property
def parmak_izi(self):
"""İçerik değişti mi? Güncellemede bu özetle karşılaştırırız."""
return hashlib.sha256(self.metin.encode("utf-8")).hexdigest()[:12]
def tr_kucuk(s):
return s.replace("I", "ı").replace("İ", "i").lower()
ALTBILGI = re.compile(r"^(sayfa \d+ / \d+|kurum içi - gizli)$")
def temizle(ham):
metin = re.sub(r"<[^>]+>", " ", ham) # HTML etiketleri
metin = unicodedata.normalize("NFC", html.unescape(metin))
satirlar = []
for s in metin.splitlines():
s = re.sub(r"\s+", " ", s).strip() # sekme, çift boşluk
if s and not ALTBILGI.match(tr_kucuk(s)): # boş satır, altbilgi
satirlar.append(s)
return "\n".join(satirlar)
ham = "<h2>Hastalık izni</h2>\n<p>Doktor raporu en geç 2 iş günü & içinde iletilir.</p>\n\nSayfa 1 / 3\nKurum içi - Gizli\n"
temiz = temizle(ham)
print(temiz)
b = Belge("pusula-izin", temiz, {"kaynak": "izin_yonetmeligi.html", "erisim": "tum_calisanlar"})
print(b.parmak_izi, len(b.metin))
Her belge bir kimlik, temiz metin ve meta veri (metadata) taşır: kaynak dosya, tür, tarih ve en önemlisi erişim bilgisi. parmak_izi metnin özetidir; belge yeniden geldiğinde değişip değişmediğini bununla anlarsın. Şimdi korpusu gerçek veriyle kuralım:
POLITIKA = """# İzin yönetmeliği
## Yıllık izin
Yıllık izin hakkı kıdeme göre belirlenir: 1-5 yıl arası 14 gün, 5-15 yıl arası 20 gün, 15 yıl ve üzeri 26 gün. Yıllık izin en az 7 gün önceden İK portalından talep edilir. Talep yönetici onayıyla kesinleşir.
## Hastalık izni
Doktor raporu en geç 2 iş günü içinde İK'ya iletilir. Raporsuz hastalık izni yılda en fazla 3 gündür.
## Mazeret izni
Evlilik, doğum ve vefat durumlarında 3 güne kadar ücretli mazeret izni verilir.
## Ücretsiz izin
Ücretsiz izin yönetici ve İK direktörünün onayıyla en fazla 30 gün verilebilir.
# Çalışma düzeni
## Hibrit çalışma
Hibrit çalışanlar haftada en az 2 gün ofiste bulunur. Ofis günlerini takım lideri belirler.
## Uzaktan çalışma
Tam uzaktan çalışma yalnızca Yazılım Geliştirme ile Veri ve Analitik departmanlarında mümkündür.
# Eğitim
## Zorunlu eğitimler
Bilgi Güvenliği Farkındalığı ve KVKK Temel eğitimleri tüm çalışanlar için zorunludur. Her ikisi de 2 saattir ve her yıl yenilenir.
"""
import statistics
from collections import defaultdict
import veri
def ik_istatistik_belgeleri():
"""Gerçek ik_leaves tablosundan izin türü başına kısa bir istatistik belgesi üretir."""
izin = defaultdict(list)
for r in veri.kayitlar("pusula_ik", "ik_leaves"):
izin[r["leave_type"]].append(r["days"])
return [Belge(f"ik-izin-{tur}",
f"{tur.capitalize()} izin kayıtları: {len(g)} kayıt, ortalama {statistics.mean(g):.1f} gün, en uzun {max(g)} gün.",
{"kaynak": "ik_leaves", "tur": "istatistik", "erisim": "ik"})
for tur, g in sorted(izin.items())]
def urun_belgeleri():
return [Belge(f"urun-{u['product_id']}",
f"{u['name']} ({u['category']}): fiyat {u['price']:.0f} TL, stok {u['stock']} adet.",
{"kaynak": "products", "tur": "urun", "erisim": "tum_calisanlar"})
for u in veri.kayitlar("anadolu_market", "products")]
KORPUS = [Belge("pusula-politika", POLITIKA, {"kaynak": "ik_politika.md", "tur": "politika", "erisim": "tum_calisanlar"})]
KORPUS += ik_istatistik_belgeleri() + urun_belgeleri()
print(len(KORPUS), "belge")
print(KORPUS[2].metin)
print(KORPUS[-1].metin)
pip install pypdf python-docx ile (ikisi bu sayfada da çalışır, bkz. PDF ve Word belgeleriyle otomasyon) PdfReader("yonetmelik.pdf").pages[i].extract_text() ve Document("form.docx").paragraphs kullanırsın. Taranmış (resim) PDF'ler için OCR gerekir; tablo içeren PDF'lerde metin sırası bozulabileceği için çıktıyı mutlaka gözle kontrol et.2. Parçalama ve meta veri
Parçalama (chunking) stratejilerini (sabit pencere, cümle, başlık, örtüşme) embedding dersinde ayrıntılı gördün. Üretimde bunları birleştirirsin: önce başlıklara göre bölümlere ayır, bölüm içinde cümleleri karakter sınırına kadar topla, ardışık parçalar arasında bir cümle örtüştür. En önemli iki ek:
- Başlık yolu ("İzin yönetmeliği > Hastalık izni") her parçaya eklenir. "Raporsuz 3 gündür" cümlesi tek başına hangi izinden söz ettiğini söylemez; başlık söyler.
- Kararlı kimlik:
belge_id#sıra. Atıf, güncelleme ve silme bu kimlikle yapılır.
def cumleler(metin):
return [c for c in re.split(r"(?<=[.!?])\s+", metin.strip()) if c]
def parcala(belge, max_karakter=220, ortusme=1):
"""Başlık + cümle tabanlı parçalama: her parça başlık yolunu taşır, ardışık parçalar ortusme cümle paylaşır."""
bolumler, yol = [], {}
for satir in belge.metin.splitlines():
if m := re.match(r"(#+) (.+)", satir):
seviye = len(m.group(1))
yol = {k: v for k, v in yol.items() if k < seviye} | {seviye: m.group(2)}
elif satir.strip():
bolumler.append((" > ".join(yol[k] for k in sorted(yol)), satir))
parcalar = []
for baslik, paragraf in bolumler:
cs, i = cumleler(paragraf), 0
while i < len(cs):
j = i + 1
while j < len(cs) and len(" ".join(cs[i:j + 1])) <= max_karakter:
j += 1
parcalar.append({"id": f"{belge.id}#{len(parcalar)}", "belge_id": belge.id,
"baslik": baslik, "metin": " ".join(cs[i:j]), "meta": dict(belge.meta)})
if j >= len(cs):
break
i = max(j - ortusme, i + 1)
return parcalar
PARCALAR = [p for b in KORPUS for p in parcala(b, max_karakter=120)]
print(len(PARCALAR), "parça")
for p in PARCALAR[:3]:
print(p["id"], "|", p["baslik"], "|", p["metin"])
| Strateji | Güçlü yanı | Zayıf yanı | Uygun belge |
|---|---|---|---|
| Sabit pencere + örtüşme | Basit, her metinde çalışır | Cümleyi ortadan böler | Yapısız düz metin, log |
| Cümle tabanlı | Anlam bütünlüğü korunur | Kısaltmalar ("vb.") bölmeyi bozar | Haber, e-posta |
| Başlık tabanlı | Bağlam (başlık yolu) korunur | Bölümler çok uzun ya da çok kısa olabilir | Yönetmelik, wiki, dokümantasyon |
| Başlık + cümle (hibrit) | İkisinin iyi yanları | Biraz daha kod | Kurumsal belgelerin çoğu |
3. İndeks: TF-IDF ve sahte embedding ile hibrit arama
İki sinyal kullanıyoruz: kelime sinyali (TF-IDF; "KVKK", ürün kodu gibi tam eşleşmelerde güçlü) ve vektör sinyali (embedding; yazım farkı ve anlam yakınlığında güçlü). Tarayıcıda gerçek embedding modeli çalıştıramadığımız için harf üçlülerinden üretilen sahte embedding kullanıyoruz; arayüz gerçeğiyle aynı (metin → normalize vektör). Türkçe eklemeli bir dil olduğu için TF-IDF'e kaba bir kök bulucu veriyoruz: kelimenin ilk 5 harfi. Bu basit yöntem Türkçe bilgi erişiminde şaşırtıcı derecede iyi sonuç verir.
import numpy as np
import zlib
from sklearn.feature_extraction.text import TfidfVectorizer
def kok_analiz(metin):
"""Türkçe için kaba ama etkili kök: kelimenin ilk 5 harfi (izni, izin, izinler → izin)."""
return [k[:5] for k in re.findall(r"\w+", tr_kucuk(metin))]
def sahte_embedding(metin, boyut=256):
"""Ağsız embedding taklidi: harf üçlülerini sabit bir özetle (crc32) kovalara sayar, normalize eder."""
v = np.zeros(boyut)
t = f" {tr_kucuk(metin)} "
for i in range(len(t) - 2):
v[zlib.crc32(t[i:i + 3].encode()) % boyut] += 1
n = np.linalg.norm(v)
return v / n if n else v
def parca_metni(p):
return f"{p['baslik']}: {p['metin']}" if p["baslik"] else p["metin"]
class HibritIndeks:
def __init__(self, parcalar, alfa=0.5):
self.parcalar, self.alfa = parcalar, alfa
metinler = [parca_metni(p) for p in parcalar]
self.tfidf = TfidfVectorizer(analyzer=kok_analiz)
self.X = self.tfidf.fit_transform(metinler)
self.E = np.array([sahte_embedding(m) for m in metinler])
def ara(self, sorgu, k=3, filtre=None):
s_kelime = (self.X @ self.tfidf.transform([sorgu]).T).toarray().ravel()
s_vektor = self.E @ sahte_embedding(sorgu)
skor = self.alfa * s_kelime + (1 - self.alfa) * s_vektor
adaylar = [i for i in np.argsort(-skor, kind="stable")
if filtre is None or filtre(self.parcalar[i]["meta"])]
return [(self.parcalar[i], round(float(skor[i]), 3)) for i in adaylar[:k]]
IDX = HibritIndeks(PARCALAR)
for p, s in IDX.ara("Raporsuz hastalık izni kaç gün?"):
print(s, p["id"], p["metin"][:60])
filtre parametresine dikkat: yetki ve meta veri filtresi sıralamadan önce uygulanır (embedding dersindeki kural). Örneğin İK dışındaki bir çalışan için filtre=lambda m: m["erisim"] == "tum_calisanlar" istatistik belgelerini hiç göstermez.
Yeniden sıralama ve hata analizi
Üretimde ilk aşama 20–50 aday getirir, bir çapraz kodlayıcı (cross-encoder) soru ile her adayı birlikte okuyup en iyi 3–5'ini seçer. Ama önce sistemin nerede yanıldığını görmelisin. Şu soruda ilk sonuç yanlış:
soru = "Evlilikte kaç gün izin var?"
for p, s in IDX.ara(soru, k=3):
print(s, p["id"], p["baslik"])
print(kok_analiz(soru))
print(kok_analiz("Evlilik, doğum ve vefat durumlarında 3 güne kadar ücretli mazeret izni verilir."))
Teşhis: kaba kök "izni" ile "izin"i, "güne" ile "gün"ü eşleştiremiyor; yıllık izin parçası "izin" ve "gün" kelimeleriyle kazanıyor. Çözüm seçenekleri: gerçek bir morfolojik çözümleyici (Zemberek gibi), gerçek embedding modeli ya da çapraz kodlayıcı. Bu tür hataları ancak bir altın soru seti ile yakalarsın; değerlendirme ölçütlerini LLM uygulamalarını test etmek ve değerlendirmek dersinde ayrıntılı göreceksin.
# pip install sentence-transformers (Kendi bilgisayarında dene)
from sentence_transformers import CrossEncoder
model = CrossEncoder("cross-encoder/mmarco-mMiniLMv2-L12-H384-v1") # çok dilli
adaylar = IDX.ara(soru, k=20)
puanlar = model.predict([(soru, parca_metni(p)) for p, _ in adaylar])
en_iyi = [p for (p, _), s in sorted(zip(adaylar, puanlar), key=lambda x: -x[1])][:4]
4. Bağlam penceresi bütçesi
Modelin bağlam penceresi büyük olsa da her parça token, yani para ve gecikme demektir. Üstelik alakasız parçalar gürültü katar ve modeller uzun bağlamın ortasındaki bilgiyi daha sık kaçırır. Bu yüzden getirilen parçaları bir token bütçesine sığdırırsın: skor sırasıyla ekle, sığmayanı atla, aynı metni iki kez koyma.
import math
def token_tahmini(metin):
return math.ceil(len(metin) / 4)
def baglam_sec(sonuclar, butce=120):
"""Skor sırasıyla parça ekle; bütçeyi aşanı atla, aynı metni iki kez koyma."""
secilen, kullanilan, gorulen = [], 0, set()
for p, skor in sonuclar:
t = token_tahmini(parca_metni(p))
if p["metin"] in gorulen or kullanilan + t > butce:
continue
secilen.append(p); kullanilan += t; gorulen.add(p["metin"])
return secilen, kullanilan
secilen, t = baglam_sec(IDX.ara("Yıllık izin kaç gün ve nasıl talep edilir?", k=6), butce=90)
print([p["id"] for p in secilen], t, "token")
Bütçe şöyle hesaplanır: pencere - sistem istemi - soru - sohbet geçmişi - cevap için ayrılan. Token tahmini için LLM API'leriyle çalışmak: istemci, JSON ve araç çağrısı dersindeki "4 karakter ≈ 1 token" kuralını kullandık; Türkçede gerçek oran daha yüksektir, üretimde modelin kendi tokenizer'ı ile say (bunu Hugging Face ve yerel modellerle Python dersinde göreceksin).
5. İstem birleştirme, kaynak gösterme ve "bilmiyorum"
İstem üç şey söyler: yalnız kaynaklara dayan, her iddiaya [n] atfı koy, cevap yoksa "Bilmiyorum." de. İstem şablonu yazma tekniklerini Prompt mühendisliği ve yapılandırılmış çıktı dersinde ayrıntılı göreceksin; burada önemli olan kodun modelin sözüne güvenmemesi. İki savunma hattı kurarsın:
- Getirmeden önce: en iyi skor eşiğin altındaysa modeli hiç çağırma. Hem halüsinasyonu hem maliyeti keser.
- Üretimden sonra: cevapta atıf yoksa ya da var olmayan bir kaynağa ([7] gibi) atıf varsa cevabı reddet.
def istem_kur(soru, parcalar):
kaynak = "\n".join(f"[{i}] {parca_metni(p)}" for i, p in enumerate(parcalar, 1))
return ("Yalnızca aşağıdaki numaralı kaynaklara dayanarak cevap ver. Her cümlenin sonuna "
"kullandığın kaynağı [n] biçiminde yaz. Kaynaklarda cevap yoksa yalnızca 'Bilmiyorum.' de.\n"
f"Kaynaklar:\n{kaynak}\nSoru: {soru}")
class SahteLLM:
"""Ağsız model: istemdeki kaynaklardan soruyla en çok kök paylaşanı seçip atıfla döndürür."""
def tamamla(self, istem):
soru = istem.rsplit("Soru:", 1)[-1]
kaynaklar = re.findall(r"^\[(\d+)\] (.+)$", istem, re.M)
if not kaynaklar:
return "Bilmiyorum."
q = set(kok_analiz(soru))
no, metin = max(kaynaklar, key=lambda k: len(q & set(kok_analiz(k[1]))))
return f"{metin.split(': ', 1)[-1]} [{no}]"
def atiflari_dogrula(cevap, n_kaynak):
atif = [int(x) for x in re.findall(r"\[(\d+)\]", cevap)]
return bool(atif) and all(1 <= a <= n_kaynak for a in atif)
def cevapla(soru, llm, esik=0.25, k=4):
sonuclar = IDX.ara(soru, k=k)
if not sonuclar or sonuclar[0][1] < esik:
return {"cevap": "Bilmiyorum.", "kaynaklar": [], "neden": "düşük getirme skoru"}
parcalar, _ = baglam_sec(sonuclar, butce=150)
cevap = llm.tamamla(istem_kur(soru, parcalar))
if not atiflari_dogrula(cevap, len(parcalar)):
return {"cevap": "Bilmiyorum.", "kaynaklar": [], "neden": "atıfsız cevap"}
nolar = sorted({int(x) for x in re.findall(r"\[(\d+)\]", cevap)})
return {"cevap": cevap, "kaynaklar": [parcalar[n - 1]["id"] for n in nolar], "neden": "ok"}
llm = SahteLLM()
print(cevapla("Raporsuz hastalık izni kaç gün?", llm))
print(cevapla("Şirket aracı kimlere verilir?", llm))
İkinci soru yönetmelikte yok ama yine de bir cevap geldi: "verilir" kelimesi mazeret izni parçasıyla eşleşti ve skor 0.25 eşiğini geçti. Eşiği 0.35'e çıkarırsan cevapla("Şirket aracı kimlere verilir?", llm, esik=0.35) "Bilmiyorum." döndürür. Eşik sezgiyle değil, altın setteki cevaplı ve cevapsız sorularla ölçerek seçilir; çok yüksek eşik de doğru soruları reddeder.
6. Önbellek
İK asistanına aynı sorular tekrar tekrar gelir. Önbellek (cache) anahtarı soru metninin normalleştirilmiş hâli ve indeks sürümünden oluşur; böylece belge güncellenince eski cevaplar kendiliğinden geçersiz olur.
class OnbellekliRAG:
def __init__(self, cevapla_fn, surum=1):
self.fn, self.surum, self.kayit = cevapla_fn, surum, {}
self.isabet = self.iska = 0
def anahtar(self, soru):
normal = " ".join(re.findall(r"\w+", tr_kucuk(soru)))
return hashlib.sha256(f"v{self.surum}|{normal}".encode()).hexdigest()[:16]
def sor(self, soru):
a = self.anahtar(soru)
if a in self.kayit:
self.isabet += 1
else:
self.iska += 1
self.kayit[a] = self.fn(soru)
return self.kayit[a]
r = OnbellekliRAG(lambda s: cevapla(s, llm))
r.sor("Raporsuz hastalık izni kaç gün?")
r.sor(" raporsuz HASTALIK izni kaç gün ")
r.surum += 1 # indeks güncellendi: eski kayıtlar artık eşleşmez
r.sor("Raporsuz hastalık izni kaç gün?")
print(r.isabet, r.iska)
Üç katman önbellek vardır: embedding önbelleği (aynı metin için ikinci kez embedding hesaplanmaz), getirme önbelleği (soru → parça kimlikleri) ve cevap önbelleği. Cevap önbelleğini kullanıcıya özel içerikte (ör. "benim izin bakiyem") kullanma; anahtara kullanıcı ya da yetki grubunu ekle. Sağlayıcıların istem önbelleği (prompt caching) ise sabit sistem istemini ucuzlatır; ikisi birbirinin yerine geçmez.
7. Güncelleme ve silme
Belge değişince en sık yapılan hata yeni parçaları eskilerin üzerine yazmaktır: yeni sürüm daha kısaysa eski parçaların bir kısmı indekste hayalet olarak kalır ve model artık geçerli olmayan kuralı söyler. Doğru yol belge düzeyinde düşünmektir: parmak izi aynıysa hiçbir şey yapma; değiştiyse belgenin tüm eski parçalarını sil, yenilerini ekle, sürümü artır.
class CanliIndeks:
"""Belge düzeyinde ekle/güncelle/sil; değişmeyen belgeyi yeniden işlemez."""
def __init__(self):
self.belgeler, self.parcalar, self.surum = {}, {}, 0
def yukle(self, belge):
eski = self.belgeler.get(belge.id)
if eski and eski.parmak_izi == belge.parmak_izi:
return "değişmedi"
self.sil(belge.id, sessiz=True) # önce eski parçaların hepsi gider
self.belgeler[belge.id] = belge
for p in parcala(belge, max_karakter=120):
self.parcalar[p["id"]] = p
self.surum += 1
return "güncellendi" if eski else "eklendi"
def sil(self, belge_id, sessiz=False):
self.belgeler.pop(belge_id, None)
for pid in [pid for pid, p in self.parcalar.items() if p["belge_id"] == belge_id]:
del self.parcalar[pid]
if not sessiz:
self.surum += 1
c = CanliIndeks()
print(c.yukle(KORPUS[0]), len(c.parcalar), c.surum)
print(c.yukle(KORPUS[0]), c.surum)
kisa = Belge("pusula-politika", "# İzin yönetmeliği\nYıllık izin 14 gündür.", KORPUS[0].meta)
print(c.yukle(kisa), len(c.parcalar), c.surum)
Silme de aynı yoldan geçer; KVKK kapsamında "unutulma hakkı" talebi geldiğinde ilgili belgenin parçaları, vektörleri ve önbellek kayıtları birlikte silinmelidir. Vektör veritabanlarında bunun için parçalara belge_id meta verisi koyup "bu belge kimliğine sahip her şeyi sil" filtresiyle silersin.
8. FastAPI uç noktası
Asistanı bir web servisine çevirmek FastAPI ile API geliştirme dersindeki kalıplarla birkaç satırdır. İstek ve cevabı pydantic ile tanımla, indeksi uygulama başlarken bir kez yükle:
# pip install fastapi uvicorn scikit-learn numpy (Kendi bilgisayarında dene)
# Çalıştır: uvicorn rag_api:app --reload → http://127.0.0.1:8000/docs
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="Pusula İK Asistanı")
llm = GercekLLMIstemcisi() # llm-api dersindeki sağlayıcıdan bağımsız istemci
onbellek = OnbellekliRAG(lambda s: cevapla(s, llm, esik=0.35))
class Soru(BaseModel):
soru: str = Field(min_length=3, max_length=500)
class Cevap(BaseModel):
cevap: str
kaynaklar: list[str]
neden: str
@app.post("/sor", response_model=Cevap)
def sor(istek: Soru):
return onbellek.sor(istek.soru)
@app.post("/belgeler/{belge_id}")
def belge_yukle(belge_id: str, metin: str):
durum = canli.yukle(Belge(belge_id, temizle(metin), {"erisim": "tum_calisanlar"}))
onbellek.surum = canli.surum
return {"durum": durum, "surum": canli.surum}
@app.delete("/belgeler/{belge_id}")
def belge_sil(belge_id: str):
if belge_id not in canli.belgeler:
raise HTTPException(404, "belge yok")
canli.sil(belge_id)
onbellek.surum = canli.surum
return {"silindi": belge_id}
9. LangChain ve LlamaIndex ile karşılaştırma
Bu derste her adımı elle yazdın; çerçeveler aynı adımları hazır bileşenlerle sunar. Aynı hattın LangChain hâli:
# pip install langchain langchain-community langchain-text-splitters faiss-cpu langchain-huggingface
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
bolucu = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80)
parcalar = bolucu.create_documents([POLITIKA], metadatas=[{"kaynak": "ik_politika.md"}])
emb = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
db = FAISS.from_documents(parcalar, emb)
getirici = db.as_retriever(search_kwargs={"k": 4})
for d in getirici.invoke("Raporsuz hastalık izni kaç gün?"):
print(d.metadata, d.page_content[:60])
LlamaIndex ise belge odaklıdır; yükleme, indeks ve sorgu motoru tek akışta gelir:
# pip install llama-index llama-index-embeddings-huggingface
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
Settings.embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
belgeler = SimpleDirectoryReader("politikalar/").load_data() # PDF, docx, md
indeks = VectorStoreIndex.from_documents(belgeler)
motor = indeks.as_query_engine(similarity_top_k=4) # Settings.llm ile model seçilir
yanit = motor.query("Raporsuz hastalık izni kaç gün?")
print(yanit, [n.node.metadata for n in yanit.source_nodes])
| Elle (bu ders) | LangChain | LlamaIndex | |
|---|---|---|---|
| Odak | Tam kontrol, az bağımlılık | Genel LLM uygulama bileşenleri, zincirler | Belge yükleme, indeksleme, sorgu motorları |
| Parçalama | Kendi kuralın (başlık + cümle) | RecursiveCharacterTextSplitter vb. | SentenceSplitter, düğüm ayrıştırıcıları |
| Atıf ve "bilmiyorum" | Açıkça kodda | İstem + çıktı ayrıştırıcı ile senin işin | source_nodes hazır; eşik senin işin |
| Ne zaman? | Küçük-orta sistem, öğrenme, sıkı denetim | Çok bileşenli akışlar, ajanlar (LangGraph) | Çok sayıda belge kaynağı, hızlı prototip |
Çerçeve seçsen de bu derste yazdığın parçaları anlaman gerekir: hata ayıklarken "hangi parça neden geldi, neden bütçeye girmedi, atıf neden reddedildi" sorularını cevaplayabilmelisin.
Ebeveyn-çocuk parçalar
Küçük parçalarla ara (isabet artar), modele parçanın ait olduğu büyük bölümü ya da komşu parçaları ver (bağlam artar). Meydan okuma alıştırmasında komşu parça genişletmeyi yazacaksın.
Sorgu dönüşümü
Sohbette "peki ya ücretsiz olanı?" sorusu tek başına aranamaz; önce geçmişle birlikte bağımsız bir soruya yeniden yazılır. HyDE yaklaşımında model önce varsayımsal bir cevap yazar, arama o cevapla yapılır.
Değerlendirme hattı
Getirme (recall@k, MRR), cevap sadakati, atıf doğruluğu ve ret oranını ayrı ayrı ölç; her indeks ya da istem değişikliğinde altın seti otomatik çalıştır. Ölçütlerin ayrıntısı LLM uygulamalarını test etmek ve değerlendirmek dersinde.
Ne zaman RAG yetmez?
"Geçen yıl kaç gün hastalık izni kullanıldı?" gibi toplama soruları metin aramasıyla değil SQL ile cevaplanır; bu soruları bir yönlendirici ile text-to-SQL ajanına (LLM ajanları: araç döngüsü, hafıza, MCP ve güvenli sınırlar) gönder.