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

Extraction Prompt'ları: Serbest Metinden Yapılandırılmış Veri, Eksik Alan, Belirsizlik ve "Bilmiyorum"

Mert Özen 15 Kas 2026 11 dk 6 görüntülenme
Extraction Prompt'ları: Serbest Metinden Yapılandırılmış Veri, Eksik Alan, Belirsizlik ve "Bilmiyorum"

Extraction prompt'unda bilinmeyebilecek her alan nullable olmalı, değerin kaynağı alıntıyla kanıtlanmalı ve tarih gibi hesaplar koda bırakılmalı; JSON schema yalnız şekli garanti eder. Nevin Hanım'ın sipariş e-postalarında zorunlu alanlı v1 sekizde bir doğru çıktı ve "Ekte" gibi ISBN'ler uydurdu; nullable şema, kaynak alıntısı, tek örnek, Türkçe tarih resolver'ı ve validator ile kalemleri 12/12, tarihleri 8/8'e çıkarıyoruz.

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 küçük açık modellerden alınmıştır: gemma3:4b (4,3 milyar parametre) ve karşılaştırma için qwen3:4b (4 milyar parametre), ikisi de Q4_K_M; bulut sağlayıcıyla koşulmamıştır.

Önce Tahmin Et

Nevin Hanım bir okulun satın alma sorumlusu ve sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) kurumsal müşterisi. Siparişlerini formdan değil, e-postayla veriyor. Hakan'ın kurumsal satış ekibi her e-postayı okuyup Kurumsal Sipariş ekranına elle giriyor. İstenen şey basit görünüyor: e-postadan kitapları, adetleri ve teslim tarihini çıkar, formu doldur, Hakan yalnız onaylasın.

Sent: 2026-11-12 (Thursday)

Merhaba,
9. sınıflar için 40 adet Dune (ISBN 9780441013593) ve 35 adet Emma istiyoruz. Kitapların gelecek salıya kadar okulda olması gerekiyor.

Nevin Hanım
Lale Koleji Satın Alma

Bu e-postada Emma'nın ISBN'i (kitapların uluslararası standart numarası) yok. JSON schema'da isbn alanı zorunlu bir string ise model ne yazar? Boş string mi, Dune'un numarasını mı, yoksa hiç var olmayan bir numara mı? "Gelecek salı" hangi gün? Model bunu hesaplayabilir mi, yoksa hesabı koda mı bırakmalısın? Tahminini not et; aşağıda sekiz e-postalık bir setle gerçek cevapları göreceğiz.

v1: Şema Doğru, Değer Yanlış

İlk deneme, extraction için en sık görülen kurulum: bir record, ondan üretilen JSON schema ve tek cümlelik bir talimat. Alanların hepsi zorunlu ve null olamaz:

// v1 şeması: her alan zorunlu ve null olamaz
public sealed record OrderRequestV1(List<RequestedItemV1> Items, string DeliveryDate);

public sealed record RequestedItemV1(string Title, string Isbn, int Quantity);

public static readonly PromptVersion V1 = new(
    Task: "order-extraction",
    Version: "v1",
    System: "Extract the book order from the email as JSON.",
    BuildUser: email => email,
    Format: ChatResponseFormat.ForJsonSchema<OrderRequestV1>());

Modelin bugünün tarihini bilmesinin bir yolu yok, bu yüzden e-postanın gönderildiği gün her zaman girdinin başında, Sent: satırında gidiyor. Test setinde Nevin Hanım'dan sekiz e-posta var; her biri ayrı bir zorluk taşıyor: adedi "birkaç" diye yazılmış bir kitap, "geçen yılki siparişi aynen tekrarlayalım" diyen bir talep, "ISBN'leri ekte" notu, 40'tan 45'e güncellenen bir adet, yazıyla "yirmi beş adet" ve tireli yazılmış bir ISBN. Scorer her alanı ayrı sayıyor; e-posta bazında tek bir PASS/FAIL, prompt'un nerede zayıf olduğunu göstermez.

