Tersine Mühendislikte Model Halüsinasyonunu Durduran Tek Kapı: Diferansiyel Test

Son Güncelleme: 2026-07-30 10:58:49

Bir leaf function’ı modele okutuyorsunuz; model de net bir sonuca varıyor: "Bu ChaCha20, yalnızca tur sayısı anahtardan türetilen bir değişkene dönüştürülmüş." Mantıklı geliyor. Hatta bu açıklamaya güvenip doğrudan yeniden yazımı hazırlamaya başlayabilirsiniz.

Durun. Bu cümleyi çalışan bir assert hâline getirmediğiniz sürece, elinizde yalnızca bir cümle var. Model ne kadar çok açık kaynak uygulama görmüş olursa olsun, bu özel örneği doğru tanıyıp tanımadığını kontrol edemez. Bu bir dil meselesi değil, olgusal bir sorudur; yanıtı da iki tarafın çıktısını aynı girdilerle üretip bayt bayt karşılaştırmaktır.

Bu yazının konusu, o soruya cevap verebilen tek mekanizma: diferansiyel test. Bu yöntem, dört aşamalı akışın 3. aşaması ve sapma noktası avından çıkan her hipotezin uğraması gereken yer. Model şüphe üretir; hükmü diferansiyel test verir.

Modele "Doğru mu?" diye sormak neden işe yaramaz

Yeni başlayanların modele en sık yönelttiği soru şudur: "Bu uygulamanın doğru olduğunu doğrulayabilir misin?"

Bu soru üç ayrı nedenle çıkmaz sokaktır.

İlk olarak, model sizinle aynı fikirde olmaya eğilimlidir. "Bu implementasyon doğru mu?" sorusu kendi ön kabulünü taşır: Elbette doğru olmasını istiyorsunuzdur ve model de bu tonu yakalayıp size bir "evet" verme eğilimindedir.

İkinci sorun, modelin karar verecek zemine sahip olmamasıdır. "Doğru" diyebilmek için tek gerçek ölçüt, aynı girdi kümesinde kara kutunun ürettiği çıktıdır; bu veri modelin bağlamında yer almaz. Karşılaştıracağı bir ground truth olmadığından, kodun "makul görünüp görünmediğine" bakarak tahmin eder. Oysa obfuscate edilmiş kodun en iyi yaptığı şey zaten makul görünmektir.

Üçüncüsü, soruyu yanlış mercie soruyorsunuz. Doğruluk, üzerinde pazarlık edilecek bir görüş değil; bir eşitliğin sağlanıp sağlanmadığıdır. Bu kararı modele değil, assert'e bırakın: Model doğru dese ama test başarısız olsa, kazanan testtir. Model yanlış dese ama tüm testler geçse, yine test kazanır. Kararı modelin eline verdiğiniz anda, yapınızı halüsinasyon üzerine kurmuş olursunuz.

Sınır girdileri nasıl seçilmeli?

Diferansiyel testin iskeleti basittir: Orijinali kara kutu kabul eder, yeniden yazımınızı onunla aynı girdi grubunda vaka vaka karşılaştırırsınız. Asıl değer döngüde değil, o döngüye verdiğiniz girdi setindedir.

On binlerce rastgele, sıradan string gönderip tüm testlerin yeşil olması hiçbir şey kanıtlamaz. Normal girdiler ana yolu çalıştırır; sapma noktaları ise çoğunlukla köşelerde saklanır. Bilgi taşıyan şey sınır vakalarıdır:

# Boundary cases: derived from block size B
def boundary_cases(B: int) -> list[bytes]:
    return [
        b"",                 # empty: exposes initial state and padding logic
        b"\x00",             # single zero byte
        b"\xff",             # single high byte: checks sign-bit / unsigned handling
        b"A" * (B - 1),      # one below the block boundary
        b"A" * B,            # exactly one block
        b"A" * (B + 1),      # one above: checks carry and padding
        bytes(range(256)),   # full byte coverage: checks the alphabet map covers the whole domain
    ]

