SanalOrtam.net
Yapay Zekâ ve Yazılım

Yapay zekâ için birim test: Eval nasıl yazılır?

Hit@k, MRR ve cevap kontrolleriyle C#'ta basit bir eval çalıştırıcısı. Üstelik testimizin bu seriyi yazarken yakaladığı gerçek bir hatayla. Serinin üçüncü yazısı.

Yapay zekâ için birim test: Eval nasıl yazılır?

Şirket içi yapay zekâ asistanı serisi

  1. PostgreSQL ve pgvector ile RAG
  2. C# ile MCP sunucusu
  3. Eval ile yapay zekâyı test etmek (bu yazı)

RAG asistanımızı kurduk, MCP ile de dışarı açtık. Birkaç soru sorduk, cevaplar güzel. Tamam mı?

Değil. Parça boyutunu değiştirdiğinizde, yeni model denediğinizde ya da tek bir satır düzelttiğinizde neyin bozulduğunu "birkaç soru sorarak" anlayamazsınız. Yazılımda bunun için birim test yazarız. Yapay zekâda karşılığı eval.

Testimiz gerçek bir hata yakaladı

Bunu laf olsun diye söylemiyoruz. Bu seriyi hazırlarken ilk yazıdaki asistanda, doküman metnini modele göndermeden önce WebUtility.HtmlEncode ile kaçırıyorduk. Doğru paragraf geliyor, kaynak da gösteriliyordu.

Eval ise şunu verdi (çıktının yalnızca bir kısmı):

KALDI: Yıllık izin talebi kaç gün önceden girilir?
  eksik: 10 gün
  cevap: Yıllık izin talepleri en az 10 gün önceden sisteme girilir...
Test sayısı: 6
Hit@5:      %100
MRR:         1,00
Cevap geçti: %33
SONUÇ: KALDI

HtmlEncode, Türkçe harfleri de kodluyormuş. "gün" kelimesi g&#252;n olmuştu. Arama kusursuzdu (%100), hata aramayla model arasındaki adımdaydı. Yalnızca < ve > karakterlerini kaçıracak şekilde düzelttik:

Test sayısı: 6
Hit@5:      %100
MRR:         1,00
Cevap geçti: %100
SONUÇ: GEÇTİ

Ölçümlerin ayrı ayrı tutulmasının faydası da burada görülüyor: Tek bir "başarı" puanı olsaydı sorunun aramada mı cevapta mı olduğunu anlayamazdık.

Neyi ölçüyoruz?

  • Hit@k: Doğru doküman ilk k sonucun içinde mi?
  • MRR: Doğru doküman kaçıncı sırada? 1. sıra 1,0; 2. sıra 0,5 puan alır, ortalaması raporlanır.
  • Cevap kontrolü: Cevapta olması gereken bilgi geçiyor mu?

Test seti

Test seti koddan daha değerlidir. En iyi kaynak, kullanıcıların gerçekten sorduğu sorulardır. Satır başına bir test olan basit bir JSONL dosyası yeter:

{"soru":"Ürünü kaç gün içinde iade edebilirim?","kaynak":"iade.md","gecmeli":["14 gün"]}
{"soru":"İade onaylanınca para ne zaman karta geri gönderilir?","kaynak":"iade.md","gecmeli":["5 iş günü"]}
{"soru":"Kargo hangi tutarın üzerinde ücretsizdir?","kaynak":"kargo.md","gecmeli":["750"]}
{"soru":"Yurt dışına gönderim yapılıyor mu?","kaynak":"kargo.md","gecmeli":["yapılmamaktadır"]}
{"soru":"Yıllık izin talebi kaç gün önceden girilir?","kaynak":"izin.md","gecmeli":["10 gün"]}
{"soru":"Şirketin hisse senedi temettü oranı nedir?","kaynak":null,"gecmeli":["dokümanlarda yok"]}

Son satıra dikkat: Cevabı dokümanlarda olmayan sorular da eklenmeli. Asistanın doğru cevap vermesi kadar, bilmediğinde uydurmaması da önemlidir.

Çalıştırıcı

Özel bir çerçeveye gerek yok:

using System.Text.Json;
using Asistan.Core;

var dosya = args.ElementAtOrDefault(0) ?? "testler.jsonl";
var adet = int.Parse(args.ElementAtOrDefault(1) ?? "5");
var secenek = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var testler = File.ReadLines(dosya)
    .Where(s => !string.IsNullOrWhiteSpace(s))
    .Select(s => JsonSerializer.Deserialize<Test>(s, secenek)!)
    .ToList();

await using var db = Kurulum.VeriKaynagi(Kurulum.Baglanti);
var asistan = new RagAsistani(Kurulum.Model(), new DokumanDeposu(db));

