Mert Özen Her Satırda Daha İleri
Yapay Zeka Prompt Engineering Uygulamada — #03

Classification ve Routing Prompt'ları: Etiket Seti, Etiket Açıklamaları, Few-shot Seçimi, Belirsiz Vakalar ve Güven

Mert Özen 15 Kas 2026 11 dk 7 görüntülenme
Classification ve Routing Prompt'ları: Etiket Seti, Etiket Açıklamaları, Few-shot Seçimi, Belirsiz Vakalar ve Güven

Classification prompt'unun doğruluğunu en çok karşılıklı dışlayan etiket tanımları, şemada enum ve bir "insana aktar" etiketi belirler; modelin kendi güven beyanı yanlışları ayırmaz. Gül'ün 24 ticket'lık setinde tanımlar skoru 14/24'ten 22/24'e çıkardı, few-shot bir şey eklemedi, qwen3:4b etiket sırası ters çevrilince üç ticket'ı değiştirdi ve iki küçük modelin uyuşması yanlışların ikisini de insana gönderdi.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; Microsoft.Extensions.AI 11.0.0, OllamaSharp 5.5.2 ve xUnit v3 4.0.3 kullanılmıştır. Model çıktıları yerelde Ollama 0.34.4 üzerinde çalışan iki küçük açık modelden alınmıştır: gemma3:4b (4,3 milyar parametre) ve qwen3:4b (4 milyar parametre), ikisi de Q4_K_M; bulut sağlayıcıyla koşulmamıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) müşteri hizmetlerine günde yüzlerce mesaj geliyor ve Gül her sabah önce bunları ekiplere dağıtıyor: iade, kargo, ödeme ve kurumsal satış. Bu yazıda o dağıtımı bir classification prompt'una veriyoruz. Ticket'lardan biri şu: "İade ettiğim kitabın parası 10 gündür hesabıma yatmadı."

Bu ticket iade ekibinin mi, ödeme ekibinin mi? Paradan bahsediyor, ama para bir iadenin parası. Etiket listesinde yalnız ekip adları varsa model hangisini seçer? Etiketleri prompt'ta ters sırayla yazsan cevap değişir mi? Modelden bir de "ne kadar eminsin" diye 0 ile 1 arasında bir sayı istesen, yanlış cevaplarda bu sayı düşük mü çıkar? Üç tahmin yap; üçünü de ölçeceğiz.

Etiket Seti Bir Sözleşmedir

Classification prompt'unun asıl işi model seçmek ya da talimat cümlesini parlatmak değil, etiket setini tanımlamak. Her etiket bir ekibe gidiyor; ekipler arasındaki sınır net değilse model de net olamaz. İyi bir etiket setinin üç özelliği var: etiketler karşılıklı dışlayan tanımlara sahip, sınır vakaları tanımın içinde açıkça yazıyor ve hiçbir etiketin tutmadığı durum için bir çıkış etiketi var. Bizim set şöyle:

// Her ticket tam olarak bir ekibe gider; sınırlar tanımın parçası
public static readonly IReadOnlyList<LabelDefinition> All =
[
    new("Returns", "The customer wants to return or exchange a book they received (damaged, wrong, missing pages, changed their mind), or asks about the status of a return or of a refund for a returned book."),
    new("Shipping", "Delivery of an individual order that has not arrived yet: tracking, delays, courier problems, changing the address before dispatch. A book that arrived damaged is Returns, not Shipping."),
    new("Payments", "Charging and invoicing of an individual order: double charges, declined cards, installments, invoice details. A refund for a returned book is Returns, not Payments."),
    new("Institutional", "Any message from a school, library or company about an institutional order: quotes for 20 or more copies, institutional discount, purchase approval, and also delivery, damage or invoices of an institutional order."),
    new(Human, "Anything else: complaints about staff, legal threats, several unrelated problems in one message, unclear or empty messages, and questions no team above owns."),
];