Bu vakaların her biri belirli bir dalı sorgular. Yüksek bayt \xff, işaret biti işleme farklarını açığa çıkarır: JavaScript'te >>> ile >> arasındaki fark, Python tarafında & 0xff kullanılıp kullanılmadığı, bu tek vakada görünür. En sert test ise B-1 / B / B+1 üçlüsüdür; blok doldurma mantığı yalnızca burada kendini ele verir. Tam bayt kapsamı da özel alfabeler içindir: Eşleme tablosunda bir karakter fazla ya da eksikse, bu vaka istisnasız başarısız olur.

Diferansiyel testin kendisi on satırın bile altında kalır:

# Original as black box, rewrite compared case by case
def diff_test(blackbox, rewritten, B: int) -> None:
    for case in boundary_cases(B):
        got, want = rewritten(case), blackbox(case)
        assert got == want, f"len={len(case)} hex={case.hex()}"

Şunu da not etmekte fayda var: Sapma noktası hipoteziniz standart bir algoritmayla ilgiliyse, her zaman orijinal kara kutuya ihtiyacınız olmayabilir. Açık standartlar çoğu zaman yetkili test vektörleri yayımlar. Örneğin RFC 8439 §2.1.1, ChaCha20 quarter round için sabit bir girdi/çıktı çifti verir: a=0x11111111, b=0x01020304, c=0x9b8d6f43, d=0x01234567 verdiğinizde a=0xea2a92f4, … elde edersiniz. Önce yeniden yazımınızın standart vektörü geçmesini sağlayın, ardından hedef kara kutuyla karşılaştırın. Böylece iki farklı problemi net biçimde ayırırsınız: "ChaCha implementasyonunu ben yanlış yazdım" ve "hedef ChaCha'yı değiştirmiş."

assert mesajında len(case) bulunmasına dikkat edin. Bu, tüm test paketindeki en yararlı teşhis satırıdır. Yedi vakanın yalnızca B+1 olanı başarısızsa sorun neredeyse kesin olarak padding veya carry katmanındadır. Sadece tam bayt kapsamı başarısız oluyorsa hata alfabe eşlemesindedir. Başarısız vakanın uzunluğu, tahmin yürütmeden sizi doğrudan sorunlu katmana götürür.

Aynı girdiyi mutlaka iki kez çalıştırın

İmza mantığını yeniden yazarken en sık karşılaşılan engel, algoritmayı yanlış tanımak değildir. Genellikle kodda ayrıştırılmamış bir entropi kaynağı vardır.

Bunu yakalamak için tek satır yeter: Aynı girdiyi iki kez çalıştırın.

# Same input twice: differing results mean a hidden RNG / timestamp
def assert_deterministic(fn, case: bytes) -> None:
    assert fn(case) == fn(case), "unfixed entropy source or timestamp present"

İki sonuç farklıysa implementasyon, her çağrıda değişen bir time.time(), nonce, kendini artıran sayaç ya da benzeri bir değer kullanıyordur. Bu noktada diferansiyel karşılaştırma yapamazsınız; kara kutu her seferinde başka yanıt veriyorsa neyi neyle karşılaştıracaksınız?

Çözüm entropi kaynağını silmek değildir; silerseniz imza yanlış olur. Yapılması gereken, bu kaynağı kodun içinden çıkarıp enjekte edilebilir bir parametreye dönüştürmek ve diferansiyel test sırasında sabit bir değere kilitlemektir:

# Lift the entropy source into an injectable parameter, pin it with a stub for diffing
class Rewritten:
    def __init__(self, clock=time.time, rng=os.urandom):
        self._clock = clock          # formerly an inline time.time(), now injected
        self._rng = rng

    def __call__(self, data: bytes) -> bytes:
        ts = int(self._clock())      # for diffing, clock=lambda: 0
        nonce = self._rng(16)        # for diffing, rng=lambda n: b"\x00" * n
        ...

