Sertaç Yıldırım saha notları

Ana sayfa → Teknik

Prompt Loglamak: Her Şeyi Değil, Güvenle

Perşembe 16:40. Asistanın yanlış bir cevabını ayıklarken log ekranında bir satır açıyorum: müşterinin adı soyadı, TC kimlik numarası, IBAN’ı ve “annemin hesabına 48.000 TL göndereceğim” cümlesi. Bu müşterinin kim olduğunu bilmeme gerek yoktu. O ekrana erişimi olan 23 kişinin de.

Özet
  • Prompt log’u, ikinci bir müşteri veritabanıdır. Ama ne yetkisi, ne saklama süresi, ne de erişim kaydı veritabanı gibi yönetilir.
  • Kişisel verinin çoğunu log’a biz koymuştuk. IBAN geçen sohbetlerin %91’inde kaynak müşteri değil, bizim prompt’a eklediğimiz hesap özetiydi.
  • Maskeleme log’a yazmadan önce, uygulamanın içinde. Log platformunda maskelemek geç kalmaktır; ham metin o noktaya kadar kuyruktan ve diskten geçmiştir.
  • Düz hash maskeleme değildir. TC kimlik numarasının uzayı küçük; anahtarsız özet birkaç dakikada geri çözülür.
  • Her şeyi değil, iki katman. Metadata her istekte; maskeli içerik örneklenerek ve 30 gün. Ham metin log’da hiç yok.
  • Maskeleme kuralı değil, maskeleme testi. Her gece log’u tarayan bir iş, kuralın kaçırdığını buluyor. İlk hafta 14 kaçak çıktı.

Sahadan: log ekranındaki TC kimlik numarası

Bu log’u açan bendim. Destek asistanının gecikme sorunlarını çözerken prompt’un neden 6.000 token olduğunu görmek istedik ve her isteğin tam prompt’unu ve tam cevabını log platformuna yazmaya başladık. Tek satırlık bir değişiklikti. Saklama süresi platformun varsayılanıydı: 90 gün. Erişim de varsayılandı: bütün mühendislik ekibi, iki analist ve bir dış danışman, toplam 23 kişi.

O perşembe log satırını görünce ilk işim son 14 günü taramak oldu. 41.200 sohbet vardı. Sonuç:

Ne geçiyorSohbetOranNereden geldi
TC kimlik no (sağlama hanesi geçerli)2.310%5,6Müşteri kendisi yazmış: “TC’m şu, kontrol eder misin?”
IBAN9.800%23,88.900’ünde bizim hesap özetimiz, 900’ünde müşteri
Kart numarası (Luhn geçerli)37%0,1Müşteri yapıştırmış

İkinci satır canımı en çok sıkan satır oldu. Prompt’a eklediğimiz hesap özetinde müşterinin tam IBAN’ı vardı. Neden? Çünkü “model belki lazım eder” demiştim. Model bir kez bile IBAN’ın tamamına ihtiyaç duymamıştı. Üstelik ironik bir ayrıntı vardı: cevaptaki hesap numaralarını ekranda maskeleyen bir çıktı filtremiz vardı. Müşterinin ekranı temizdi. Log’a ise filtreden önceki hâl yazılıyordu.

Hesap basitti. 90 gün dolduğunda log’da yaklaşık 265 bin sohbet birikmiş olacaktı ve her birine 23 kişi bakabilecekti. Kimsenin kötü niyeti yoktu. Sadece kimse bu log’u bir müşteri veritabanı gibi düşünmemişti.

Prompt log’u ikinci bir müşteri veritabanıdır — sadece kimse ona veritabanı gibi davranmaz.

Log neden lazım, neden tehlikeli

Log’u kapatmak kolay çözüm gibi görünüyor. Değil. LLM özelliğinde hata ayıklamanın üç sorusu var ve üçü de içeriğe bakmadan cevaplanmıyor: model ne gördü, ne söyledi, neden? “Asistan komisyonu yanlış hesapladı” şikâyetini prompt’u görmeden çözemezsin; belki model yanlış yaptı, belki biz ona eski tarife tablosunu verdik.

Öte yandan klasik bir servisin log’undan farklı bir şey var: LLM’in girdisi serbest metin. Klasik serviste hangi alana ne yazıldığını sen belirlersin; kişisel veriyi alan adıyla bulup maskelersin. Sohbette müşteri kimlik numarasını, annesinin adını, borcunu, hastalığını istediği yere yazabilir. Alan adı yok, tahmin var.

