Neyi ölçüyoruz, neyi ölçmüyoruz?
RAG değerlendirmesinde iki ayrı soru var: gerekli kaynaklar bulundu mu ve yanıt bu kaynaklardaki gerekli bilgiyi aktardı mı? Akıcı bir yanıt, eksik retrieval sonucunu gizleyebilir. Tersine bütün kaynakların bulunması, yanıtın onları doğru kullandığını göstermez.
Aşağıdaki örnek bu ayrımı yerel olarak test etmek için hazırlandı ve Node.js v24.19.0 üzerinde çalıştırıldı. Üç sentetik doküman, iki soru ve anahtar kelime eşleştirmesi kullanıyor. Embedding, vektör veritabanı veya LLM çağrısı yok. Bu, tam bir RAG sistemi ya da üretim kalitesi benchmark'ı değil; değerlendirme mantığını görünür kılan küçük bir test düzeneği.
Veri kümesi ve beklenen kanıt
Yayın sorusu için iki belge gerekiyor: build belgesi test/build adımını, secrets belgesi gizli anahtar kontrolünü anlatıyor. Medya sorusunda yalnızca images belgesi bekleniyor. Beklenen belge kimlikleri ve yanıt olguları test verisinde açıkça tutuluyor; retrieval fonksiyonu bu beklenen etiketleri okumuyor.
Recall = bulunan ilgili belge / beklenen ilgili belge. Precision = bulunan ilgili belge / getirilen belge. factCoverage burada yalnızca beklenen metin parçalarının yanıt içinde bulunma oranı. Bu son kontrol anlamsal doğruluk veya hallucination ölçümü değildir; farklı ama doğru bir ifadeyi kaçırabilir, bir olumsuzlama içindeki kelimeyi yanlış kabul edebilir.
Örneği çalıştırın
Kaynak bağlantılarındaki rag-evaluation.mjs dosyasını kaydedin; Node.js 24 ile node rag-evaluation.mjs komutunu çalıştırın. Ek paket, hesap, API anahtarı veya ağ erişimi gerekmez. Assertion başarısız olursa komut hata vererek sonlanır.
// Synthetic documents; no LLM, API key, embedding model or external request.
import assert from 'node:assert/strict';
const corpus = [
{ id: 'build', text: 'Yayın öncesi test ve build çalıştırılır.' },
{ id: 'secrets', text: 'Yayın öncesi gizli anahtar kontrolü yapılır.' },
{ id: 'images', text: 'Blog görselleri WebP biçiminde saklanır.' },
];
const cases = [
{ id: 'release', query: 'yayın test build gizli anahtar', expected: ['build', 'secrets'], facts: ['test ve build', 'gizli anahtar'] },
{ id: 'media', query: 'blog görselleri', expected: ['images'], facts: ['WebP'] },
];
const tokens = text => new Set(text.toLocaleLowerCase('tr-TR').match(/[\p{L}\p{N}]+/gu) || []);
function retrieve(query, k) {
const terms = tokens(query);
return corpus.map(doc => ({ ...doc, score: [...tokens(doc.text)].filter(t => terms.has(t)).length }))
.filter(doc => doc.score > 0).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, k);
}
function evaluate(item, docs, answer) {
const hits = docs.filter(doc => item.expected.includes(doc.id)).length;
return { recall: hits / item.expected.length, precision: docs.length ? hits / docs.length : 0,
factCoverage: item.facts.filter(fact => answer.includes(fact)).length / item.facts.length };
}
for (const item of cases) {
for (const k of [1, 2]) {
const docs = retrieve(item.query, k);
// This is evidence concatenation, NOT an LLM-generated answer.
const result = evaluate(item, docs, docs.map(doc => doc.text).join(' '));
if (item.id === 'release') assert.equal(result.recall, k === 1 ? 0.5 : 1);
else assert.equal(result.recall, 1);
assert.equal(result.precision, 1);
console.log(JSON.stringify({ case: item.id, k, retrieved: docs.map(d => d.id), ...result }));
}
}
const release = cases[0];
const fullEvidence = retrieve(release.query, 2);
const incompleteAnswer = fullEvidence[0].text;
const incomplete = evaluate(release, fullEvidence, incompleteAnswer);
assert.equal(incomplete.recall, 1);
assert.equal(incomplete.factCoverage, 0.5);
console.log(JSON.stringify({ case: 'answer-omission', ...incomplete }));
assert.equal(retrieve('pasaport yenileme', 2).length, 0);
console.log('PASS: retrieval, answer omission and out-of-corpus checks');Çalıştırmada elde edilen sonuçlar
Yayın sorusunda k=1 yalnızca build belgesini buldu: recall 0.5, precision 1, factCoverage 0.5. k=2 build ve secrets belgelerini buldu: üç oran da 1 oldu. Medya sorusunda her iki k değerinde yalnızca ilgili images belgesi getirildi. Bunlar bu küçük veri kümesinin çıktıları; genel doğruluk yüzdesi değil.
answer-omission testi tam kanıtı getirdikten sonra yanıtı bilerek ilk belgeyle sınırlıyor. Recall 1 kalırken factCoverage 0.5'e düşüyor. Böylece retrieval başarısıyla yanıt bütünlüğünün neden ayrı kontrol edilmesi gerektiğini yeniden üretebiliyoruz. Kaynak dışı 'pasaport yenileme' sorgusunda sonuç sayısının sıfır olduğu ayrıca doğrulanıyor; gerçek ürünün yanıt vermekten kaçınması ayrıca tasarlanmalı.
{"case":"release","k":1,"retrieved":["build"],"recall":0.5,"precision":1,"factCoverage":0.5}
{"case":"release","k":2,"retrieved":["build","secrets"],"recall":1,"precision":1,"factCoverage":1}
{"case":"answer-omission","recall":1,"precision":1,"factCoverage":0.5}Gerçek bir sisteme taşırken
Bu örnek iki soruyla 'daha büyük k her zaman daha iyi' sonucunu vermez. Üretim değerlendirmesinde farklı kullanıcı soruları, ilgili/ilgisiz belgeler, eksik kaynaklar ve çelişkiler bulunmalı. k artarken maliyet, gecikme ve gereksiz bağlam da ölçülmeli. Beklenen kanıt etiketleri ekip tarafından gözden geçirilmeli; test için hazırlanmış yanıtlara göre retriever ayarlayıp aynı testte başarı ilan edilmemeli.
Gerçek LLM yanıtlarını bu metin eşleştirmesiyle tek başına kabul etmeyin. Kaynağa bağlılık, cevaplanamayan soruda kaçınma ve alan riskini insan incelemesiyle ayrıca değerlendirin. İzlerde belge kimliği ve sürümü saklanabilir; özel belge metni veya anahtarlar herkese açık loglara taşınmamalı.
İlk değerlendirme kaydı
Soru kimliği, corpus sürümü, beklenen kanıt kimlikleri, getirilen kimlikler/sıralar, k, yanıt ve ayrı değerlendirme sonuçlarını birlikte kaydedin. Önce hangi katmanın başarısız olduğunu belirleyin; sonra yalnızca o katmanda değişiklik yaparak aynı soruları yeniden çalıştırın. Aşağıdaki debugging rehberi değişiklik başına tek hipotez yaklaşımını tamamlıyor.
Kaynak: Çalıştırılabilir değerlendirme dosyası
Kaynak: RAG araştırma makalesi — kavramsal arka plan
Kaynak: Node.js assertion belgeleri