Hedef kara kutu için de aynı işlemi yapın: Zaman damgasının veya rastgeleliğin eklendiği noktayı bulun ve onu sabitlemenin yolunu keşfedin. Sayfada Date.now'u hook'layabilir ya da Node'a sabit bir seed verebilirsiniz. Her iki taraftaki entropi sabitlendiğinde çıktı yeniden deterministik olur ve diff anlamlı hâle gelir. Yeniden yazım uçtan uca testi geçtikten sonra clock ve rng'yi gerçek implementasyonlarına geri döndürün.

Bu da modelin yardımcı olamayacağı adımlardan biridir. Entropinin nerede saklandığı ve nasıl enjekte edildiği çalışma zamanı davranışıdır; bunu kodu okuyarak değil, aynı girdiyi iki kez çalıştırarak ortaya çıkarırsınız.

Tek çıktıya bakmayın, katmanları tek tek ayırın

Yeniden yazımınızın genel çıktısı kara kutuyla eşleşmiyorsa, son bayt dizisine takılıp kalmayın. Bu çıktı iç içe geçmiş birden çok katmanın ürünüdür ve hangisinin yanlış olduğunu henüz bilmiyorsunuzdur.

Bir signer çoğunlukla katmanlıdır: En içeride hash ya da blok şifre bulunur; bunun üstünde bir encoding katmanı vardır (bir Base64 ailesi, hex veya özel bir tablo); en dışta ise birleştirme katmanı yer alır (prefix ekleme, alanları yerleştirme, uzunluk başlığı ekleme). Katman katman ayırmak, en içten başlayarak karşılaştırmak ve mevcut katman geçmeden bir dış katmana ilerlememek demektir:

# Peel layer by layer: match the innermost first, then move outward
LAYERS = ["digest", "encode", "assemble"]   # inner → outer

def first_divergent_layer(bb_dump, rw_dump, case: bytes) -> str | None:
    for layer in LAYERS:
        if bb_dump(case)[layer] != rw_dump(case)[layer]:
            return layer                     # the first layer to diverge is the faulty one
    return None

Bunun için yeniden yazımınızın her katmandaki ara durumu dump edebilmesi gerekir. Kara kutudan da karşılık gelen ara değerleri, çoğu zaman çalışma zamanında enstrümantasyon yaparak çıkarmanız gerekir. Ek emeğe kesinlikle değer: İlk ayrışan katman, hatalı katmandır ve araştırma alanınız anında daralır.

Bu yaklaşım, dört sapma noktası kategorisine doğrudan bağlanır. Ayrışma digest katmanındaysa çoğu zaman bir sabit değiştirilmiş ya da tur sayısı farklılaştırılmıştır. Encode katmanındaki ayrışma genellikle alfabenin yeniden sıralandığını gösterir. Assemble katmanında ise çıktıya bir malzemenin gömülmüş olması muhtemeldir. Ayrışan katman, fingerprinting yazısında hangi kategoriyi incelemeniz gerektiğini söyler.

Fixture'ların zaman içinde artan değeri

Diferansiyel testin yeşile dönmesi harika bir histir, ama tek seferliktir. Upstream yarın yeni bir sürüm çıkarır ve bugün doğruladığınız implementasyon tamamen yanlış hâle gelebilir.

Zamanla gerçekten değer kazanan artefact ise fixture'dır: Repoya commit edilen, bilinen girdi → bilinen çıktı tablosu.

# Fixture: known input → known output, committed to the repo
# Re-run on every upstream change; green yesterday, red today = upstream changed, not you
VECTORS = load_json("vectors.json")   # [{"in": "<hex>", "out": "<hex>"}, ...]

for v in VECTORS:
    got = rewritten(bytes.fromhex(v["in"])).hex()
    assert got == v["out"], v["in"]

