LLM güvenliği başlangıçta modelin kendisine bırakılmıştı. Sağlayıcılar modelleri alignment (RLHF vb.) ile eğitiyor, uygulama tarafında da system prompt’a kurallar yazılıyordu. Jailbreak’ler ve özellikle prompt injection bu yaklaşımın sınırını gösterdi, çünkü model instruction ile işlediği veriyi aynı context içinde ayırt edemiyor. Tool use ve agent’larla birlikte modele giren untrusted content ve modelin tetikleyebildiği aksiyonlar arttıkça kontrolü modelin dışına, runtime’da input ve output’u denetleyen ayrı bir katmana taşımak gerekti. Bu katmana guardrail diyoruz.
Bu yazıda guardrail’in uygulama akışında nereye yerleştiğini, hangi adımda hangi modellerin çalıştığını ve entegrasyon yöntemlerini Lakera Guard üzerinden, kod örnekleriyle anlatıyoruz. Pattern’lerin çoğu diğer guardrail ürünlerine de uygulanabilir.
Guardrail nedir?
Guardrail, LLM’in önünde ve arkasında çalışan, metni classify edip her threat kategorisi için bir score döndüren bağımsız bir servistir.
- Generative değil, discriminative modellerden oluşur. Metni input olarak alır ama metin üretmez, output olarak kategori ve confidence score verir. Tipik olarak encoder-based classifier’lar, NER modelleri, regex ve checksum validation birlikte kullanılır.
- LLM’in context’i dışında çalışır. Karar, korunan modelin instruction following davranışına bağlı değildir. System prompt’a “kötü niyetli istekleri reddet” yazmak, saldırganın hedef aldığı mekanizmanın kendisine güvenmek demektir.
- Low latency için tasarlanır. Her istekte senkron çağrılabilmesi için tek forward pass ile sonuç veren küçük modeller kullanılır. Büyük bir LLM’i judge olarak kullanmak (LLM-as-a-judge) bu latency budget’a genelde sığmaz.
- Policy decision point’tir, enforcement point değildir. Guardrail score’ları policy’deki threshold’larla karşılaştırıp
flaggedkararını döner. Block, mask veya sadece log aksiyonu uygulamanın sorumluluğundadır.
Kontrol noktaları
Basit bir chatbot’ta kullanıcı input’u ve model output’u olmak üzere iki kontrol noktası vardır. Tool kullanan agent mimarilerinde modelin tetiklediği tool call’lar ve tool’lardan dönen içerik de kontrol noktasına dönüşür.
flowchart LR
K([Kullanıcı]) -->|"① input"| APP[Uygulama]
APP <-->|prompt / response| LLM[LLM]
APP -->|"② tool call"| TOOL[Tools / API / MCP]
TOOL -->|"③ tool result"| APP
APP -->|"④ output"| K
G{{Guardrail}}
APP -. her noktada .-> G
| # | Nokta | Risk |
|---|---|---|
| ① | User input | Direct prompt injection, jailbreak, PII |
| ② | Agent’ın tool call’u | Data exfiltration, silme, privilege escalation gibi yetkisiz aksiyonlar |
| ③ | Tool result | Dış kaynaktan gelen indirect prompt injection |
| ④ | LLM output | Harmful content, PII ve system prompt leakage, phishing linkleri |
Genel kural olarak modelin context’ine giren ve modelin ürettiği her içerik bir kontrol noktasıdır.
Adımlar ve devreye giren modeller
Guardrail tek bir modelden oluşmaz. Her threat kategorisi için ayrı bir detector vardır ve hangi detector’ların hangi threshold ile çalışacağını policy belirler. Lakera Guard’daki detector ailelerini aşağıdaki tabloda bulabilirsiniz.
| Detector | Kapsam | Yöntem | Kontrol noktası |
|---|---|---|---|
| Prompt Defense | Direct ve indirect prompt injection, jailbreak | Multilingual classifier modeller (100+ dil) | ① ③ |
| Data Leakage Prevention | E-posta, IBAN, kredi kartı, isim, adres, IP | İsim ve adres gibi contextual entity’ler için ML modelleri, kart numarası ve IBAN için algorithmic validation (Luhn vb.), yapısal formatlar için pattern matching | ① ④ |
| Content Moderation | Hate, violence, sexual, crime, self-harm, profanity, weapons | Kategori bazlı content classifier’lar | ④ (gerekirse ①) |
| Malicious Links | Unknown ve phishing linkler | Bilinen domain listesi ve policy’deki allowlist | ④ ③ |
| Agent Behavior Defense | Agent’ın yetki alanı dışındaki tehlikeli tool call’lar, izin verilmeyen tool’lar | Tool call’u conversation history ile birlikte değerlendiren detector (beta) ve deterministik tool allow/deny list | ② |
| Custom Guardrails | Kuruma özel kurallar | Pattern (regex) ve semantic kurallar, tarama sırasında LLM çağrısı yapılmaz | İhtiyaca göre |
Bir Guard çağrısı kabaca şu akışla işlenir.
flowchart TB
REQ["POST /v2/guard<br/>messages + project_id"] --> POL[Projeye bağlı policy'yi yükle]
POL --> CH[Uzun metni chunk'lara böl<br/>paralel işle]
CH --> D1[Prompt Defense<br/>classifier]
CH --> D2[PII<br/>ML + validation + pattern]
CH --> D3[Moderation<br/>classifier'lar]
CH --> D4[Link kontrolü<br/>domain listeleri]
CH --> D5[Custom kurallar]
D1 & D2 & D3 & D4 & D5 --> TH[Score'ları policy'deki<br/>threshold'larla karşılaştır]
TH --> RES["flagged: true/false<br/>+ breakdown / payload"]
Burada dikkat edilmesi gereken iki nokta var.
- Mesaj role’ü hangi detector’ların çalışacağını etkiler. Policy’de detector’lar input (
user,tool) ve output (assistant) için ayrı tanımlanabilir. Örneğin content moderation sadece output’ta, PII her iki yönde çalışabilir.systemmesajları context olarak kullanılır. - Sensitivity policy’de tanımlanır. L1 (en düşük false positive) ile L4 (en düşük false negative) arasında seçilir ve kod değişikliği gerektirmez.
Guard API çağrısı
Aşağıdaki tüm entegrasyon yöntemleri aynı çağrıyı kullanır.
import os, requests
GUARD_URL = os.getenv("GUARD_URL", "https://api.lakera.ai/v2/guard")
HEADERS = {"Authorization": f"Bearer {os.getenv('LAKERA_GUARD_API_KEY')}"}
PROJECT_ID = "project-ornek-asistan"
def guard(messages, **opts):
r = requests.post(GUARD_URL, headers=HEADERS, timeout=3, json={
"messages": messages,
"project_id": PROJECT_ID,
"breakdown": True, # detector bazında sonuç
**opts,
})
r.raise_for_status()
return r.json()
Guard’ın döndüğü response şuna benzer.
{
"flagged": true,
"metadata": { "request_uuid": "01a15f8e-b7c9-7bad-a351-09d380117fa0" },
"breakdown": [
{ "detector_type": "prompt_attack", "detected": true, "result": "l1_confident" },
{ "detector_type": "pii/email", "detected": false, "result": "l5_unlikely" }
]
}
project_iduygulanacak policy’yi belirler. Uygulama ve environment başına ayrı proje tanımlamak policy’leri birbirinden izole eder.flaggedpolicy kararıdır,breakdownise her detector’ün sonucunu ve confidence seviyesini içerir.request_uuidisteğin Guard tarafındaki ID’sidir. Uygulama loguna yazılırsa Guard logları ve SIEM kayıtlarıyla korelasyon kurulabilir.- System prompt ayrı bir
systemmesajı olarak gönderilmelidir. Kullanıcı içeriğini template’lere gömmek yerine modelin gördüğü raw mesajları göndermek en doğru sonucu verir.
Entegrasyon yöntemleri
1. Asenkron monitoring (Detect modu)
İlk aşamada Guard çağrısı critical path’in dışında, asenkron yapılır ve sonuç loglanır. Proje Detect modunda çalıştığı için detector sonuçları breakdown‘da döner ama flagged her zaman false olur. Kullanıcı akışına latency eklenmez.
sequenceDiagram
participant K as Kullanıcı
participant U as Uygulama
participant G as Guard
participant L as LLM
K->>U: Mesaj
U-)G: input kontrolü (async)
U->>L: Mesaj
L-->>U: Response
U-)G: output kontrolü (async)
U-->>K: Response
G--)U: sonuç → log / SIEM
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(max_workers=8)
def guard_log(stage, messages):
try:
res = guard(messages)
levels = {b["detector_type"]: b["result"] for b in res["breakdown"]}
log.info("guard stage=%s uuid=%s results=%s", stage, res["metadata"]["request_uuid"], levels)
except Exception as e:
log.warning("guard stage=%s hata=%s", stage, e)
def chat(user_text):
msgs = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_text}]
pool.submit(guard_log, "input", msgs)
answer = call_llm(msgs)
pool.submit(guard_log, "output", msgs + [{"role": "assistant", "content": answer}])
return answer
Bu aşamada toplanan veriyle false positive’ler analiz edilir ve threshold’lar kalibre edilir. Lakera dokümantasyonuna göre kalibrasyon için yaklaşık 6.000 temsili örnek genelde yeterlidir.
2. Blocking (Enforce modu)
Kalibrasyondan sonra aynı çağrılar senkron yapılır ve proje Enforce moduna alınır. Kodda değişen tek şey sonuca göre akışın kesilmesidir.
sequenceDiagram
participant K as Kullanıcı
participant U as Uygulama
participant G as Guard
participant L as LLM
K->>U: Mesaj
U->>G: input kontrolü
alt flagged
G-->>U: true
U-->>K: Ret mesajı
else temiz
G-->>U: false
U->>L: Mesaj
L-->>U: Response
U->>G: output kontrolü
G-->>U: true / false
U-->>K: Response veya ret mesajı
end
BLOCKED = "Bu konuda yardımcı olamıyorum."
def chat(user_text):
msgs = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_text}]
if guard(msgs)["flagged"]:
return BLOCKED
answer = call_llm(msgs)
if guard(msgs + [{"role": "assistant", "content": answer}])["flagged"]:
return BLOCKED
return answer
Bu aşamada iki tasarım kararı verilmesi gerekir.
- Ret mesajı tespit edilen kategoriyi açık etmemelidir. Saldırgana hangi payload’ın yakalandığı bilgisini vermemek için generic bir mesaj kullanılır.
- Guard’a erişilemediğinde uygulamanın nasıl davranacağı tanımlanmalıdır. Fail-open availability’yi, fail-closed güvenliği önceliklendirir. Timeout değeri ve seçilen davranış uygulamanın risk profiline göre belirlenmelidir.
3. Input kontrolünü LLM ile paralel çalıştırma
Guard response süresi tipik olarak LLM inference süresinden çok daha kısadır. Input kontrolü LLM çağrısıyla paralel başlatılıp response kullanıcıya dönmeden önce sonucu beklenirse, eklenen latency pratikte sıfıra yaklaşır.
import asyncio
async def chat(user_text):
msgs = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_text}]
guard_task = asyncio.create_task(guard_async(msgs))
llm_task = asyncio.create_task(call_llm_async(msgs))
if (await guard_task)["flagged"]:
llm_task.cancel()
return BLOCKED
answer = await llm_task
if (await guard_async(msgs + [{"role": "assistant", "content": answer}]))["flagged"]:
return BLOCKED
return answer
Bu yöntemde flag’lenen input da LLM’e ulaşmış olur. PII’nin third-party bir LLM provider’a gitmemesi gerekiyorsa input kontrolü sıralı yapılmalıdır.
4. Tek çağrıyla interaction taraması
Lakera’nın önerdiği minimum entegrasyon, LLM response’undan sonra user mesajı ve response’u tek çağrıda göndermektir. Guard last interaction’ı (son user mesajı ve sonrasını) tarar, önceki mesajları context olarak kullanır.
def chat(history, user_text):
msgs = history + [{"role": "user", "content": user_text}]
answer = call_llm(msgs)
if guard(msgs + [{"role": "assistant", "content": answer}])["flagged"]:
return BLOCKED
return answer
Avantajı tek çağrı ve tam context, dezavantajı ise input’un LLM’e ulaştıktan sonra kontrol edilmesidir. Self-hosted kurulumlarda last interaction davranışı sürüme ve konfigürasyona göre farklılık gösterebileceği için user mesajının da tarandığı bir test isteğiyle doğrulanmalıdır.
5. Agent’lar ve tool call’lar
Agent mimarisinde Guard agent loop’un her adımında çağrılır. User isteği, agent’ın ürettiği tool call (execute edilmeden önce), tool result ve final response ayrı ayrı kontrol edilir. Tool definition’ları da (örneğin MCP handshake sırasında) tool poisoning’e karşı taranabilir.
sequenceDiagram
participant U as Uygulama / Agent
participant G as Guard
participant L as LLM
participant T as Tool
U->>G: user mesajı
U->>L: mesaj + tool definitions
L-->>U: tool_call (ör. export_records)
U->>G: assistant tool_call + tools
G-->>U: tehlikeli / yetki dışı mı?
U->>T: tool call'u execute et (temizse)
T-->>U: tool result
U->>G: tool result (indirect injection?)
U->>L: tool result
L-->>U: final response
U->>G: assistant response
def run_tool_step(msgs, tool_call, tools):
# 1) Execute etmeden önce tool call'u kontrol et
step = msgs + [{"role": "assistant", "content": "", "tool_calls": [tool_call]}]
if guard(step, tools=tools)["flagged"]:
return {"role": "tool", "tool_call_id": tool_call["id"], "content": "Bu işlem engellendi."}
# 2) Execute et, sonucu untrusted content olarak tara
result = execute(tool_call)
tool_msg = {"role": "tool", "tool_call_id": tool_call["id"],
"content": [{"type": "text", "text": result}]}
if guard(step + [tool_msg])["flagged"]:
return {"role": "tool", "tool_call_id": tool_call["id"], "content": "Tool sonucu güvenilir değil."}
return tool_msg
Bu adımda Lakera tarafında Agent Behavior Defense çalışır. Tool allow/deny list deterministiktir. Dangerous Deviation detector’ü ise tool call’u conversation history ve tools definition’larıyla birlikte değerlendirir ve user veya system mesajlarında gerekçesi olmayan tehlikeli aksiyonları (data exfiltration, privilege escalation, kalıcı resource silme) flag’ler. tool mesajlarından gelen instruction’lar gerekçe olarak kabul edilmez.
Canlıya alma adımları
flowchart LR
A[Detect modu<br/>async, result seviyeleri loglanır] --> B[Seviye dağılımından<br/>threshold seçimi]
B --> C[Enforce<br/>belirlenen threshold ile]
C --> D[Monitoring ve<br/>gerekirse threshold güncelleme]
Kalibrasyonda kullanılacak asıl veri breakdown içindeki result alanıdır. Guard her detector için policy’deki threshold’dan bağımsız olarak metnin gerçek seviyesini (l1_confident, l2_very_likely, l3_likely, l4_less_likely, l5_unlikely) döndürür. Bir threshold kendi seviyesini ve daha güçlü seviyeleri flag’lediği için, örneğin L3 threshold’u l1, l2 ve l3 sonuçlarını yakalar. Detect modunda bu seviyeler loglanırsa her threshold’un gerçek trafikte ne kadar flag üreteceği Enforce’a geçmeden hesaplanabilir.
LEVELS = ["l1_confident", "l2_very_likely", "l3_likely", "l4_less_likely"]
def flagged_at(results, threshold):
"""results: Detect loglarından bir detector'ün result değerleri"""
allowed = LEVELS[: LEVELS.index(threshold) + 1]
return sum(1 for r in results if r in allowed)
for t in LEVELS:
print(t, flagged_at(results, t))
- Detect modunda başlanır ve her istekte detector bazında
resultseviyeleri loglanır. - Flag’lenen örnekler seviyelerine göre incelenir, her threshold’daki false positive ve false negative oranı çıkarılır. Türkçe gibi dillerde ve kısa mesajlarda davranış bu aşamada kendi verinizle test edilmelidir.
- Enforce, veriyle belirlenen threshold ile açılır.
- Enforce sonrasında flag’ler izlenir ve threshold gerekirse güncellenir.
Detect verisi azsa veya gerçek kullanımı yansıtmıyorsa Enforce’u en düşük sensitivity ile (L1) açıp threshold’u kademeli olarak yükseltmek daha güvenli bir alternatiftir.
Mod ve threshold policy tarafında yönetildiği için bu adımlar kod değişikliği gerektirmez. Bu nedenle entegrasyon baştan output kontrolünü de içerecek şekilde kurulmalıdır.