İçeriğe geç

Embedding-Based Selection: Bağlamdan İlişkisiz Parçaları Atmanın En Pratik Yolu

RAG'da retrieved chunks'ın çoğu (~%50-70'i) gerçekte cevaba katkı yapmıyor. Embedding similarity ile question-irrelevant kısımları atmak %50-80 token tasarrufu sağlar. Bu derste implementasyon, threshold seçimi ve LLM-as-judge ile doğrulama.

Şükrü Yusuf KAYA
16 dakikalık okuma
Orta
Embedding-Based Selection: Bağlamdan İlişkisiz Parçaları Atmanın En Pratik Yolu
🎯 RAG'ın gizli israfı
Tipik RAG: top-10 chunks retrieve, hepsini prompt'a ekle. Ama gerçek: bunlardan 3-4'ü asıl soruya ilgili. Geri kalan token israfı + model confusion. Bu derste o israfı kaldırıyoruz.

Pattern — Retrieved'in Retrieved'i#

[User Query] ↓ [Vector DB search] → top-20 chunks (RAW) ↓ [Embedding-based selection] → top-5 most relevant ↓ [LLM call with compact context]
İki katmanlı retrieval. Birinci geniş (recall odaklı), ikinci dar (precision odaklı).

Implementation#

import numpy as np from openai import OpenAI client = OpenAI() def embed(text: str) -> np.ndarray: response = client.embeddings.create( model="text-embedding-3-small", input=text, ) return np.array(response.data[0].embedding) def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) def select_relevant_chunks( query: str, chunks: list[str], top_k: int = 5, threshold: float = 0.6, ) -> list[str]: query_emb = embed(query) scored = [] for chunk in chunks: chunk_emb = embed(chunk) score = cosine_similarity(query_emb, chunk_emb) scored.append((chunk, score)) # Top-K + threshold filter scored.sort(key=lambda x: x[1], reverse=True) filtered = [c for c, s in scored if s >= threshold][:top_k] return filtered # Usage all_chunks = vector_db_search(query, top_k=20) # geniş retrieve selected = select_relevant_chunks(query, all_chunks, top_k=5, threshold=0.6) context = "\n\n".join(selected) response = llm(query, context)

Threshold Seçimi — Tasarruf vs Recall#

Threshold dilemma:
  • Çok düşük (0.3): hepsi geçer, tasarruf az
  • Çok yüksek (0.8): çok azı geçer, kritik bilgiyi kaçırırsın

Doğru threshold'u bulma#

def find_optimal_threshold(eval_set): """50 test sorusunda farklı threshold'ları test et.""" thresholds = [0.4, 0.5, 0.6, 0.7, 0.8] results = {} for t in thresholds: accuracies = [] for query, expected_chunks in eval_set: selected = select_relevant_chunks(query, all_chunks, threshold=t) recall = len(set(selected) & set(expected_chunks)) / len(expected_chunks) accuracies.append(recall) results[t] = np.mean(accuracies) return results # Output # 0.4: recall 0.95, avg_chunks 18 # 0.5: recall 0.92, avg_chunks 12 # 0.6: recall 0.86, avg_chunks 7 ← sweet spot # 0.7: recall 0.71, avg_chunks 4 ← çok agresif # 0.8: recall 0.45, avg_chunks 2
%90+ recall'da en yüksek threshold = en iyi tasarruf.

Reranking vs Selection — Fark#

Bazen karıştırılır:
RerankingSelection
Sıralamayı düzeltirAz olanı atar
Tüm chunks tutulurFiltreleme yapar
Token sayısı aynı kalırToken sayısı azalır
Kalite içinCost için

Birlikte kullan#

# 1. Geniş retrieve (top-20) chunks = vector_db.search(query, k=20) # 2. Rerank (Cohere Rerank veya Voyage rerank) reranked = cohere_rerank(query, chunks, top_n=10) # 3. Embedding selection (top-K with threshold) selected = select_relevant_chunks(query, reranked, top_k=5, threshold=0.6) # 4. LLM call
Bu pipeline: en iyi recall + en az token.

LLM-as-Judge ile Doğrulama#

Selection kalitesini test et:
def is_chunk_relevant(query: str, chunk: str) -> bool: """Bir küçük LLM (Haiku) ile chunk-relevance check.""" response = client.messages.create( model="claude-haiku-4-5", max_tokens=10, messages=[{ "role": "user", "content": f"Soru: {query}\nMetin: {chunk}\n\nBu metin sorunun cevabına katkı yapar mı? Sadece 'YES' veya 'NO' yaz." }], ) return "YES" in response.content[0].text.upper() # Validation for chunk in selected: if not is_chunk_relevant(query, chunk): print(f"⚠️ Selected ama relevant değil: {chunk[:80]}") for chunk in all_chunks: if chunk not in selected and is_chunk_relevant(query, chunk): print(f"⚠️ Atıldı ama relevant idi: {chunk[:80]}")
Bu validation 50-100 test sorusunda yap. False-positive ve false-negative oranları %5-10'a düşürmeyi hedefle.

Gerçek Etki — Bir Vaka#

Bir Türkçe customer-support RAG:

Önce (selection yok)#

  • top-10 chunks retrieve
  • Ortalama 8K context token
  • /istek:/istek: 0.024 (Sonnet 4.6)
  • 100K istek/ay = $2.400

Sonra (embedding selection)#

  • top-20 retrieve, top-5 select (threshold 0.6)
  • Ortalama 3.2K context token
  • /istek:/istek: 0.011
  • 100K istek/ay = $1.100
Tasarruf: %54, yıllık $15.600.
Plus kalite iyileşti — çünkü model daha az "irrelevant chunk gürültüsü" ile cevaplıyor (LLM-as-judge ile +4 puan).
▶️ Sıradaki ders
6.4 — Otomatik Prompt Distillation. Büyük modelin prompt'unu küçük modele aktarmak — fine-tuning ile %95 maliyet düşüşü, kalite parity. Self-hosting bağlamı.

Sık Sorulan Sorular

OpenAI text-embedding-3-small ($0.02/M) çoğu use case için yeterli. Türkçe-spesifik için BGE-M3 (multilingual, open-source) veya Cohere Embed-Multilingual daha iyi. Modül 12'de Türkçe için karşılaştırma.

Yorumlar & Soru-Cevap

(0)
Yorum yazmak için giriş yap.
Yorumlar yükleniyor...

İlgili İçerikler