KVKK’nın genel ilkeleri burada iki cümleye iniyor: veri, işlendiği amaçla bağlantılı, sınırlı ve ölçülü olmalı; ve amacın gerektirdiği süreden uzun tutulmamalı. “Hata ayıklamak” meşru bir amaç. “Hata ayıklamak için her müşterinin her mesajını 90 gün boyunca 23 kişiye açık tutmak” ölçülü değil.

Önce kaynağı kurut

İlk düzeltme maskelemeyle değil, prompt’la ilgiliydi. Hesap özetinin her alanına tek bir soru sorduk: model bunu cevap üretmek için gerçekten kullanıyor mu?

Önce hesap özetinde
  • Ad soyad
  • TC kimlik numarası
  • Tam IBAN
  • Doğum tarihi
  • Son 20 işlem, tutarlarıyla
Sonra hesap özetinde
  • Hitap için yalnızca ad
  • Kimlik numarası yok
  • IBAN’ın son 4 hanesi (“…4821 biten hesabın”)
  • Yaş grubu, doğum tarihi değil
  • Soruyla ilgili son 5 işlem

Cevap kalitesinde bir fark görmedik; aynı hafta olumsuz geri bildirim oranı yerinde durdu. IBAN geçen sohbetlerin 8.900’ü tek hamlede sıfıra indi. Bu maskeleme değil, daha iyisi: hiç yazılmayan veri sızmaz.

En iyi maskelenmiş veri, prompt’a hiç girmemiş veridir.

Maskeleme: log’a yazmadan önce

Müşterinin kendi yazdıklarını kaynağında kurutamazsın; onları maskelemek zorundasın. Asıl karar maskelemenin nerede yapılacağı. Bizde cevap: uygulamanın içinde, log kütüphanesinin kendisinde. Geliştirici maskelemeyi hatırlamak zorunda değil; log’a giden her metin aynı fonksiyondan geçiyor.

Maskeleme: loga yazmadan once
TCKN = re.compile(r"\b[1-9](?:\s?\d){10}\b")          # bosluklu yazilani da yakala
IBAN = re.compile(r"\bTR\d{2}(?:\s?\d{4}){5}\s?\d{2}\b", re.IGNORECASE)
KART = re.compile(r"\b(?:\d[ -]?){15,16}\b")

def tckn_gecerli(s):
    d = [int(c) for c in s if c.isdigit()]
    onuncu = ((d[0]+d[2]+d[4]+d[6]+d[8]) * 7 - (d[1]+d[3]+d[5]+d[7])) % 10
    return d[9] == onuncu and d[10] == sum(d[:10]) % 10   # rastgele 11 hanenin ~%1'i gecer

def belirtec(tur, deger):
    # duz hash DEGIL: anahtarli ozet. anahtar log erisimi olanlarda yok.
    ozet = hmac.new(MASKE_ANAHTARI, normalize(deger), "sha256").hexdigest()[:8]
    return f"<{tur}:{ozet}>"          # ayni musteri, ayni belirtec: iz surulebilir

def maskele(metin):
    metin = TCKN.sub(lambda m: belirtec("TCKN", m[0]) if tckn_gecerli(m[0]) else m[0], metin)
    metin = IBAN.sub(lambda m: belirtec("IBAN", m[0]), metin)
    metin = KART.sub(lambda m: belirtec("KART", m[0]) if luhn(m[0]) else m[0], metin)
    return metin

log.icerik(sohbet_id, prompt=maskele(prompt), cevap=maskele(cevap))

Kodda üç karar var ve her biri bir hatadan geliyor.

Sağlama hanesi kontrolü. 11 haneli her sayıyı maskelersen emir numaralarını da maskelersin ve hata ayıklayamazsın. TC kimlik numarasının iki sağlama hanesi var; rastgele 11 haneli bir sayının yaklaşık %1’i bu kontrolden geçer. 11 haneli emir numaralarımızın da %1’i gereksiz yere maskeleniyor. Bunu şöyle çözdük: emir numarası, müşteri numarası gibi kayıt numaraları serbest metinden değil, ayrı bir yapılandırılmış alandan açık olarak yazılıyor. Serbest metindeki kopyası maskelense de bir şey kaybetmiyoruz.