Asıl değeri upstream değiştiğinde ortaya çıkar. Bir gün CI kırmızıya döner ve dün hiç dokunmadığınız implementasyon fixture karşısında başarısız olur. Bu bilgi paha biçilmezdir: "Ben hata yaptım" ihtimalini eler ve sorunu doğrudan "upstream değişti" sonucuna bağlar. Fixture olmadan, hata sizde mi yoksa hedef kuralları mı değiştirdi anlayamadığınız için, kusursuz çalışan kodu yarım gün boyunca debug edebilirsiniz.

Fixture için en iyi girdiler, daha önceki sınır vakalarının ta kendisidir; zaten elinizdeki en yüksek kapsamlı kümedir.

Bu aşamada modelin yalnızca iki görevi var

Diferansiyel testte iş bölümünü açıkça koyalım: Modelin iki görevi vardır ve hüküm verme yetkisi yoktur.

İlki, vaka üretmektir. Çok sayıda primitive için toplu sınır girdisi üretmek ya da şüpheli bir sabiti yoklamak üzere tek bit farkla ayrılan girdilerden bir küme oluşturmak, muhakeme değil enumerasyon işidir. Bu nedenle ucuz ve yüksek eşzamanlılık sunan bir katman en ekonomik seçenektir.

İkincisi, diff'i açıklamaktır. Bir vaka başarısız olduğunda iki katman dump'ını modele verirsiniz; belirli baytlar üzerinden ilk ayrışmanın nerede başladığını ve bunun dört sapma kategorisinden hangisine en çok uyduğunu açıklamasını istersiniz. Bu iş orta seviye muhakeme ve "baytlar üzerinden açıklama" becerisi ister; bu yazıda modelin gerçekten çalıştığı yer burasıdır.

Karar her zaman assert'e aittir; bu değişmez. Modelin açıklaması bir ipucudur, sonuç değil. Yanlış yöne işaret eden ipuçları normaldir ve bunları assert yakalar.

Bu iki görev modelden farklı kabiliyetler bekler. Her iş için tek bir katman kullanmak ya para israfına ya da hassasiyet kaybına yol açar:

Diferansiyel test alt adımı

Gerekli yetkinlik

Seçim

model id

Toplu sınır/kontrol vakası üretimi (çok sayıda primitive için)

Ucuz, yüksek eşzamanlılık; enumerasyon muhakeme gerektirmez

Claude Sonnet 5

claude-sonnet-5

Tek bir başarısız diff'i okuyup baytlar üzerinden açıklama

Orta seviye muhakeme; atıf belirli bir katmana oturmalı

GPT-5.6 Sol

gpt-5.6-sol

Atıf netleşmediğinde kök nedene derinlemesine inme (sabit perturbation gibi)

Güçlü muhakeme; çok turlu ara durumlar arasında çıkarım yapabilme

Claude Opus 5

claude-opus-5

Ayrışmayı bulmak için çok sayıda katman dump'ını / tüm fixture grubunu işleme

Uzun bağlam

Kimi K3

kimi-k3

Asıl iş yükünü ikinci katman taşır. Fark atfı için özel model seçmenin değip değmeyeceğini, tek tur testle kendiniz anlayabilirsiniz. Protokol kısa:

  1. Diferansiyel test paketinizden, kara kutu ve yeniden yazım için iki katman dump'ıyla birlikte gerçekten başarısız olan tek bir vaka seçin.

  2. Aynı diff'i gpt-5.6-sol ve claude-opus-5'e ayrı ayrı verin. Yalnızca iki şey sorun: İlk ayrışma hangi katmanda ve en olası dört sapma kategorisinden hangisi?

  3. Yalnızca atfın belirli bir bayta ve belirli bir katmana oturup oturmadığına bakın. Size muğlak bir "padding sorunu olabilir" yanıtı veriyorsa bunu yeterli saymayın.

  4. Seçim ölçütünüz atıf hassasiyeti olmalı; çünkü bu, tek bir vakayı yeşile çevirmek için kaç tur düzenleme yapacağınızı doğrudan belirler.