var sonuclar = new List<Sonuc>();
foreach (var t in testler)
{
    var cevap = await asistan.SorAsync(t.Soru, adet);

    // Arama ölçümü: beklenen kaynak kaçıncı sırada geldi? (0 = hiç gelmedi)
    var sira = t.Kaynak is null ? 0 : cevap.Kaynaklar.FindIndex(k => k.Kaynak == t.Kaynak) + 1;
    var eksik = t.Gecmeli.Where(g => !cevap.Metin.Contains(g, StringComparison.OrdinalIgnoreCase)).ToList();

    sonuclar.Add(new Sonuc(t, sira, eksik.Count == 0));
    if (eksik.Count > 0)
        Console.WriteLine($"KALDI: {t.Soru}\n  eksik: {string.Join(", ", eksik)}\n  cevap: {cevap.Metin}");
}

var aramaTestleri = sonuclar.Where(s => s.Test.Kaynak is not null).ToList();
var isabet = aramaTestleri.Average(s => s.Sira > 0 ? 1.0 : 0.0);
var mrr = aramaTestleri.Average(s => s.Sira > 0 ? 1.0 / s.Sira : 0.0);
var cevapDogruluk = sonuclar.Average(s => s.Gecti ? 1.0 : 0.0);

Console.WriteLine($"Test sayısı: {sonuclar.Count}");
Console.WriteLine($"Hit@{adet}:      {isabet:P0}");
Console.WriteLine($"MRR:         {mrr:F2}");
Console.WriteLine($"Cevap geçti: {cevapDogruluk:P0}");

// Eşiklerin altında kalırsa CI kırmızıya düşsün.
var gecti = isabet >= 0.90 && mrr >= 0.70 && cevapDogruluk >= 0.80;
Console.WriteLine(gecti ? "SONUÇ: GEÇTİ" : "SONUÇ: KALDI");
return gecti ? 0 : 1;

record Test(string Soru, string? Kaynak, string[] Gecmeli);
record Sonuc(Test Test, int Sira, bool Gecti);
dotnet run --project Asistan.Eval -- testler.jsonl 5

Program eşiklerin altında kalınca 1 koduyla çıkar. Bu yüzden CI hattına doğrudan eklenebilir; beklenen cevapları bilerek bozduğumuz bir test setiyle denediğimizde de sonuç "KALDI" ve çıkış kodu 1 oldu.

Eşikler (%90, 0,70, %80) bizim örnek başlangıç değerlerimizdir, evrensel bir standart değil. Önce kendi sisteminizi ölçün, sonra mevcut seviyenin biraz altını alt sınır yapın ve zamanla yükseltin.

Eval nasıl kullanılır?

Asıl değeri karşılaştırmadan gelir. Her seferinde tek bir şeyi değiştirip aynı seti çalıştırın:

  • Parça boyutu 1200 mü, 800 mü?
  • Hibrit arama, yalnızca vektör aramasından gerçekten iyi mi?
  • Modele kaç parça gönderilmeli?
  • Yeni model ya da yeni sistem talimatı bir şeyi bozdu mu?

Sonuçları Git'te tutun; "geçen ay daha mı iyiydi?" sorusunun cevabı elinizde olsun.

Bu yaklaşımın sınırları

  • Metin eşleşmesi kabadır. "14 gün" geçmeli demek, "iki hafta" cevabını yanlış sayar. Gerekirse birden fazla kabul edilebilir ifade tanımlayın.
  • Küçük set yanıltır. Altı testte bir soru sonucu yaklaşık 17 puan oynatır. Burada amaç yöntemi göstermek; gerçek bir sistemde onlarca, tercihen yüzlerce soru gerekir.
  • Modeller sabit davranmaz. temperature = 0 değişkenliği azaltır ama tam tekrar garantisi vermez. Sınırda kalan sonuçlarda seti birkaç kez çalıştırın.
  • Anlam ölçmek için model gerekir. Doğruluk, üslup veya kaynağa sadakat için ikinci bir modeli hakem olarak kullanabilirsiniz. Ama hakem de yanılabilir; önce basit kontrollerle başlayın, örnekleri elle de inceleyin.
  • Kendi testinizi kandırabilirsiniz. Talimatı yalnızca test sorularına göre ayarlarsanız set ezberlenir. Yeni soruları düzenli ekleyin.

Serinin sonu

Üç yazıda elimizde şunlar oldu: Kendi verimizde arama yapan bir asistan, onu başka araçlara açan yetkisi sınırlı bir MCP sunucusu ve değişikliklerin neyi bozduğunu gösteren bir test düzeni. Sıralama önemli: Eval olmadan yapılan her "iyileştirme", yalnızca bir tahmindir.

Bu yazıdaki kodu .NET 10, pgvector 0.8 ve PostgreSQL 18 ile derleyip çalıştırdık. Veritabanı olarak pgvector eklentili PGlite, model yerine ise OpenAI uyumlu sahte bir test sunucusu kullandık. Yani kodun akışı sınandı; gerçek bir modelin Türkçe cevap kalitesi sınanmadı. Bu yüzden yukarıdaki rakamlar performans ölçümü değil; yöntemin ve yakalanan hatanın gerçek çıktısıdır.

Kapak fotoğrafı: Peter Southwood, CC BY-SA 3.0, Wikimedia Commons

TH // yazarTaner HacıoğluYazılım ERP Müdürü

Yorumlar (0)

Yorum yazın

Gönderiler onaylandıktan sonra yayımlanır.

// aynı bölümden

Bunlar da ilginizi çekebilir