dotnet run --project src/Bookstore.Prompts.Lab -- order-extraction v1
order-extraction v1 | gemma3:4b | 8 cases
FAIL ox-01  Emma: isbn null -> 9780441013593; date (model) 2026-11-17 -> null
FAIL ox-02  Middlemarch: quantity null -> 3; date (model) null -> 2026-11-22
FAIL ox-03  Frankenstein: quantity null -> 1
PASS ox-04
FAIL ox-05  date (model) 2026-11-25 -> null
FAIL ox-06  Emma: isbn null -> 25; date (model) 2026-12-15 -> null
FAIL ox-07  The Hobbit: quantity null -> 0; date (model) null -> 2026-11-30
FAIL ox-08  date (model) 2026-12-02 -> 2026-11-30
1/8 passed (13%)

items found       12/12 (extra: 0)
quantity correct  9/12 (invented: 3)
isbn correct      10/12 (invented: 2)
date by model     2/8

Kitapların hepsi bulunmuş; sorun bulunan değerlerde. Ham çıktılardan üçü durumu iyi anlatıyor:

{"items": [{"title": "Jane Eyre", "isbn": "Ekte", "quantity": 30}, {"title": "Pride and Prejudice", "isbn": "Ekte", "quantity": 30}], "deliveryDate": "Acelemiz yok"}
{"items": [{"title": "Emma", "isbn": "25 adet", "quantity": 25}], "deliveryDate": "15 Aralık'tan önce teslim edilmesi gerekiyor"}
{"items": [{"title": "Middlemarch", "isbn": "978-0-14-143954-9", "quantity": 20}, {"title": "The Hobbit", "isbn": "N/A", "quantity": 0}], "deliveryDate": "2026-11-30"}

Her biri şemaya uyuyor. isbn bir string olmalıydı, model de string yazdı: "Ekte", "25 adet", "N/A". quantity bir integer olmalıydı: adedi belli olmayan The Hobbit için 0, ox-02'deki "birkaç" için 3, "geçen yılki gibi" için 1. ox-01'de Emma'ya Dune'un ISBN'i kopyalanmış. Null olamayan bir alan, bilgi yoksa modeli bir şey uydurmaya zorluyor; şema "boş bırak" seçeneği vermediği için model en makul görünen değeri yazıyor. Tarih alanı da aynı durumda: model bazen ISO (uluslararası standart; burada yyyy-MM-dd biçimi) formatında bir tarih, bazen "Acelemiz yok" gibi bir cümle yazmış. Sekiz tarihin yalnız ikisi doğru.

v2: Nullable Alanlar, Alan Açıklamaları ve Kaynak Alıntısı

v2'de şemayı değiştiriyoruz. Üç kural var: bilgi yoksa alan null; her alanın ne anlama geldiği şemada yazıyor; her kalem, geldiği cümleyi kelimesi kelimesine taşıyor. Description attribute'ları Microsoft.Extensions.AI'ın ürettiği JSON schema'ya description olarak giriyor, yani alan açıklaması model için prompt'un bir parçası oluyor:

// v2 şeması: nullable alanlar, açıklamalar ve her kalem için kaynak alıntısı
public sealed record OrderRequest(
    [property: Description("One entry per book the sender wants to order now.")]
    List<RequestedItem> Items,
    [property: Description("The delivery date phrase copied exactly from the email, e.g. \"by next Tuesday\". null if no date is mentioned.")]
    string? DeliveryDateText,
    [property: Description("Your reading of DeliveryDateText as yyyy-MM-dd, using the Sent date. null if no date is mentioned.")]
    string? DeliveryDate);

public sealed record RequestedItem(
    [property: Description("Book title as written in the email.")]
    string Title,
    [property: Description("ISBN-13 digits only, only if written in the email body. Otherwise null.")]
    string? Isbn,
    [property: Description("Number of copies requested now. null if the email gives no exact number.")]
    int? Quantity,
    [property: Description("The sentence from the email this item comes from, copied word for word.")]
    string SourceQuote);

Tarih için iki alan var. DeliveryDateText e-postadaki ifadenin aynısı ("gelecek salıya kadar"); tarihi production'da bu ifadeden kod hesaplayacak. DeliveryDate ise modelin kendi hesabı; yalnız karşılaştırma için istiyoruz, böylece aynı koşuda "model mi, kod mu" sorusunu ölçebiliyoruz. System prompt'a da e-postanın türüne özgü kuralları yazıyoruz:

