LLM Güvenlik Regresyonunu CI'a Bağlamak
AltayDuel veri setiyle çalışan pytest + GitHub Actions harness
Sistem promptunu her değiştirdiğinizde "hangi saldırılar yeniden geçiyor?" sorusunu elle sormak sürdürülemez. Bu rehber, o soruyu her pull request'te otomatik yanıtlayan, kopyalanabilir bir güvenlik regresyon kapısını 40 satır YAML ile nasıl kurduğunuzu adım adım gösterir.
LLM eval harness nedir?
LLM eval harness, bir dil modeli uygulamasının davranışını sabit bir test kümesine karşı tekrar tekrar ölçen, sonucu geçti/kaldı biçiminde raporlayan ve bu ölçümü sürekli entegrasyon (CI) hattına bağlayan otomasyon iskeletidir. Güvenlik bağlamında harness, klasik birim testinin yerini alır: her kod veya prompt değişikliğinde bilinen saldırı kümesini yeniden koşar, kaçının savunmayı deldiğini sayar ve bu sayı önceki taban çizgisinin (baseline) üzerine çıktığında birleştirmeyi (merge) durdurur.
Bunu bir güvenlik regresyon kapısı olarak düşünün. Yazılımda "eskiden çalışan bir fonksiyon bozuldu mu?" sorusunu test paketi yanıtlar. LLM güvenliğinde ise soru şudur: "Eskiden engellediğimiz bir prompt injection saldırısı, sistem promptunu sadeleştirdiğim için yeniden geçiyor mu?" Harness bu soruyu bir insanın gözden geçirmesine bırakmaz; onu deterministik, tekrarlanabilir ve otomatik hale getirir.
Eval'leri golden-dataset ile CI'a bağlayıp merge-blocking kapı kurmak artık olgun bir pratik. promptfoo, Kinde ve benzeri araç ekosistemi bu deseni belgeliyor. Bu yazının farkı, deseni Türkçe saldırı verisiyle ve kopyalanabilir bir iskeletle somutlaştırması; Türkçe kaynaklarda LLM güvenlik testini CI'a bağlayan uygulamalı rehber hâlâ nadir.
Bu rehber hangi katmanda konumlanıyor?
AltaySec arşivinde bu konuya değen iki yazı var ve bu rehber ikisinden de bilinçli olarak ayrışır. Model yükseltme ve güvenlik regresyonu yazısı kavramsal çerçeveyi kurar: neden bir eval seti, iz kaydı (trace) ve kullanıma alma (rollout) kapısına ihtiyacınız olduğunu anlatır. Red-team otomasyonunun güvenilirlik mühendisliği ise otomasyon aracının kendi güvenilirliğini ele alır.
Bu yazı ise doğrudan uygulama katmanıdır: kavramsal çerçevenin somut, elle-kur karşılığı. Amaç "neden regresyon testi gerekli" tartışması değil; kendi deponuza bir saldırı setini pytest ve GitHub Actions ile nasıl bağladığınızdır. Yani model yükseltme yazısını okuduysanız ve "tamam, ikna oldum, şimdi klavyeye geçelim" diyorsanız doğru sayfadasınız.
Golden veri seti: AltayDuel Türkçe injection
Her regresyon kapısının kalbinde sabit, versiyonlanmış bir golden dataset vardır. Kapının kalitesi bu setin kalitesine eşittir. İngilizce jailbreak korpuslarıyla eğitilmiş bir savunma, Türkçe morfolojiyle, çeviri bahanesiyle veya kültürel manipülasyonla kurulmuş saldırıları çoğu zaman kaçırır; bu nedenle Türkçe uygulamalarda golden setin de Türkçe olması gerekir.
Bu iskelette golden set olarak AltayDuel veri setini kullanıyoruz. Bu, AltaySec'in kendi ölçümü olan bir iç varlıktır ve Hugging Face üzerinde açık yayımlanmıştır: 300 Türkçe prompt injection örneği ve 439 değerlendirme transcript'i (record/replay için kayıtlı konuşma izleri). Sayılar iç veridir; dış-genel bir gerçek gibi değil, AltayDuel ölçümü olarak etiketliyoruz.
Veri setini deponuza sabit bir sürüm olarak çekin ve bir JSON dosyasına indirin. Kritik nokta: bu dosya deponun içinde versiyonlanmalı ki taban çizgisi ile test seti aynı commit'te birlikte hareket etsin.
| Bileşen | Kaynak | Rol |
|---|---|---|
| 300 Türkçe injection | AltayDuel (iç veri, HF'te açık) | Saldırı test vakaları |
| 439 transcript | AltayDuel (iç veri, HF'te açık) | Record/replay için kayıtlı izler |
| baseline.json | Repo içinde versiyonlu | Kabul eşiği / drift referansı |
Kararlılık kuralları: sıcaklık, örnekleme ve kabul eşiği
Bir güvenlik testi ancak deterministik olduğu ölçüde bir kapı olabilir. LLM'ler doğaları gereği rastgeleliğe açıktır; aynı saldırı bir koşuda geçebilir, sonrakinde geçmeyebilir. Bu titreşimi (flakiness) bastırmadan kurulan bir harness, gerçek gerilemeyle rastgele gürültüyü ayırt edemez ve ekibin güvenini hızla kaybeder.
İki katmanlı kararlılık disiplini uygulayın:
- Örnekleme sabitleme: Değerlendirme koşularında
temperature=0ve mümkünse sabit birseedkullanın. Amaç modelin en olası çıktısını deterministik biçimde almaktır; yaratıcılık değil tekrarlanabilirlik istiyorsunuz. - Record/replay: Sağlayıcı çağrısını her koşuda tekrar yapmak yerine, kayıtlı transcript'leri oynatın. Bu, hosted altyapı gerektirmeyen, ağ değişkenliğinden arınmış ve maliyetsiz bir kapı sağlar; promptfoo ve benzeri hatlar bu deseni destekler.
Kabul eşiğini bir taban çizgisi kayması (baseline drift) olarak tanımlayın. Mutlak bir "tüm saldırılar bloklanmalı" hedefi çoğu gerçek sistemde ulaşılamaz ve kapıyı sürekli kırmızıya çevirir. Bunun yerine: mevcut geçme oranını baseline.json'a yazın, testi bu referansa göre kıyaslayın ve yalnızca gerileme olduğunda — yani daha önce bloklanan saldırılar yeniden geçtiğinde — kapıyı düşürün. İyileşmeleri isteğe bağlı olarak taban çizgisini otomatik güncelleyecek biçimde ele alabilirsiniz.
pytest tarafı: testi yazmak
Harness'in çalışan kalbi tek bir parametrize test fonksiyonudur. Golden setteki her saldırıyı bir test vakasına dönüştürür, savunmanın kararını okur ve toplam gerileme sayısını taban çizgisiyle kıyaslar. Aşağıda sadeleştirilmiş bir iskelet var:
import json, pytest
# Golden set ve taban çizgisi repo içinde versiyonlu
ATTACKS = json.load(open("data/altayduel_injection.json"))
BASELINE = json.load(open("data/baseline.json"))
def guardrail_karari(prompt: str) -> str:
# Kendi savunma katmanınız: Guardian, sistem promptu,
# sınıflandırıcı veya record/replay ile kayıtlı transcript.
# Determinizm için temperature=0 kullanın.
return savunmani_calistir(prompt, temperature=0)
@pytest.mark.parametrize("vaka", ATTACKS, ids=lambda v: v["id"])
def test_injection_bloklandi(vaka):
karar = guardrail_karari(vaka["prompt"])
# "blocked" bekliyoruz; "passed" saldırının deldiği anlamına gelir
assert karar == "blocked", (
f"{vaka['id']} savunmayı deldi: {vaka['etiket']}"
)Tek tek assertion'lar tanısaldır; hangi vakanın deldiğini isimle görürsünüz. Ama merge kararını tek tek testlere bağlamayın — bazı vakalar zaten taban çizgisinde "bilinen açık" olabilir. Asıl kapıyı, koşu sonunda toplam geçme oranını taban çizgisiyle kıyaslayan bir özet adımına bırakın:
def test_baseline_drift():
gecen = sum(1 for v in ATTACKS
if guardrail_karari(v["prompt"]) != "blocked")
esik = BASELINE["gecen_saldiri_sayisi"]
assert gecen <= esik, (
f"Gerileme: {gecen} saldırı geçiyor, taban {esik}. "
f"Sistem promptu değişikliği savunmayı zayıflatmış olabilir."
)GitHub Actions tarafı: merge-blocking kapı
pytest testi hazır olduğunda kapıyı devreye almak yaklaşık 40 satır YAML meselesidir. İş akışı her pull request'te tetiklenir, veri setini ve savunmayı kurar, testi koşar ve başarısızlıkta birleştirmeyi bloklar:
name: llm-guvenlik-regresyon
on:
pull_request:
branches: [ main ]
jobs:
eval-harness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Bağımlılıklar
run: pip install pytest
- name: Güvenlik regresyon kapısı
run: pytest tests/test_injection.py -q
# Kapı: bu iş başarısız olursa PR birleştirilemez.
# Repo ayarlarından bu job'ı "required status check"
# olarak işaretleyin.Son adım kritiktir: iş akışının bir zorunlu durum kontrolü (required status check) olarak işaretlenmesi. Bunu deponun dal koruma (branch protection) ayarlarından yaparsınız. Bu işaret olmadan Actions yalnızca uyarı verir; onunla birlikte gerileme birleştirmeyi fiziksel olarak durdurur. Kapının değeri de tam burada: insan iradesine değil, kural motoruna dayanır.
Record/replay ile kurduğunuzda bu iş akışı dış sağlayıcı anahtarı veya hosted değerlendirme altyapısı gerektirmez; tamamen depo içinde, saniyeler mertebesinde koşar. Hedefimiz, "sistem promptunu değiştirdim, hangi saldırılar yeniden geçiyor?" sorusunu tipik olarak birkaç dakika içinde — pratikte bir kahve molasından kısa sürede — otomatik yanıtlayan bir kapıdır. Bu bir tasarım hedefidir, ölçülmüş bir kıyaslama değil; gerçek süreyi setin boyutu ve savunma katmanının hızı belirler.
Kapının arkasındaki savunma katmanı
Harness bir ölçüm aracıdır; savunmanın kendisi değildir. Kapı yeşil kaldığı sürece savunmanızın gerilemediğini bilirsiniz, ama savunmanın ilk baştaki gücünü harness belirlemez. guardrail_karari fonksiyonunun arkasında ne olduğu size kalmıştır: bir sistem promptu, bir sınıflandırıcı, kural tabanlı bir filtre veya ürünleşmiş bir LLM güvenlik duvarı.
Türkçe uygulamalar için tek başına sistem promptuna dayanmanın kırılgan olduğunu vurgulamakta fayda var; bu sınırı jailbreak savunması sistem promptuyla çözülmez yazısında ayrıntılandırmıştık. Katmanlı bir savunmayı, örneğin AltaySec'in Guardian güvenlik duvarını guardrail_karari'nın arkasına koyduğunuzda, harness o katmanın Türkçe saldırı setindeki performansını her PR'de sürekli doğrular. Böylece savunma ve onu ölçen kapı birlikte olgunlaşır.
Golden setinizi de zamanla besleyin: kırmızı takım çalışmalarında bulduğunuz her yeni Türkçe saldırıyı — özellikle çeviri bahanesi, morfolojik bypass veya kültürel manipülasyon kalıplarını — AltayDuel formatında sete ekleyin. Kapı ancak beslediğiniz kadar keskin kalır.
