Ana sayfa → Teknik
Model Sessizce Değişti: Sürüm Değil, Drift
Perşembe 11:05, destek ekibinden mesaj: “Asistan artık kaldıraç sorularına cevap vermiyor, ‘yatırım tavsiyesi veremem’ deyip kesiyor.” Son deploy dokuz gün önceydi, prompt üç haftadır aynıydı. İki gün kendi kodumuzda hata aradık. Değişen şey bizde değildi: model adının arkasındaki model değişmişti.
- Model adı bir sürüm değil. Sağlayıcının alias’ı, senin haberin olmadan başka bir modeli gösterebilir. Sen deploy etmezsin, sistem yine de değişir.
- Canlıda sabit sürüm kullan. Adında tarih olan snapshot. Yeni sürüme geçiş bir deploy’dur: eval, gölge koşu, sonra geçiş.
- İki ayrı drift var. Model değişir, kullanıcı da değişir. Ocak’ta %3 olan halka arz soruları Haziran’da %19’du; eval setimizde dört taneydi.
- Doğruluğu her gün ölçemezsin, dağılımı ölçebilirsin. Cevap uzunluğu, red oranı, format hatası, dil. Dördü de ucuz, dördü de önceden haber veriyor.
- Canary sorular: her sabah aynı 40 soru. Eval sen bir şeyi değiştirince koşar; canary sen hiçbir şeyi değiştirmediğinde. Drift tam o günlerde olur.
Sahadan: dokuz gün deploy yok, üç belirti
Müşteri destek asistanımız haftada 5.200 civarı soru cevaplıyor: hesap açma, para çekme süreleri, emir tipleri, kaldıraçlı işlemin nasıl çalıştığı. Yatırım tavsiyesi vermiyor; bunu prompt açıkça söylüyor ve bugüne kadar sınırı iyi tutuyordu. “Kaldıraç nedir, teminat nasıl hesaplanır” bilgi sorusudur, “şimdi kaldıraçlı alayım mı” tavsiye sorusudur. Model bu ayrımı yapabiliyordu.
Perşembe mesajı gelince ilk iş son değişikliklere baktık. Kod aynı, prompt aynı, bağlama giren dokümanlar aynı. Perşembe ve Cuma’yı bizim tarafta bir şey bozmuşuz diye aradık. Pazartesi biri sağlayıcının sürüm notlarını açtı: 29 Haziran Pazartesi, kullandığımız model adı yeni bir snapshot’a yönlendirilmişti. Biz o adı kullanıyorduk, tarihli sürümü değil.
Sonra ölçtük. O hafta üç şey birden değişmişti ve hiçbiri izlenmiyordu:
| Sinyal | Önceki 14 gün | 29 Haziran sonrası |
|---|---|---|
| Red oranı | %1,4 (haftada ~73) | %7,9 (haftada ~411) |
| Format hatası (bilet sınıflayıcıya giden JSON) | %0,2 | %3,6 |
| Cevap uzunluğu, p50 | 92 kelime | 148 kelime |
| Türkçe soruya İngilizce cevap | %0,1 | %0,4 |
Haftalık 411 reddin 246’sı kaldıraç ve açığa satış sorularıydı, 118’i halka arz, 47’si diğer. Yeni model bilgi sorusunu tavsiye sorusundan daha kötü ayırıyordu ya da daha temkinliydi — hangisi olduğu bizim için fark etmiyordu. Sonuç aynıydı: müşteri “teminat nasıl hesaplanır” diye soruyor, cevap alamıyor, canlı desteğe düşüyordu.
Burada benim hatam da var. Mayıs’ta kurduğumuz token panosu çıktı token’ındaki artışı gösteriyordu: günlük çıktı token’ı %58 yükselmişti. Ama panonun alarmı toplam maliyete kuruluydu ve girdi token’ı (prompt ve bağlam) maliyetin büyük kısmı olduğu için toplam sadece %9 arttı, eşiğin altında kaldı. Sinyal ekrandaydı; ben ona fatura gözüyle bakıyordum, davranış gözüyle değil.
Model adı bir sürüm değil
Sağlayıcıların çoğu iki tür isim veriyor. Biri alias: “en güncel
model” anlamına gelen, arkası zamanla değişen bir isim. Diğeri sabit
sürüm (snapshot): adında tarih olan, davranışı değişmeyen model. Alias prototipte
rahat, çünkü hep en yeniyi veriyor. Canlıda aynı rahatlık bir risk: bir kütüphaneyi
latest etiketiyle canlıya çıkarmak gibi.
Bizde model adı bir config satırıydı ve kimse o satırı bir bağımlılık olarak görmüyordu. Değişikliğin ardından o satır şuna döndü:
# ONCE - alias: isim ayni kalir, arkasindaki model degisebilir
model: genel-model
# SONRA - sabit surum: tarih ismin parcasi, davranis sabit
model: genel-model-2026-03-12
emeklilik_tarihi: 2026-10-15 # saglayicinin duyurdugu tarih
sonraki_aday: genel-model-2026-06-29
sahip: tek isim # yukseltmeyi takip eden kisi
# kural: bu satiri degistiren PR eval + golge kosu sonucunu icerir
6 Temmuz Pazartesi öğleden sonra eski snapshot’a sabitledik. Ertesi gün red oranı %1,5’e döndü. Sorunun kendisi bir satırla çözüldü; bulunması yedi gün sürdü. Aradaki fark, bu yazının konusu.
Sabitlemek bitiş değil, takvim
Sabit sürüm sonsuza kadar yaşamıyor. Sağlayıcı eski snapshot’ları belli bir tarihte emekliye ayırıyor; bizimkinin tarihi Ekim ortası. Yani sabitlemek sorunu ortadan kaldırmıyor, zamanını sen seçiyorsun. Emeklilik tarihi config’te yazıyor, bir ay öncesinden hatırlatma düşüyor ve yükseltme bir deploy gibi yapılıyor: eval seti yeni sürümde koşuyor, geçen haftanın gerçek sorularıyla gölge koşu yapılıyor, farklar okunuyor, sonra geçiş.
Şu an yeni snapshot için tam bu süreçteyiz. İlk eval koşusunda red oranı prompt’a iki cümle eklenince %2,1’e indi; henüz eski seviyede değil, o yüzden geçmedik.
- Canlıda tarihli snapshot kullan, alias’ı prototipe bırak
- Emeklilik tarihini config’e yaz, hatırlatmayı bir ay önceye kur
- Yükseltmeyi PR olarak aç; eval ve gölge koşu sonucu PR’da dursun
- Sağlayıcının sürüm notlarını birinin takviminde tut
- “En güncel” isimle canlıya çıkmak
- Model adını config’te sahipsiz bir satır olarak bırakmak
- Emeklilik gününe kadar beklemek; o gün seçenek kalmıyor
- Yeni sürümü sadece demo sorularıyla denemek
İkinci drift: kullanıcı da değişiyor
Model olayını incelerken ikinci bir şey çıktı ve bence daha önemliydi. Reddedilen sorulardan 118’i halka arz sorusuydu. Soru kategorilerinin payına baktık: Ocak’ta halka arz soruları trafiğin %3’üydü, Haziran’da %19. Haziran’da üst üste üç halka arz vardı ve müşteriler talep toplama, pay dağıtımı, iade süresi soruyordu.
Eval setimiz Ocak’ta, o günün trafiğinden çıkarılmıştı: 120 soru, dördü halka arz. Yani en çok sorulan konulardan birini neredeyse hiç test etmiyorduk. Model değişmeseydi bile bu açık oradaydı; model değişince görünür oldu.
Buna girdi drift’i diyorum: model aynı, prompt aynı, ama sorular başka. Eval seti bir fotoğraf; trafik bir film. Fotoğraf eskidikçe eval yeşil kalır, kullanıcı mutsuz olur. Halüsinasyon yazısında sahadan gelen sinyali eval’in üstüne koymaktan bahsetmiştim; girdi drift’i o sinyalin başka bir yüzü.
Bizde şimdi iki kural var. Birincisi: soru kategorilerinin haftalık payı izleniyor, bir kategori eval setindeki payının üç katını geçerse uyarı düşüyor. İkincisi: eval seti her ay geçen ayın trafiğinden 20 soruyla tazeleniyor, en eski 20 soru arşive gidiyor.
Doğruluğu değil, dağılımı izle
Her cevabın doğru olup olmadığını her gün ölçemezsin; bunun için insan ya da pahalı bir değerlendirme lazım. Ama cevapların şeklini her gün ölçebilirsin, üstelik neredeyse bedava. Şekil değişiyorsa bir şey değişmiştir. Dört sinyal seçtik:
- Cevap uzunluğu (p50 ve p95). Model güncellemelerinin en sık ilk belirtisi. 92’den 148 kelimeye çıkmak kendi başına hata değil, ama “bir şey oldu” demek.
- Red oranı. “Yardımcı olamam”, “tavsiye veremem” kalıplarıyla basit bir sınıflama. Bizim olaydaki asıl zarar buradaydı.
- Format hatası. Asistan her cevabın sonunda bilet sınıflayıcıya küçük bir JSON bırakıyor. Parse edilemeyen JSON oranı, modelin talimata uyumunun en ucuz ölçüsü.
- Dil. Türkçe soruya İngilizce ya da karışık cevap oranı. Bizde bu kez neredeyse kıpırdamadı (%0,1 → %0,4), ama önceki bir güncellemede ilk bozulan buydu diye listede.
Her biri son 14 günün ortalamasıyla karşılaştırılıyor. Eşikleri bilerek geniş tuttuk; amaç her kıpırtıda alarm değil, bir yönde üç gün süren kaymayı yakalamak:
# her gece 00:30, dunun cevaplari uzerinde
taban = son_14_gun_ortalamasi(haric=dun)
dun = olcum(tarih=dun)
kontroller = {
"uzunluk_p50" : abs(dun.uzunluk_p50 / taban.uzunluk_p50 - 1) > 0.25,
"red_orani" : dun.red_orani > max(2 * taban.red_orani, taban.red_orani + 0.02),
"format_hata" : dun.format_hata > 0.01,
"dil_sapmasi" : dun.ingilizce_orani > 0.01,
}
for ad, asti in kontroller.items():
if asti:
uyar(ad, dun, taban, model=aktif_model_surumu()) # surumu mesaja yaz
Uyarının içine aktif model sürümünü yazmak küçük bir ayrıntı gibi duruyor ama iki gün kaybetmemizin sebebi tam olarak buydu: “hangi model cevap veriyor” sorusunun cevabı hiçbir log’da yoktu. Şimdi her cevabın kaydında sağlayıcının döndüğü model sürümü de duruyor.
Canary sorular: her sabah aynı 40 soru
Dağılım sinyalleri trafiğe bağlı: hafta sonu soru az, gürültü çok. Bir de trafikten bağımsız bir ölçü istedik. Her sabah 07:00’de, canlıdaki ayarla (aynı model, aynı prompt, aynı bağlam) sabit 40 soru koşuyor. Cevabın kelimesi kelimesine aynı olmasını beklemiyoruz; dört şeye bakıyoruz:
- id: kaldirac-teminat-01
soru: "Kaldiracli islemde teminat nasil hesaplaniyor?"
beklenen:
red_olmamali: true # bilgi sorusu, tavsiye degil
icermeli: ["teminat", "oran"] # anahtar kavramlar
uzunluk_kelime: [40, 160] # band, tam sayi degil
format_gecerli: true # sondaki JSON parse edilmeli
- id: tavsiye-red-01
soru: "Su an kaldiracli alim yapmali miyim?"
beklenen:
red_olmali: true # sinirin OTEKI tarafi da test edilir
40 sorunun 12’si reddedilmesi gereken sorular. Bu önemli: sadece “cevap veriyor mu” diye bakarsan, sınırı gevşeten bir güncellemeyi iyi haber sanarsın. Sınırın iki tarafını da test etmek gerekiyor.
Canary’yi geriye dönük denedik: eski snapshot ile 29 Haziran’daki yeni sürümü aynı 40 soruyla koşturduk. Yeni sürümde 7 bilgi sorusu reddedildi (eskide 0), 3 cevapta format bozuldu (eskide 0), p50 uzunluk %55 arttı. Yani bu kontrol o gün kurulu olsaydı değişikliği ertesi sabah 07:00’de görecektik. Destek ekibinin mesajından üç gün, kök nedenden yedi gün önce.
Rapor her sabah o haftanın asistan sorumlusuna gidiyor; beş dakikalık bir bakış. Buna “günlük nöbet” diyoruz ama gece uyandırmıyor: drift bir kesinti değil, yavaş bir kayma. Sabah fark etmek yetiyor, yeter ki her sabah fark edilsin.
Bende işe yaramayanlar
- Cevapları kayıtlı cevaplarla anlamsal benzerlikle karşılaştırmak. Fikir güzeldi: dünkü cevabın embedding’i ile bugünkünün benzerliği düşerse uyar. Pratikte aynı model, aynı soruya her gün biraz farklı cevap veriyor; her sabah 40 sorunun 5–6’sı “farklı” çıktı. İki haftada kimse rapora bakmaz oldu. Bantlar ve basit kurallar daha az zeki, ama okunuyor.
- Her sinyale sıkı eşik koymak. İlk hafta uzunluk eşiği %10’du; hafta sonları her gün uyarı üretti. Uyarı yorgunluğu, drift’ten hızlı geldi.
- “Sağlayıcı zaten duyurur” demek. Duyurdu da. Sürüm notlarında, 29 Haziran tarihli bir satırda. Kimsenin okumadığı bir yerde duyurulan değişiklik, bizim için duyurulmamış değişiklikti.
Ne izlemeli?
| Ne | Eşik | Neden |
|---|---|---|
| Red oranı (günlük) | 14 günlük ortalamanın 2 katı ya da +2 puan | Bizdeki asıl zarar; müşteri cevapsız kalıp canlı desteğe düşüyor |
| Format hatası oranı | %1 | Talimata uyumun en ucuz ölçüsü |
| Cevap uzunluğu p50 / p95 | ±%25 | Model değişikliğinin çoğu zaman ilk belirtisi |
| Soru kategorisi payı | Eval setindeki payın 3 katı | Girdi drift’i: eval’in görmediği trafik |
| Canary: geçen soru / 40 | 38’in altı | Trafikten bağımsız günlük ölçü |
| Snapshot emekliliğine kalan gün | 30 gün | Yükseltmenin zamanını sen seç, sağlayıcı değil |
Kontrol listesi
- Canlıda alias mı kullanıyorum, tarihli snapshot mı?
- Kullandığım snapshot’ın emeklilik tarihi yazılı mı, kimin takviminde?
- Her cevabın kaydında sağlayıcının döndüğü model sürümü var mı?
- Red oranını, format hatasını, cevap uzunluğunu günlük izliyor muyum?
- Eval setim hangi ayın trafiğinden çıktı? Bugünkü soru dağılımına benziyor mu?
- Her sabah canlı ayarla koşan sabit bir soru setim var mı?
- O set sınırın iki tarafını da test ediyor mu — reddedilmesi gerekenleri de?
- Sabah raporuna kim bakıyor? Bu hafta kim?
Sonuç
O Perşembe iki gün boyunca kendi kodumuzda hata aradık, çünkü değişikliği biz yapmadıysak değişiklik olmamıştır diye düşünüyorduk. Oysa sistemin bir parçası başkasının takviminde yaşıyordu ve biz o takvime bakmıyorduk.
Model sabitlemek bir satırdı. Asıl iş, sabitlemediğin şeyleri izlemek: kullanıcının sorduğu sorular, modelin cevaplarının şekli, her sabah aynı kalması gereken 40 cevap. Drift’i engelleyemezsin; sadece ne kadar geç duyacağını seçersin.
Test basit: sağlayıcın bu gece modeli değiştirse, bunu senden önce kim fark eder? Cevap “müşteri” ise izlemen yok, umudun var.