## Rules
- Extract only what the email states. Do not use outside knowledge to fill fields.
- A field the email does not state is null. Words like "a few", "same as last year"
  or "we will confirm later" are not quantities: use null.
- Write numbers given in words as digits ("twenty-five" is 25).
- If the sender changes an earlier quantity, use the new one.
- ISBN: digits only, without hyphens, and only if it is written in the email body.
  "ISBNs are attached" means null.
- Copy SourceQuote and DeliveryDateText exactly as they appear in the email.
items found       8/12 (extra: 0)
quantity correct  8/12 (invented: 0)
isbn correct      8/12 (invented: 0)
date by model     2/8
date by code      6/8
quotes in email   6/8

Uydurulan değer sıfıra indi; ama dört kitap kayboldu. Üç e-postada model items listesini boş döndürmüş: adedi belli olmayan iki kitabın olduğu ox-02, yazıyla "yirmi beş adet" yazan ox-06 ve kısa ox-08. ox-04'te de sourceQuote alanına JSON null yerine "null" string'i yazılmış. Bu, küçük modellerde sık görülen bir yan etki: "bilmiyorsan null yaz" kuralı kuvvetli olunca model, emin olmadığı kalemi tamamen atlıyor. Extraction'da iki hata türü birbirine karşı çalışıyor: uydurma (halüsinasyon) ve eksik bırakma. Bir prompt değişikliği birini azaltırken diğerini artırabilir; bu yüzden ikisini ayrı saymak gerekiyor.

v3: "Her Kitabı Listele" ve Tek Bir Örnek

v3, v2'nin kurallarına iki satır ve bir örnek ekliyor. Örnek, test setindeki e-postalardan biri değil; adedi yazıyla verilen bir kitabı ve adedi belli olmayan bir kitabı aynı anda gösteriyor:

- Every book the sender wants to order goes into Items, even when its quantity is null.
- Use JSON null, never the string "null".

## Example
<email>
Sent: 2026-10-05 (Monday)

Hello, we need twelve copies of Persuasion and some copies of Walden, I will confirm
the number. ISBN for Walden: 978-0-00-000000-2. Delivery by 20 October please.
</email>
{"items": [
  {"title": "Persuasion", "isbn": null, "quantity": 12,
   "sourceQuote": "we need twelve copies of Persuasion"},
  {"title": "Walden", "isbn": "9780000000002", "quantity": null,
   "sourceQuote": "some copies of Walden, I will confirm the number"}],
 "deliveryDateText": "by 20 October", "deliveryDate": "2026-10-20"}
dotnet run --project src/Bookstore.Prompts.Lab -- order-extraction v3
order-extraction v3 | gemma3:4b | 8 cases
PASS ox-01
PASS ox-02
FAIL ox-03  Frankenstein: quote not in email
PASS ox-04
PASS ox-05
PASS ox-06
PASS ox-07
PASS ox-08
7/8 passed (88%)

items found       12/12 (extra: 0)
quantity correct  12/12 (invented: 0)
isbn correct      12/12 (invented: 0)
date by model     4/8
date by code      8/8
quotes in email   11/12

Kalemler, adetler ve ISBN'ler tam. ox-03'teki hata ilginç: model alıntıyı "Geçen yıl aldığımız Frankenstein siparişını" diye yazmış; e-postada "siparişini". Tek bir harf değişmiş ve alıntı artık e-postada geçmiyor. Kelimesi kelimesine alıntı istemenin faydası tam da bu: kod, alıntının kaynakta geçip geçmediğine bakarak modelin metni gerçekten okuyup okumadığını kontrol edebiliyor. Alıntı tutmuyorsa kalem otomatik işlenmiyor, Hakan'a gidiyor.

Tarih: Model mi Hesaplasın, Kod mu?