Tanımlardaki her "X değil, Y" cümlesi Gül'le bir kararın kaydı. İade edilen kitabın parası Returns'e gidiyor, çünkü iadeyi açan ekip para iadesini de takip ediyor. Hasarlı gelen kitap Shipping değil Returns, çünkü çözüm yeni bir gönderi değil iade akışı. Kurumsal bir siparişin kargosu, hasarı ve faturası da Institutional'a gidiyor; Hakan'ın ekibi kurumsal müşteriyle baştan sona tek muhatap olmak istiyor. Human etiketi bir başarısızlık değil, tasarımın parçası: kaba temsilci şikâyeti, hukuki süreç tehdidi ve aynı mesajda üç ayrı sorun bir insanın kararı gerektiriyor.

Test seti 24 ticket: her ekipten dört beş tane, üstüne bilerek seçilmiş sınır vakaları. İade edilen kitabın parası (t02), ezik kargo kutusundaki yırtık kitap (t03), kütüphanenin 120 adetlik siparişinin kargosu (t15), kurumsal indirimin uygulanmaması (t17), okula gelen hasarlı kitaplar (t18) ve kargo, ödeme ve hasar şikâyetini aynı mesajda toplayan müşteri (t21).

v1: Yalnız Etiket Adları

İlk prompt yalnız ekip adlarını sayıyor ve JSON istiyor; etiket serbest bir string:

System: "Route the Bookstore customer ticket to one team: Returns, Shipping, Payments, Institutional or Human. Reply with JSON: {\"label\": \"...\"}",
dotnet run --project src/Bookstore.Prompts.Lab -- ticket-routing v1
FAIL t02    expected Returns, got Payments
FAIL t08    expected Shipping, got Kargo takibi için destek ekibine ilet...
FAIL t12    expected Payments, got Please provide me with the invoice de...
FAIL t15    expected Institutional, got Shipping
FAIL t16    expected Institutional, got Kurumsal siparişleri onaylamak için l...
FAIL t17    expected Institutional, got Payments
FAIL t21    expected Human, got Returns
FAIL t22    expected Human, got Yazar imza günüyle ilgili bilgileri k...
FAIL t23    expected Human, got Returns
FAIL t24    expected Human, got Returns
14/24 passed (58%)

expected \ got  Retur Shipp Payme Insti Human     ?
Returns             4     .     1     .     .     .
Shipping            .     3     .     .     .     1
Payments            .     .     3     .     .     1
Institutional       .     1     1     2     .     1
Human               3     .     .     .     2     1
accuracy 14/24

Hatalar iki türlü. Dört ticket'ta model etiket yerine müşteriye cevap yazmış: label alanında "Kargo takibi için destek ekibine iletin..." gibi cümleler var. Matrix'in son sütunu (?) bunları sayıyor; etiket serbest string olunca setin dışına çıkmak mümkün. Diğer hatalar sınırda: kurumsal siparişin kargosu Shipping'e, kurumsal indirim Payments'a gitmiş; "asdfgh" ve teşekkür mesajı Returns olmuş. Confusion matrix (gerçek etiketle tahmin edilen etiketin çaprazlandığı tablo) toplam skorun söylemediğini söylüyor: Human satırındaki üç ticket Returns'e kaçıyor, yani model emin olmadığında en sık gördüğü etikete gidiyor. Confusion matrix'in ve precision/recall'ün ayrıntısı Makine Öğrenmesine Giriş serisinde.

v2: Tanımlar ve Şemada Enum

v2'de iki şey değişiyor. Tanımlar prompt'a giriyor ve etiket artık JSON schema'da bir enum. Şemayı prompt'taki etiket sırasıyla aynı sırada üreten küçük bir builder var; sırayı sonra test edeceğiz:

// Şemadaki etiket listesi prompt'taki sırayı izler
static JsonElement Schema(IEnumerable<LabelDefinition> labels, bool askConfidence)
{
    Dictionary<string, object> properties = new()
    {
        ["label"] = new { type = "string", @enum = labels.Select(l => l.Name).ToArray() },
    };
    List<string> required = ["label"];
    if (askConfidence)
    {
        properties["confidence"] = new { type = "number", minimum = 0, maximum = 1 };
        required.Add("confidence");
    }
    return JsonSerializer.SerializeToElement(new { type = "object", properties, required, additionalProperties = false });
}
dotnet run --project src/Bookstore.Prompts.Lab -- ticket-routing v2
FAIL t02    expected Returns, got Payments
FAIL t21    expected Human, got Payments
22/24 passed (92%)

