Tüm yazılar
Teknik
  • #structured-output
  • #validation

Doğrulama pipeline'ları

06.07.2026·13 dk

Doğrulama pipeline'ları

Structured Outputs · 19-c03

26 Haziran 2026 · Uygulama sınırı okuması

Bir LLM'in ürettiği JSON, prompt'ta ne kadar net şema tarif etmiş olursanız olun, bir validator onu kabul edene kadar güvenilmez girdidir. Doğrulama, prompt'un içine yazılan bir ricaya değil, uygulamanın sınırına ait bir bileşendir.

Soru
çıktıya güvenilir mi?
Sınır
model → validator → kod
Kaldıraç
schema as code
Tuzak
JSON geçerli = doğru sanmak

Bu yazıdaki kavramlar

  • JSON Schema
  • type · enum · required
  • nested schema
  • Pydantic · Zod
  • Instructor
  • Guardrails · OnFail
  • partial streaming
  • SLOT

Önceki yazılarda prompt'u kurduk ve modelden yapılandırılmış çıktı istedik. Şimdiki soru daha sert: model bize bir JSON döndürdü, peki bu JSON'a gerçekten güvenebilir miyiz? Cevap, çıktıyı nerede karşıladığınıza bağlı. "Geçerli JSON mu?" ile "benim şemama uyuyor mu?" ile "eksik alanlar gerçekten var mı?" çok farklı şeyleri ölçer.

Bu yazıda doğrulamayı bir uygulama sınırı (application boundary) olarak ele alıyorum: modelden gelen her şey, bir validator onu kabul edene kadar dış dünyadan gelen ham girdi gibi muamele görür. type, enum, required, nested schema ve streaming sırasındaki partial output'u tek tek kapılara ayırıyorum; her şemayı gerçek bir kaynağa bağlıyorum: JSON Schema 2020-12, OpenAI ve Azure structured outputs, Pydantic, Zod, Instructor, Guardrails AI ve SLOT. Diyagramlar kendi başına sürekli oynayıp her kapının neyi yakaladığını gösteriyor.

01/09

Çıktı, kabul edilene kadar güvenilmez girdidir

Bir validation pipeline'ının tek bir temel ilkesi vardır: modelin ürettiği metin, sizin tip sisteminize girene kadar dışarıdan gelen ham girdidir. Tıpkı bir kullanıcının form alanına yazdığı şey gibi. Pydantic bu ayrımı net koyar: validation, çıktı nesnesinin alanlarınıza ve kısıtlarınıza uyduğunu garanti eder, ham girdinin baştan doğru tipte olduğunu değil.

Bu yüzden doğrulamayı prompt'un içine "lütfen geçerli JSON döndür" diye yazmak yetmez. O bir ricadır, bir garanti değil. Garantiyi, çıktıyı karşılayan kod katmanı verir: parse et, şemaya göre kontrol et, geçerse tipli bir nesne üret, geçmezse reddet. Aşağıda modelin çıktısı kesik çizgili sınırı geçiyor; sınırın solunda "untrusted" bölgesinde, sağında ancak validator onayladıktan sonra "typed" bölgesinde.

Bu çerçeve sıkıcı görünebilir ama tüm yazının özü budur. Sınırı kabul ederseniz, geri kalan her şey (type check, enum, nested schema, retry) o sınırın üstünde duran somut kapılara dönüşür.

uygulama sınırı · untrusted çıktı, typed bölgeye geçiyor

ham girdi bölgesi

model çıktısı · untrusted
validator

tipli / doğrulanmış bölge

tipli nesne

sınırı geçen tek şey, validator'ın kabul ettiği çıktıdır

ham girdi bölgesi validator tipli / doğrulanmış bölge
02/09

Geçerli JSON, doğru JSON değildir

İlk kafa karışıklığı burada: "JSON parse edildi" ile "şemaya uyuyor" aynı şey değildir. Bir metin sözdizimsel olarak geçerli JSON olabilir ama beklediğiniz alanları, tipleri ya da değerleri taşımayabilir. OpenAI bu ayrımı kendi dokümanında net yapar: eski JSON mode geçerli JSON üretmeyi garanti eder, structured outputs ise verdiğiniz şemaya uymayı.