Anahtarlı özet. İlk sürümde TC kimlik numarasını düz SHA-256 ile özetliyorduk ve bunu maskeleme sanıyorduk. Güvenlik ekibinden bir arkadaş bir öğle arasında geri çözdü. İlk dokuz hane gerisini belirlediği için 900 milyon aday var; sıradan bir dizüstü bilgisayar bunların hepsini dakikalar içinde deniyor. Düz hash, küçük bir uzayda sadece yavaş bir arama tablosudur. Anahtarlı özette (HMAC) anahtar olmadan deneme yapılamıyor; anahtar, log’a erişimi olan hiç kimsenin erişemediği bir yerde duruyor.

Belirteç, silme değil. <TCKN:7f3a91c2> müşterinin kim olduğunu söylemiyor ama aynı müşterinin üç farklı sohbetini birbirine bağlayabiliyor. “Bu hata hep aynı müşteride mi çıkıyor?” sorusu için bu yeterli.

Kodun yakalayamadığı şeyler de var ve bunu açıkça söylemek gerek: isimler, adresler, “annemin hastalığı” gibi serbest cümleler. Bunları regex’le yakalamaya çalışmadık; aşağıdaki iki katman ve erişim kuralları onlar için var.

Her şeyi değil: iki katman ve örnekleme

Log’u ikiye böldük. Birincisi her istekte yazılıyor ve içinde müşterinin tek bir kelimesi yok. İkincisi içerik taşıyor ama maskeli, örneklenmiş ve kısa ömürlü.

KatmanNe varHangi isteklerSaklama
MetadataSüre, token sayıları, model ve prompt sürümü, maskelenen alan sayısı, kayıt numaralarıHepsi13 ay
Maskeli içerikPrompt ve cevap, maskeleme sonrasıRastgele %5 + olumsuz geri bildirim, temsilciye aktarma, hata, filtre tetiklenen sohbetlerin tamamı30 gün
Ham içerikHiçbiriLog’da yok

Örneklemenin mantığı basit: hata ayıklamaya ihtiyaç duyduğun sohbetler rastgele dağılmıyor. Şikâyetler olumsuz geri bildirimden, temsilciye aktarılan sohbetlerden ve hatalardan geliyor; bunların hepsini tutuyoruz. Rastgele %5 ise “normal görünen” sohbetlerde gizli bir sorun var mı diye bakmak için. Toplamda sohbetlerin yaklaşık %11’i içerik log’una giriyor. 30 günlük saklamayla log’da herhangi bir anda 265 bin değil, yaklaşık 9.700 sohbet var.

Metadata’nın 13 ay tutulmasının sebebi trend: “prompt son altı ayda ne kadar büyüdü” sorusu içerik değil, sayı istiyor. İçinde müşteri metni olmadığı için uzun tutmanın bir bedeli yok.

Kim ne görür

RolMetadataMaskeli içerikHam metin
Bütün mühendislikEvetHayırHayır
Asistan ekibi (4 kişi)EvetEvetOnaylı talep ile
Destek ekip lideriEvetEvetHayır (sohbet ekranında zaten görüyor)
Güvenlik (1 kişi)EvetEvetTalebi onaylayan kişi

İçeriğe erişen kişi sayısı 23’ten 6’ya indi. Dış danışmanın erişimi tamamen kalktı. Her içerik görüntülemesi ayrıca kaydediliyor: kim, hangi sohbete, ne zaman baktı. Bu kaydın iki işi var. Birincisi caydırıcı. İkincisi, biri “müşteri verisine kim erişebiliyor?” diye sorduğunda cevabı bir cümle değil, bir liste olarak verebiliyoruz — güvenlik denetimi yazısında anlattığım kanıt mantığının aynısı.

Ham metin gerçekten lazım olduğunda

Maskeleme bazen hatanın kendisini siler. Yeni düzenin ilk haftasında bir hata geldi: asistan boşluklu yazılmış IBAN’ları tanımıyordu. Maskeli log’da bütün IBAN’lar <IBAN:…> olarak göründüğü için sorun görünmüyordu.

Buradaki kilit fikir şu: ham metin zaten bir yerde var. Sohbet veritabanında, müşteri verisi kurallarıyla, kendi saklama süresi ve yetkileriyle. Log’un onu ikinci kez tutmasına gerek yok. Ham metin gerektiğinde, asistan ekibinden biri sohbet numarasıyla bir talep açıyor, güvenlik onaylıyor, tek sohbet 24 saatliğine açılıyor ve erişim kaydediliyor. Yeni düzenin ilk iki haftasında gelen 14 hata bildiriminin 12’si maskeli log’la çözüldü, 2’si için bu yol kullanıldı.