expected \ got  Retur Shipp Payme Insti Human     ?
Returns             4     .     1     .     .     .
Shipping            .     4     .     .     .     .
Payments            .     .     4     .     .     .
Institutional       .     .     .     5     .     .
Human               .     .     1     .     5     .
accuracy 22/24

14/24'ten 22/24'e. Artı sekizin hepsi tanımlardan ve enum'dan geliyor; prompt'un tonuna, rol cümlesine dokunmadık. Kalan iki hata tam beklediğimiz yerde: t02 (iade edilen kitabın parası) tanım açıkça "Returns, not Payments" dediği hâlde Payments, t21 (üç sorun birden) Payments.

v3: Few-shot Örneklerini Sınırdan Seçmek

Few-shot örnek seçerken içgüdü, her etiketten tipik bir örnek koymak. Ama tipik örnekleri model zaten doğru sınıflandırıyor; matrix bunu gösterdi. Örneklerin işe yaradığı yer sınırlar. v3'te dört örnek var, dördü de bir sınırı gösteriyor ve hiçbiri test setinde değil:

// Test setinde olmayan sınır vakaları
static readonly (string Ticket, string Label)[] Examples =
[
    ("Gönderdiğim iade kargosu size ulaştı mı?", "Returns"),
    ("Paket geldi ama içinden başka bir kitap çıktı.", "Returns"),
    ("Kurumsal siparişimizin faturası hâlâ gelmedi.", "Institutional"),
    ("İndirim kodum çalışmadı, ayrıca kargom da gecikti.", "Human"),
];
dotnet run --project src/Bookstore.Prompts.Lab -- ticket-routing v3
FAIL t02    expected Returns, got Payments
FAIL t21    expected Human, got Returns
22/24 passed (92%)

Skor aynı kaldı. t21 bu kez Returns'e gitti, t02 inatla Payments. Bu da bir sonuç: bu model ve bu sette örnekler tanımların üstüne bir şey eklemedi. Few-shot'ın her zaman işe yaradığını varsaymak yerine ölçmek gerekiyor; örnek eklemek token ve maliyet demek, katkısı ölçülmüyorsa çıkarılabilir.

Etiket Sırası Sonucu Değiştirir mi?

LLM'lerde bilinen bir yanlılık, seçeneklerin sırasına duyarlılık: model listede önce gelen ya da en son okuduğu seçeneğe kayabiliyor. Bunu ölçmenin en basit yolu, aynı prompt'u etiketler ters sırada olacak şekilde koşup kaç ticket'ın etiketinin değiştiğini saymak. Builder'daki reverse parametresi hem tanım listesinin hem şemadaki enum'un sırasını çeviriyor, başka hiçbir şeye dokunmuyor:

dotnet run --project src/Bookstore.Prompts.Lab -- ticket-routing order
normal 22/24, reversed 22/24, flips 0

BOOKSTORE_LLM_MODEL=qwen3:4b dotnet run --project src/Bookstore.Prompts.Lab -- ticket-routing order
flip t02: Returns / Payments
flip t03: Returns / Shipping
flip t12: Payments / Institutional
normal 24/24, reversed 21/24, flips 3

gemma3:4b'de sıra hiçbir ticket'ı değiştirmedi. qwen3:4b normal sırada setin tamamını doğru bildi, ters sırada üç ticket'ın etiketi değişti ve üçü de sınır vakası: iade edilen kitabın parası, hasarlı kargo, fatura. Yani qwen3:4b'nin 24/24'ü kısmen etiket sırasına bağlı. Sıra yanlılığı model başına farklı; bir modelde sıfır çıkması diğerinde de sıfır olacağı anlamına gelmiyor. Model değiştirdiğinde bu testi tekrar koşmak gerekiyor.

Confidence: Modelin Kendi Beyanı

Akla ilk gelen güvenlik önlemi, modelden bir güven skoru istemek ve düşük skorlu ticket'ları insana göndermek. Şemaya confidence alanını ekleyip v3'ü koşuyoruz:

dotnet run --project src/Bookstore.Prompts.Lab -- ticket-routing confidence
accuracy 22/24
mean confidence when right: 0.93
mean confidence when wrong: 0.90
wrong answers with confidence >= 0.9: 2
distinct confidence values: 0.8, 0.9, 0.95, 1

Doğru cevaplarda ortalama 0.93, yanlışlarda 0.90. İki yanlış cevabın ikisi de 0.9 ve üstünde. Model 24 ticket için yalnız dört farklı değer kullanmış. Bu sayı bir olasılık değil; modelin "güven" kelimesine verdiği metinsel bir cevap. Eşik koysan, 0.9'un altını insana göndersen, yanlışların hiçbirini yakalamazdın. Bazı sağlayıcılar token olasılıklarını (logprobs) döndürüyor ve bu daha anlamlı bir sinyal olabilir; ama o da kalibre edilmeden eşik olarak kullanılmamalı ve her sağlayıcıda yok.

İki Aşamalı Doğrulama: İki Küçük Model

Daha güvenilir bir sinyal, birbirinden bağımsız iki tahminin uyuşması. Aynı prompt'u iki farklı küçük modelle koşuyoruz; iki model aynı etikette uyuşursa ticket otomatik yönleniyor, uyuşmazsa Gül'e gidiyor:

// İki küçük model bağımsız yönlendirir; yalnız uyuştukları ticket otomatik gider
public static async Task CrossCheckAsync(EvalRunner first, EvalRunner second)
{
    IReadOnlyList<TestCase<string>> cases = Cases();
    PromptVersion prompt = TicketRoutingPrompts.Build(withExamples: true);
    EvalReport a = await RunAsync(first, prompt, print: false);
    EvalReport b = await RunAsync(second, prompt, print: false);

    int auto = 0, autoWrong = 0, toPerson = 0, wrongCaught = 0;
    foreach ((CaseResult x, CaseResult y) in a.Results.Zip(b.Results))
    {
        string expected = cases.Single(c => c.Id == x.Id).Expected;
        string labelX = Parse(x.Output)?.Label ?? "?";
        string labelY = Parse(y.Output)?.Label ?? "?";
        if (labelX == labelY)
        {
            auto++;
            autoWrong += labelX == expected ? 0 : 1;
            continue;
        }
        toPerson++;
        wrongCaught += labelX == expected ? 0 : 1;
        Console.WriteLine($"disagree {x.Id}: {labelX} / {labelY} (expected {expected})");
    }
    Console.WriteLine($"first model {a.Passed}/{cases.Count}, second model {b.Passed}/{cases.Count}");
    Console.WriteLine($"agreed {auto} (wrong {autoWrong}), sent to a person {toPerson} (first model was wrong in {wrongCaught})");
}
dotnet run --project src/Bookstore.Prompts.Lab -- ticket-routing cross-check
disagree t02: Payments / Returns (expected Returns)
disagree t21: Returns / Human (expected Human)
first model 22/24, second model 24/24
agreed 22 (wrong 0), sent to a person 2 (first model was wrong in 2)

İki model 22 ticket'ta uyuştu ve 22'si de doğru; uyuşmadıkları iki ticket, gemma3:4b'nin yanlış bildiği iki ticket'ın ta kendisi. 24 vakada bu kesin bir kanıt değil, ama confidence alanının veremediği bir sinyal: yanlışlar uyuşmazlıkta toplanıyor. Bedeli iki model çağrısı; küçük modellerde bu yerel kurulumda ticket başına toplam bir saniye civarında. Alternatif, aynı modeli iki farklı etiket sırasıyla koşmak; qwen3:4b'nin sıra testindeki üç flip'i de bu yöntemle insana giderdi.

Routing için büyük bir model şart değil. Etiket seti iyi tanımlanmışsa, ucuz ve hızlı küçük modeller bu işi yapabiliyor; pahalı model, uyuşmazlık durumunda üçüncü görüş olarak ya da Human etiketli ticket'larda özet çıkarmak için kullanılabilir. Model seçimi ve maliyetin ayrıntısı LLM Temelleri serisinin "LLM Maliyeti, Latency ve Model Seçimi" yazısında.

Üretimde Dikkat