Aynı koşuda model sekiz tarihin dördünü doğru hesapladı; kod, modelin kopyaladığı ifadeden sekizini de. Fark tesadüf değil. "Gelecek salı" gibi bir ifadeyi tarihe çevirmek iki adımdan oluşuyor: ifadeyi bulmak ve takvim aritmetiği yapmak. Birincisi dil işi, LLM bunda iyi. İkincisi kesin bir hesap ve küçük modeller bunda tutarsız: 25 Kasım çarşamba gönderilen "önümüzdeki çarşamba" için model 30 Kasım dedi; doğrusu 2 Aralık. Kodda bu hesap birkaç satır:

foreach ((string stem, DayOfWeek day) in Days)
{
    if (!text.Contains(stem))
    {
        continue;
    }
    int ahead = ((int)day - (int)sent.DayOfWeek + 7) % 7;
    DateOnly next = sent.AddDays(ahead == 0 ? 7 : ahead);
    // Pazartesi söylenen "gelecek cuma": bu cuma mı, sonraki mi? Tahmin etme.
    bool sameWeek = ISOWeek.GetWeekOfYear(next.ToDateTime(TimeOnly.MinValue))
        == ISOWeek.GetWeekOfYear(sent.ToDateTime(TimeOnly.MinValue));
    bool saysNext = text.Contains("gelecek") || text.Contains("önümüzdeki") || text.Contains("haftaya");
    return saysNext && sameWeek ? new(null, true) : new(next, false);
}

Kodun bir avantajı daha var: belirsizliği tanıyabiliyor. ox-02 pazartesi gönderilmiş ve "teslimat gelecek cuma olsun" diyor. Türkçede bu, aynı haftanın cuması da olabilir, bir sonraki haftanınki de. Resolver bu durumda tarih uydurmuyor, Ambiguous döndürüyor ve sipariş ekranında "teslim tarihini teyit et" uyarısı çıkıyor. Model v1'de ox-02 için 22 Kasım yazmıştı; bu tarih bir pazar. v3'te tarihi boş bıraktı, ama "acelemiz yok" diyen ox-04 için e-postanın gönderildiği günü, 20 Kasım'ı yazdı. Resolver'ın tanımadığı bir ifade de (ör. "ay sonuna doğru") null ve insan onayı demek. Bu resolver yalnız setteki ifade türlerini kapsıyor; production'da tanımadığı her ifadeyi loglayıp kapsamı ona göre genişletmek gerekiyor.

Genel kural şu: normalizasyonun dil kısmını modele, hesap kısmını koda ver. Birim dönüşümü, para tutarı, ISBN'den tire temizlemek ve tarih aritmetiği deterministik kod işi. Model ham ifadeyi kopyalasın; o ifade log'da da kalır ve hata ayıklarken neyin neye çevrildiği görülür.

Şemanın Söyleyemediğini Validator Söyler

v3 sonuçlarını bir de başka bir küçük modelle, qwen3:4b ile koştum. Toplam sonuç 6/8; hatalardan biri ox-01'de, v1'deki hatanın aynısı: Emma'ya Dune'un ISBN'i yazılmış. Prompt "ISBN yalnız e-postada yazıyorsa" diyor; ISBN e-postada gerçekten yazıyor, ama başka bir kitabın yanında. Bunu ne şema ne de prompt yakalayabilir. Validator, e-postadaki ISBN'e en yakın kitap adının kalemin kendi adı olup olmadığına bakıyor:

else if (emailDigits.IndexOf(isbn, StringComparison.Ordinal) is int at and >= 0)
{
    // ISBN başka bir kitabın değil, bu kalemin adının yanında yazıyor olmalı
    string? nearest = NearestTitle(extraction, email.Body, digitPositions[at]);
    if (nearest is not null && !string.Equals(nearest, item.Title, StringComparison.OrdinalIgnoreCase))
    {
        issues.Add($"{item.Title}: ISBN {isbn} is written next to {nearest}");
    }
}

Validator ayrıca ISBN-13 kontrol hanesini, adedin pozitif olmasını, alıntının e-postada geçmesini ve en az bir kalem bulunmasını kontrol ediyor. Sorun bulursa e-posta otomatik işlenmiyor:

ox-01 review: Emma: ISBN 9780441013593 is written next to Dune
ox-02 auto
ox-03 auto
ox-04 auto
ox-05 auto
ox-06 auto
ox-07 auto
ox-08 auto