Bende işe yaramayanlar

  • Log platformunda maskelemek. İlk refleks buydu: platformun giriş hattına bir maskeleme kuralı. Ham metin o hatta gelene kadar mesaj kuyruğundan geçiyordu ve kuyruğun kendi 7 günlük saklaması vardı. Yedekler de ayrı bir konu. Maskeleme, verinin çıktığı ilk yerde yapılmazsa her ara durak bir kopya.
  • Kişisel veriyi başka bir modele buldurmak. “Bir model metni okusun, kişisel veriyi işaretlesin” denedik. Her isteğe 400 ms ekledi, maliyeti ikiye katladı ve işin ironisi, müşteri metnini maskelemek için onu bir modele daha gönderiyorduk. Regex ve sağlama kontrolü sıkıcı ama hızlı, ucuz ve ölçülebilir.
  • “Şimdilik hepsini tutalım, sonra temizleriz.” Birikmiş log’u geriye dönük temizlemeyi iki kez planladık, ikisinde de ertelendi. Sonunda log’u tamamen silip yeni kurallarla sıfırdan başladık. Sonra temizlenecek log, temizlenmez.

Ne izlemeli?

Maskeleme bir kural olarak yazılır ama bir test olarak işler. Her gece maskeli log’u aynı desenlerin daha gevşek hâlleriyle tarayan bir iş kurduk. İlk hafta 14 kaçak buldu: boşluklu yazılmış kimlik numaraları, küçük harfle başlayan IBAN’lar. Hepsi düzeltildi ve yukarıdaki kodun bugünkü hâli o 14 kaçağın sonucu.

NeNeden
Gece taramasında bulunan kaçak sayısıHedef sıfır; sıfır değilse her biri bir iş kaydı, desen güncellenir
İstek başına maskelenen alan sayısıAniden düşerse maskeleme bozulmuş olabilir; aniden artarsa prompt’a yeni veri girmiştir
İçerik log’una giren sohbet oranı%11 civarı beklenir; artarsa örnekleme kuralı gevşemiştir
İçerik görüntüleme sayısı, kişi başınaHata ayıklama dışında bir bakış alışkanlığı varsa burada görünür
Ham metin talebi sayısıArtıyorsa maskeli log hata ayıklamaya yetmiyor; hangi hatada yetmediğine bak
Prompt’taki alan listesinin değişimiHesap özetine eklenen her yeni alan, gözden geçirmesiz log’a giren yeni veridir

Kontrol listesi

Prompt log’un güvenli mi?
  • Prompt’a koyduğum her alanı model gerçekten kullanıyor mu?
  • Maskeleme log’a yazmadan önce mi yapılıyor, log platformunda mı?
  • Maskelemeyi geliştirici mi hatırlıyor, log kütüphanesi mi yapıyor?
  • Kimlik numaralarını düz hash’le mi, anahtarlı özetle mi saklıyorum?
  • Metadata ile içerik ayrı mı? İçerik örnekleniyor mu?
  • İçerik log’unun saklama süresi kaç gün ve bunu kim seçti?
  • İçeriğe kaç kişi erişebiliyor? Aralarında dışarıdan biri var mı?
  • Kim hangi sohbete baktı — bunun kaydı var mı?
  • Ham metin gerektiğinde izlenecek yol yazılı mı, yoksa biri veritabanına mı bağlanıyor?
  • Maskelemenin kaçırdığını her gece arayan bir iş var mı?
  • Model sağlayıcısı prompt’ları kendi tarafında ne kadar süre tutuyor?

Sonuç

O perşembe log’da okuduğum satırı hâlâ hatırlıyorum, çünkü içindeki bilgiyi oraya koyan bendim. Müşteri kendi kimlik numarasını yazmıştı, evet. Ama IBAN’ı prompt’a ben eklemiştim, log’u ben açmıştım, 90 günü ve 23 kişiyi varsayılan olarak ben bırakmıştım.

Çözüm log’u kapatmak değildi. Hata ayıklamak hâlâ içerik istiyor. Çözüm log’a bir müşteri veritabanı gibi davranmaktı: gereksiz veriyi hiç koymamak, koyduğunu yazmadan maskelemek, azını ve kısa süre tutmak, kimin baktığını bilmek.

Test şu: log ekranını bugün bir müşterine göstersen, orada kendisi hakkında ne okurdu? Cevap “hata ayıklamak için gerekenden fazlası” ise log’un sana değil, bir sonraki sızıntıya çalışıyor.