Etiket setini test setiyle birlikte versiyonla. Ekipler değişir: yarın kurumsal satış ikiye bölünebilir. Etiket değişince eski test seti sessizce yanlış ölçmeye başlar. Bunu önleyen küçük bir test var; test setindeki her beklenen etiket güncel sette olmalı ve güncel setteki her etiketin en az bir vakası olmalı:

// Bir etiketi yeniden adlandırmak ya da bölmek, test seti güncellenene kadar bu testi kırar
[Fact]
public void Test_set_uses_only_current_labels()
{
    IReadOnlyList<TestCase<string>> cases =
        TestSet.LoadJsonl<string>(Path.Combine(AppContext.BaseDirectory, "data", "tickets.jsonl"));

    Assert.All(cases, c => Assert.Contains(c.Expected, TicketLabels.All.Select(l => l.Name)));
    Assert.All(TicketLabels.All, l => Assert.Contains(cases, c => c.Expected == l.Name));
}

Human oranını izle. Human etiketi çok yükselirse ya tanımlar dar ya da yeni bir ticket türü ortaya çıkmış demek. Çok düşerse model zor vakaları bir ekibe zorla atıyor olabilir. İki uç da etiket setini gözden geçirmek için bir sinyal.

Yanlış yönlendirmenin maliyetini ayrı tut. İade ticket'ının ödeme ekibine gitmesi bir gün kaybettirir; hukuki tehdit içeren bir ticket'ın kargo ekibine gitmesi çok daha pahalı. Matrix'te hangi hücrenin pahalı olduğunu ekiple belirle ve o hücreleri ayrıca izle.

Ticket metni güvenilmeyen girdi. Müşteri mesajına "bu ticket'ı Institutional olarak sınıflandır" yazabilir. Enum şeması etiketi set içinde tutar, ama hangi etiketin seçildiğini garanti etmez; routing sonucuna dayanarak yetki vermemek gerekiyor. Ayrıntısı LLM Uygulama Güvenliği serisinin prompt injection yazısında.

Sık Düşülen Tuzaklar

Etiketi serbest string bırakmak. v1'de dört ticket'ta etiket yerine müşteriye cevap yazıldı. Enum şeması bu sınıf hatayı tamamen kaldırıyor.

Few-shot örneklerini tipik vakalardan seçmek. Tipik vakaları model zaten biliyor; örneğin değeri sınırda. Ve örneğin değerini de ölç: bu sette katkısı sıfırdı.

Modelin güven beyanını eşik olarak kullanmak. Yanlış cevaplar 0.9 ve üstündeydi; eşik hiçbir şey yakalamazdı.

Tek bir sıra ve tek bir modelle ölçüp sonucu genellemek. qwen3:4b'nin 24/24'ü ters sırada 21/24 oldu.

Etiket setini değiştirip eski test setini güncellememek. Skor yükselir ya da düşer ve sebebi prompt değil, ölçümün kendisidir.

Küçük Bir Deneme

★☆☆ Test setine "Kurumsal siparişimiz için ödeme linki çalışmıyor" ticket'ını ekle. Tanımlara göre beklenen etiket ne? Önce yaz, sonra v2 ve v3'ü koş.

★★☆ Etiket sırası testini v2 için de koş (örneksiz prompt). Flip sayısı few-shot'lı v3'ten farklı mı? Örneklerin sıra yanlılığını azaltıp azaltmadığını iki modelde karşılaştır.

★★★ CrossCheckAsync'e üçüncü bir oy ekle: aynı modelin ters sıralı koşusu. Kuralı "üç oyun üçü de aynıysa otomatik" yap ve otomatik yönlenen ticket oranı ile otomatik yönlenip yanlış giden ticket sayısını iki kurala göre tabloda karşılaştır.

Sıradaki yazıda metin üreten görevlere geçiyoruz: kampanya metnini push bildirimine kısaltmak, kitap açıklamalarını terim sözlüğüyle İngilizceye çevirmek ve aynı olay raporunun yönetici ve müşteri için ayrı özetini çıkarmak. Sınıflandırmada doğru cevap tekti; orada doğruluğu ölçmek için yeni kontroller yazmamız gerekecek.