Tüm yazılara dön
·4 dk okuma

LLM çıktısını koda bağlamak: şema, doğrulama ve onarım döngüsü

Bir dil modelini uygulamanıza bağladığınızda ilk gerçek soru şudur: modelin yanıtı koda nasıl girecek? Serbest metni ekranda göstermek kolaydır. Ama yanıtın bir sınıflandırma sonucu, bir sipariş taslağı ya da başka bir servise gidecek bir komut olması gerekiyorsa, metni ayrıştırmak zorundasınız. Orada işler hızla kırılganlaşır.

Bu yazıda modelden yapılandırılmış çıktı alırken izlediğim yaklaşımı anlatıyorum: şemayı bir sözleşme olarak tasarlamak, çıktıyı sunucuda her zaman doğrulamak, başarısızlıkta sınırlı sayıda onarım denemek ve hâlâ olmuyorsa güvenli bir yola düşmek. Örnekler C# ile, ama fikir her dilde aynı.

Model çıktısı güvenilmeyen bir girdidir

En önemli zihinsel değişiklik şu: modelin döndürdüğü JSON, kullanıcıdan gelen bir form verisi kadar güvenilmeyen girdidir. Birçok sağlayıcı şemaya uygun çıktı üretmeyi destekler ve bu, hata oranını belirgin biçimde düşürür. Yine de bu özelliğe yaslanıp doğrulamayı atlamak yanlış olur. Şema desteği anlamsal doğruluğu garanti etmez; model geçerli bir JSON üretip içine yanlış bir değer koyabilir. Ayrıca bağlantı kesilmesi, kesilmiş yanıt ya da sağlayıcı değişikliği gibi durumlarda biçim bozulabilir.

Bu yüzden iki katmanlı düşünüyorum: model tarafında çıktıyı şemaya yönlendirmek, uygulama tarafında ise gelen her şeyi sanki dışarıdan gelmiş gibi doğrulamak.

Şemayı sözleşme olarak tasarlamak

İyi bir şema modelin işini kolaylaştırır. Birkaç pratik kural:

  • Alanları az tutun. Her ek alan, hata yapılabilecek bir yer daha demektir.
  • Serbest metin yerine kapalı kümeler (enum) kullanın. "Öncelik" için low, medium, high gibi.
  • Alan adlarını ve açıklamalarını anlamlı yazın; model alan adından da yönlendirilir.
  • "Bilmiyorum" için açık bir yol bırakın: boş değer ya da unknown gibi bir seçenek. Aksi hâlde model bir şey uydurmaya zorlanır.

Örnek olarak bir destek talebini sınıflandıran bir kayıt:

public enum Priority { Low, Medium, High }

public sealed record TicketTriage(
    string Category,
    Priority Priority,
    bool NeedsHuman,
    string? Reason);

NeedsHuman alanı küçük ama değerli bir güvenlik vanasıdır: model emin değilse bunu açıkça söyleyebilir ve siz de otomatik akışı durdurursunuz.

Sunucu tarafında doğrulama

Ayrıştırma ve doğrulama ayrı adımlardır. Önce JSON'un biçimsel olarak geçerli olduğundan, sonra değerlerin iş kurallarına uyduğundan emin olun:

public static bool TryParse(string raw, out TicketTriage? result, out string error)
{
    result = null;
    error = "";

    try
    {
        result = JsonSerializer.Deserialize<TicketTriage>(raw, Options);
    }
    catch (JsonException ex)
    {
        error = $"Geçersiz JSON: {ex.Message}";
        return false;
    }

    if (result is null) { error = "Boş yanıt"; return false; }

    if (!Allowed.Contains(result.Category))
    {
        error = $"Bilinmeyen kategori: {result.Category}";
        return false;
    }

    return true;
}