SLOT çalışması (EMNLP 2025) bunu ölçer hale getirir: bir cevabı ancak hem geçerli JSON ise hem de hedef şemanın anahtar adlarıyla ve değer tipleriyle birebir eşleşiyorsa "şema doğru" sayar. Üstelik şema doğruluğunu içerik benzerliğinden ayrı ölçer, çünkü bir çıktı sözdizimini geçip yine de anlamı kaybedebilir ya da bozabilir.

Aşağıda aynı metin iki kapıdan geçiyor. İlki sadece "parse oldu mu?" diye sorar ve neredeyse her şeye yeşil yakar. İkincisi "alan adları ve tipler şemaya uyuyor mu?" diye sorar; eksik alan ya da yanlış tip onu kırmızıya çevirir.

JSON geçerliliği · şema doğruluğunun zayıf bir alt kümesidir

parse oldu mu?

geçerli JSON

şemaya uyuyor mu?

name: string ✓
age: int ✗
role: enum ✗

tip ve enum uyuşmuyor

geçerli JSON gerekli ama yeterli değildir

03/09

Schema as code: Pydantic ve Zod

Doğrulamayı sınıra koymanın en pratik yolu, şemayı kod olarak yazmaktır. Python tarafında Pydantic, TypeScript tarafında Zod bunu yapar: alanları, tipleri ve kısıtları kodda tanımlarsınız, kütüphane de runtime'da gelen veriyi bu tanıma göre doğrular.

İkisi de aynı sınır mantığını taşır. Pydantic'te model_validate_json başarılıysa tipli bir model örneği döner, başarısızsa tek bir ValidationError içinde bulduğu tüm hataları toplar. Zod'da parse başarıda derin kopyalı tipli bir değer döner, hatada ZodError fırlatır; safeParse ise fırlatmak yerine ya veriyi ya hatayı taşıyan bir sonuç nesnesi verir. Bu yüzden bunlar pipeline'ın "son parser"ı olmalıdır: tipli değeri ya da yapılandırılmış hatayı üreten yer.

Aşağıda aynı ham metin iki kütüphaneye giriyor. Başarı dalında sınırı geçip tipli bir nesne oluyor; hata dalında bir yapılandırılmış hata nesnesi olarak geri dönüyor. İki kütüphane farklı isimler kullanıyor ama sınır davranışı aynı.

schema as code · iki kütüphane, aynı sınır davranışı
ham JSON metni

Pydantic

model örneği
ValidationError

ham JSON metni

Zod

tipli değer
ZodError

ham JSON metni

başarı · tipli çıktı hata · yapılandırılmış hata
04/09

Üç temel kapı: type, enum, required

JSON Schema 2020-12, doğrulamayı bir JSON örneği üzerindeki assertion'lar (iddialar) olarak tanımlar: tüm uygulanabilir konumlar iddia edilen kısıtları sağlıyorsa örnek geçerlidir. Bu soyut görünür ama günlük hayatta üç kapıya iner. type, değerin beklenen ilkel tipte (string, number, integer, boolean, object, array, null) olmasını ister. enum, değerin izin verilen sabit listeden biriyle eşit olmasını ister. required ise yalnızca object'lere uygulanır ve listelenen her alan adının var olmasını ister.

Önemli bir incelik: required atlanırsa boş liste gibi davranır, yani "alan zorunlu değil" demektir. Şema açıkça söylemedikçe uygulamanız bir alanı zorunlu varsaymamalıdır. Ayrıca format anahtarı varsayılan olarak bir assertion değil, bir annotation'dır; e-posta ya da tarih gibi format'ları gerçekten zorlamak istiyorsanız bunu uygulama katmanında ayrıca doğrulamanız gerekir.

Aşağıda bir aday nesne soldan girip üç kapıdan sırayla geçiyor: type, enum, required. Üçünü de geçen nesne sağda yeşille kabul ediliyor. Sonraki turda enum kapısında takılan bir nesne tam o kapıda kırmızı yanıp reddediliyor; hangi kuralın kırdığı bu sayede görünür oluyor.

doğrulama kapıları · type → enum → required
aday nesne
type
enum
required
kabul edildi
reddedildi · enum kapısı
geçen aday kıran kural
05/09

Nested schema: doğrulama bir ağaçtır