Tek tur, aradaki farkı herhangi bir benchmark liderlik tablosundan daha doğrudan gösterir.

Asıl sorun katman değiştirme maliyeti

Bu dört katman üç farklı sağlayıcıdan geliyor: Üç SDK, üç kimlik doğrulama düzeni, üç hata formatı. Alt adımlar arasında katman değiştirmek için istemcinizi üç kez entegre etmek pratik değildir. Bu yüzden çoğu kişi her şey için tek katman kullanır, fark atfında yalnızca belirsiz ifadeler kullanan bir modele mahkûm olur ve nedenini bilmeden birkaç düzenleme turu harcar.

AIReiter bu katmanı düzleştirir: Tek anahtar, OpenAI uyumlu tek arayüz ve arkasında dört katmanın tamamı. Katman değiştirmek, istek gövdesindeki model alanını değiştirmekten ibarettir.

# Difference attribution: the mid-reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.6-sol",
    "messages": [{"role": "user", "content": "<diff of the two layer dumps + locate the first divergent layer and deviation category>"}]
  }'

# Attribution won't converge, escalate to dig the root cause: change one field
#   "model": "claude-opus-5"
# Bulk-generate boundary cases:
#   "model": "claude-sonnet-5"

Zaten OpenAI SDK kullanıyorsanız base_url'yi https://aireiter.com/api/v1 olarak ayarlayın; başka hiçbir şeyi değiştirmeniz gerekmez. Anthropic SDK kullanıyorsanız aynı anahtarla POST /api/v1/messages isteği gönderin.

Fiyat tarafında Claude modelleri liste fiyatına göre %30 indirimli, GPT modelleri ise yarı fiyatına çalışır. Bu iş akışında indirim en yoğun kullanılan noktaya denk gelir: fark atfı, diferansiyel testin en sık çağrılan parçasıdır. Her başarısız vaka için bir tur gerekir; her upstream değişikliği de fixture'ın yeniden kurulması ve atıf yapılacak yeni bir başarısız vaka grubunu beraberinde getirir. Ana yükü taşıyan gpt-5.6-sol, yarı fiyatına sunulan bir GPT modelidir; dolayısıyla işin yoğun bölümü doğrudan yarı maliyete iner. Kök nedeni derinlemesine araştırmak için ara sıra claude-opus-5'e yükseltmek gerektiğinde ise çağrı sayısı düşüktür, ancak Claude modellerindeki %30 indirim burada da geçerlidir.

  • API anahtarı alın

  • Kayıt olmadan deneyin — önce gerçek bir diff'i elle iki katmana da verin, atıf hassasiyetini kendi gözünüzle karşılaştırın; ardından hangisini entegre edeceğinize karar verin.

Sonuç

Tersine mühendislik yeniden yazımlarının aslında iki temel direği vardır: Model hipotezi verir, assert ise hükmü.

Model, herkese açık implementasyonları görmüş bir şüphe üreticisidir. Saniyeler içinde "burada değişiklik yapılmış olabilir" diyebilir; ancak bu kez haklı olup olmadığını asla bilemez. Diferansiyel test ise "olabilir"i "vardır/yoktur"a çeviren mekanizmadır: Sınır girdileri dalları açığa çıkarır, aynı girdiyi iki kez çalıştırmak entropi kaynağını ortaya çıkarır, katman katman ayırma hatalı katmanı belirler, fixture ise "ben yanlış yazdım" ile "onlar değiştirdi" ayrımını yapar.

Dört aşamalı akışın ve sapma noktası avı yazısının ürettiği her yargı, eninde sonunda bu kapıdan geçmek zorundadır. Modelin söylediklerinin hiçbiri tek başına hüküm değildir; önemli olan yalnızca assert'in söylediğidir.