Burada iki ayrıntı önemli. Birincisi, kategori gibi alanları kendi izin verilen listenize karşı kontrol ediyorsunuz; şema bunu zaten söylese bile. İkincisi, hata mesajı insan için okunabilir ve model için de kullanışlı: bir sonraki adımda bunu modele geri vereceğiz.

Enum için JsonStringEnumConverter kullanmayı ve bilinmeyen değerlerde hata vermeyi unutmayın; sessizce sayısal varsayılana düşmek, fark edilmesi en zor hatalardan biridir.

Sınırlı onarım döngüsü

Doğrulama başarısız olduğunda ilk refleks aynı isteği tekrarlamaktır. Bir ya da iki deneme makuldür, ama körlemesine tekrar yerine hatayı modele bildirmek daha etkilidir: önceki yanıtı ve doğrulama hatasını ekleyip düzeltilmiş çıktıyı isteyin.

for (var attempt = 0; attempt < MaxAttempts; attempt++)
{
    var raw = await model.CompleteAsync(messages, ct);

    if (TryParse(raw, out var ok, out var error))
        return ok;

    messages.Add(Assistant(raw));
    messages.Add(User($"Çıktı doğrulanamadı: {error}. Yalnızca şemaya uygun JSON döndür."));
}

return null; // geri düşüş yoluna geç

Üst sınır şarttır. Sınırsız döngü hem maliyeti hem gecikmeyi kontrolden çıkarır. Ben genellikle toplamda iki üç denemeyle yetiniyorum; üçüncü denemede düzelmeyen bir girdi çoğunlukla belirsiz ya da modelin kapsamı dışındadır. Her denemeyi ve nedenini günlüğe yazın; hangi alanların sık bozulduğu, şemayı iyileştirmek için en iyi ipucudur.

Geri düşüş: başarısızlık da bir sonuçtur

Onarım denemeleri de tükenirse sistemin ne yapacağı baştan tasarlanmış olmalı. Seçenekler bağlama göre değişir:

  • İşi bir insan kuyruğuna bırakmak.
  • Daha basit, kural tabanlı bir yönteme dönmek.
  • Kullanıcıya açık bir mesajla "şu an otomatik işlenemedi" demek.

Hangisini seçerseniz seçin, kural şudur: doğrulanmamış çıktıyı asla devam ettirmeyin. Yanlış ama biçimsel olarak geçerli bir komutun başka bir servise gitmesi, hiç komut göndermemekten çok daha pahalıdır. Yan etkisi olan her eylemden önce ayrıca bir yetki ve sınır kontrolü koyun; modelin "yap" demesi, yapılabilir olduğu anlamına gelmez.

Test ve izleme

Yapılandırılmış çıktı hattını diğer kodlar gibi test edin. Ayrıştırıcı ve doğrulayıcı için sıradan birim testleri yazın: eksik alan, fazla alan, yanlış tür, bilinmeyen enum değeri, kesilmiş JSON. Gerçek model çağrısı gerektirmeyen bu testler hızlıdır ve regresyonları yakalar.

Model davranışı için ise küçük bir değerlendirme kümesi tutun: gerçek örnek girdiler ve beklenen sonuçlar. Prompt ya da model değiştirdiğinizde bu kümeyi çalıştırıp doğrulama başarı oranının ve doğruluğun düşmediğine bakın. Üretimde de ilk denemede geçen yanıt oranını, onarım sayısını ve geri düşüş sıklığını ölçün. Bu üç sayı, hattın sağlığını özetler.

Kapanış

Modelden yapılandırılmış çıktı almak, "JSON iste ve umut et" işi değil; küçük bir veri işleme hattı kurmaktır. Şemayı sade ve kapalı tutun, gelen her yanıtı sunucuda doğrulayın, hatayı modele geri bildirerek sınırlı sayıda onarın ve olmuyorsa güvenli bir yola düşün. Bu dört adım, model ne kadar değişirse değişsin uygulamanızın öngörülebilir kalmasını sağlar.