Gerçek çıktılar düz değildir: bir sipariş nesnesi içinde bir müşteri, müşterinin içinde bir adres, siparişin içinde bir kalem listesi vardır. JSON Schema bunu uygulayıcı (applicator) anahtarlarıyla çözer: properties, items, $ref gibi anahtarlar, kuralları JSON örneğinin iç konumlarına uygular. Yani doğrulama bir tek kutu değil, bir ağaç üzerinde yürür.

Pydantic'te modeller başka modelleri ve model listelerini içerebilir; hata oluştuğunda mesaj nested.inner gibi bir yola işaret eder, böylece hangi derinlikte kırıldığını bilirsiniz. Zod da nested object, array, recursive object ve discriminated union destekler. Burada bir politika kararı vardır: bilinmeyen anahtarlar ne olacak? Pydantic varsayılan olarak yok sayar (forbid ile reddedebilir, allow ile saklayabilir); Zod varsayılan olarak siler, strict object modunda ise bilinmeyen anahtarda hata fırlatır. Bu "sil ya da reddet" kararı veri kaybı davranışını doğrudan belirler.

Aşağıda nested bir nesne dış kabuktan içeri doğru kapı kapı doğrulanıyor: order, içindeki customer, onun içindeki address. Doğrulama içeri yürürken yeşil ilerliyor; address derinliğinde geçersiz bir alan çıkınca tam o düğüm kırmızı yanıyor ve hata yolu o derinliği işaret ediyor.

nested schema · doğrulama ağacın içine yürür
order
customer
address
zip: pattern ✗

hata yolu · customer.address.zip

doğrulanan düğüm kıran düğüm
06/09

Provider'ın şeması sizi app-side doğrulamadan kurtarmaz

OpenAI ve Azure gibi sağlayıcılar artık şema uyumunu üretim sırasında zorlayabiliyor (strict mode). Bu güçlü, ama tehlikeli bir rahatlama yaratır: "provider zaten zorluyorsa ben neden doğrulayayım?" Cevap, provider subset'inin sizin tam şemanızdan dar olmasıdır.

Azure dokümanı somut kurallar koyar: structured outputs'ta tüm alanlar required listesinde olmalı; opsiyonelliği eksik alanla değil, tipe null ekleyerek (örneğin string ya da null) ifade edersiniz. Object'ler additionalProperties: false kullanmalı; toplam en fazla 100 property ve beş seviye nesting desteklenir. Üstelik bazı tipe özel JSON Schema anahtarları (min/max uzunluk, bazı pattern ve array kısıtları) provider decoding'inin dışında kalır, yani onları yine runtime validator'ınız zorlamalıdır. OpenAI de refusal ve yarım kalmış çıktı gibi başarısızlık durumlarının programatik olarak ele alınması gerektiğini söyler: körlemesine parse etmeyin.

Aşağıda iki katman üst üste. Üstte provider'ın garanti ettiği dar bölge (strict şema, required-nullable, additionalProperties: false). Altta sizin app-side validator'ınızın kapattığı boşluklar (uzunluk, pattern, refusal, yarım çıktı). İkinci katman olmadan, birinci katmanın bıraktığı boşluklar açıkta kalır.

provider subset'i · app-side validator'ın kapattığı boşluklar

provider garantisi (strict)

şema uyumurequired + nulladditionalProperties: false

app-side validator

min/max uzunlukpattern detayırefusal · yarım çıktı

provider zorlaması, app-side doğrulamanın yerini almaz

provider garantisi (strict) app-side validator
07/09

Partial output: streaming sırasında doğrulama gevşer

Çıktıyı streaming ile, token token alırsanız, nesne anlık olarak yarımdır: bazı alanlar daha gelmemiştir. Burada required kuralını olduğu gibi uygularsanız her ara kare "eksik alan" diye reddedilir. Instructor bu modu açıkça ayırır: bir modelin partial sürümünü üretir, bu sürümde tüm alanlar opsiyoneldir ve çıkarım tamamlanana kadar artımlı (incremental) anlık görüntüler verir.

Bunun bir bedeli var: Instructor dokümanı, streaming sırasında validator'ların çalışmadığını söyler, çünkü doğrulama eksik bir duruma güvenilir biçimde uygulanamaz. Yani partial mod ile required/validated mod farklı operasyonel modlardır. Pratik kalıp şudur: stream sırasında alanlar dolarken yumuşak gösterirsiniz, ama tam nesne tamamlandığında required'ı ve tüm validator'ları son bir kez asıl şemaya göre çalıştırırsınız.