Aynı koşu bir sınırı da gösteriyor: qwen3:4b ox-06'da kitap adı olarak yalnız "r" yazmış ve validator bunu geçirdi; çünkü "r" harfi e-postada geçiyor ve kalemin ISBN'i yok. Bu açığı kapatmanın yolu prompt değil, kitap adını Catalog'daki kayıtlarla eşleştirmek. Eşleşmeyen kalem de Hakan'a gidiyor.

Üretimde Dikkat

Alan bazında ölç. E-posta bazında 7/8 ile 1/8 arasındaki fark, hangi alanın neden düzeldiğini söylemez. Uydurulan ve kaybolan değeri ayrı say; ikisinin maliyeti farklı. Yanlış bir adet yanlış sevkiyat demek, kaybolan bir kalem ise Hakan'ın fark edip ekleyeceği bir eksik.

İnsan onayını tasarımın parçası yap. Extraction'ın hedefi Hakan'ı ortadan kaldırmak değil, onun işini "okuyup yazmak"tan "kontrol edip onaylamak"a çevirmek. Validator'ın her uyarısı ekranda alanın yanında görünür; alıntı da yanında durur, Hakan e-postayı açmadan kontrol eder.

Girdiyi temizlemeden ver ama sınırla. E-posta imzaları, alıntılanmış eski yazışmalar ve ekler context'i şişirir. Eski yazışma, ox-05'teki "40 demiştik ya, 45'e çıkaralım" gibi durumlarda model için gerekli olabilir; yine de tüm thread'i göndermek yerine son mesajı ve bir önceki siparişi yapılandırılmış hâlde vermek daha kontrollü.

Kişisel veriyi düşün. Kurumsal e-postalarda ad, telefon ve adres geçer. Prompt'a ve log'a ne girdiğini bil; hassas verinin modele gitmeden maskelenmesi LLM Uygulama Güvenliği serisinin konusu.

Sık Düşülen Tuzaklar

Zorunlu alanla eksik bilgiyi ifade etmeye çalışmak. int Quantity bir "bilinmiyor" değeri taşıyamaz; model 0, 1 veya 3 yazar. Bilinmeyebilecek her alan nullable olmalı ve null'ın ne anlama geldiği açıklamada yazmalı.

Şema geçerliliğini doğruluk sanmak. v1'in sekiz çıktısının sekizi de şemaya uyuyordu; yalnız biri doğruydu.

Halüsinasyonu azaltırken kaybı artırmak. v2'de uydurma sıfırlandı, dört kalem kayboldu. Tek metriğe bakan biri v2'yi iyileşme sanardı.

Takvim aritmetiğini modele bırakmak. Model "gelecek salı" ifadesini doğru buluyor, hangi güne düştüğünü yanlış hesaplayabiliyor. İfadeyi modele kopyalat, tarihi kodla hesapla.

Alıntı isteyip kontrol etmemek. Alıntı, kodla e-postada aranmadığı sürece yalnız güven verici görünen bir alan olarak kalır.

Küçük Bir Deneme

★☆☆ Test setine "ayın 5'ine kadar" diyen ve 20 Kasım'da gönderilen bir e-posta ekle. Resolver ne döndürüyor, model ne yazıyor? Beklenen değeri önce sen yaz.

★★☆ v3'te örneği kaldırıp yalnız iki yeni kuralı bırak. Kaybolan kalem sayısı v2'ye mi döner, v3'te mi kalır? Bu, iyileşmenin kuraldan mı örnekten mi geldiğini gösterir.

★★★ ExtractionValidator'a Catalog eşleştirmesi ekle: elinde başlık listesi olan küçük bir ICatalogLookup interface'i yaz, başlığı eşleşmeyen kalemi "review" yap ve qwen3:4b'nin ox-06'daki "r" çıktısının artık yakalandığını bir testle göster.

Sıradaki yazıda Gül'ün kutusuna dönüyoruz: ticket'ları ekiplere yönlendiren bir classification prompt'u. Etiketleri karşılıklı dışlayan tanımlar, few-shot örneklerini sınır vakalardan seçmek, etiket sırasının sonucu değiştirip değiştirmediği ve modelin kendi "güven" beyanının neden işe yaramadığı orada ölçülecek.