Aşağıda bir nesne streaming ile alan alan doluyor. Henüz gelmemiş alanlar gri ve opsiyonel; her alan geldikçe kendi mini-tipini doğrulayıp yeşile dönüyor. Stream bitince altta tek bir "final required check" kapısı yanıp tüm nesneyi asıl şemaya göre son kez doğruluyor.

partial streaming · alanlar dolarken doğrulama tamamlanınca tamamlanır
title
title
summary
summary
tags
tags
score
score
final required check · tam nesne asıl şemaya göre
henüz gelmedi · opsiyonel gelen + doğrulanan alan final required check · tam nesne asıl şemaya göre
08/09

Hata gelince: retry, fix, filter, reject

Bir validator reddettiğinde iş bitmez, asıl iş başlar: ne yapacaksınız? Instructor pipeline-biçimli bir cevap verir: modeli tanımla, isteği gönder, cevabı doğrula, geçerliyse nesneyi döndür; değilse hata bağlamını LLM'e geri gönder ve max retry'a kadar tekrar dene, sonra ValidationError'ı fırlat. Yani doğrulama hatası, modele verilen bir sonraki ipucudur.

Guardrails AI bu kararları açık politikalara çevirir. Guard, LLM çağrısını saran ana arayüzdür: doğrulamayı yönetir, çağrı geçmişini tutar ve ham çıktı, doğrulanmış çıktı, geçti mi bilgisini birlikte verir. Validator'lar PassResult ya da FailResult döner; başarısızlıkta yapılandırılmış OnFail eylemleri devreye girer: REASK (modele tekrar sor), FIX (otomatik düzelt), FILTER (yalnızca kıran alanı ele, gerisini tut), REFRAIN (çıktıyı tümden geri çek), NOOP (işaretle ama geçir), EXCEPTION (hata fırlat). Bu, geçer/geçmez tek bitten çok daha zengin bir kurtarma yelpazesidir ve hepsi gözlemlenebilirdir.

Aşağıda reddedilen bir çıktı bir politika anahtarına giriyor ve farklı OnFail dallarına ayrılıyor: REASK modele geri döner, FIX düzeltilmiş haliyle geçer, FILTER kıran alanı atıp gerisini geçirir, REFRAIN çıktıyı tümden durdurur. Her tur farklı bir dalı vurgulayarak hangi politikanın ne yaptığını gösterir.

OnFail politikaları · reddedilen çıktıya ne olur
reddedilen çıktıOnFail
REASKmodele tekrar sor
FIXotomatik düzelt
FILTERkıran alanı ele
REFRAINtümden geri çek

kurtarma politikaları açık ve gözlemlenebilir olmalıdır

09/09

Doğrulama hattı: katman katman

Tek bir kontrol bir çıktıyı güvenli kılmaz. Sağlam bir doğrulama hattı katman katmandır: önce sınırın kabulü (model çıktısı = untrusted girdi), sonra parse (geçerli JSON mu), sonra şema doğruluğu (type, enum, required), nested ağacın derinliği, provider subset'inin bıraktığı app-side boşluklar, streaming için partial/final ayrımı ve en sonda hata gelince devreye giren OnFail politikaları.

Bu katmanları birlikte okumak, "modelin çıktısına güveniyor muyum?" sorusunu "hangi kapıda, hangi kuralla, hata olursa hangi politikayla doğruluyorum?" sorusuna çevirir. İyi bir validation pipeline, modelin ne ürettiğini değil, sisteminizin sınırının nerede tuttuğunu söyler.

validation pipeline · katman katman
sınır · untrusted girdi01
parse · geçerli JSON02
şema · type · enum · required03
nested · ağaç doğrulama04
provider boşlukları · app-side05
partial → final check06
OnFail · retry · fix · filter07

Modelin çıktısı, bir validator onu kabul edene kadar güvenilmez girdidir. Bütün hat bu tek cümleyi ciddiye almaktan ibarettir.

Bu yüzden "çıktıya güveniyor muyum?" demeden önce sorun: hangi kapı parse'ı, hangi kapı şemayı, hangi katman provider boşluğunu kapatıyor ve hata olursa hangi OnFail politikası devreye giriyor?