LLM & Agent Production Mühendisliği — Principal Seviye Rehber
Notebook’ta “çalışıyor” olması ile production’da çalışması arasındaki fark tam olarak bu dokümanın konusu: LLM engineering, agent reliability, evaluation ve observability. Geri kalan her şey (framework’ler, prompt kütüphaneleri, wrapper SDK’lar) bu dört sütunun üzerine giydirilmiş süsten ibaret.
İçindekiler
- LLM Engineering
- 1.1 Prompt Design
- 1.2 Structured Generation
- 1.3 Tool Calling
- 1.4 Context Engineering
- 1.5 Model Selection
- 1.6 Token Economics
- 1.7 Streaming
- 1.8 Fallbacks
- Agent Reliability
- 2.1 Timeouts
- 2.2 Retries
- 2.3 Idempotency
- 2.4 State Persistence
- 2.5 Failure Recovery
- 2.6 Guardrails
- 2.7 Validation
- 2.8 Human Escalation
- Evaluation
- Observability
- Hepsini Bir Araya Getirmek — Referans Mimari
1. LLM Engineering
1.1 Prompt Design
Prensip: Prompt bir düz yazı değil, olasılıksal bir fonksiyonla yapılmış bir API kontratıdır. Onu kod gibi ele al: versiyonlanmış, test edilmiş, review edilmiş.
Best practices
- System, developer ve user katmanlarını açıkça ayır. System prompt = kimlik + pazarlık edilemez kısıtlar. Developer/context prompt = göreve özel talimatlar, retrieve edilmiş veri. User prompt = asıl talep. Üçünü tek bir blob’da birleştirme — bu debug etmeyi ve versiyonlamayı imkansız hale getirir.
- Örtük değil, açık ol. Modelin çıkarsamasını ummak yerine format, ton, uzunluk ve edge-case davranışını açıkça belirt. “Kısa cevap ver” ifadesi “2-3 cümlede, giriş cümlesi olmadan cevap ver” ifadesinden daha zayıftır.
- Negatif talimat yerine pozitif talimat kullan. “Sadece verilen dokümanlara referans ver” ifadesi “Bir şeyler uydurma” ifadesinden daha iyi çalışır. Modeller “X yap” talimatını “Y yapma” talimatından daha güvenilir şekilde takip eder.
- Sıra önemlidir. En kritik kısıtları, model ailesine göre ya en sona (recency bias) ya da en başa (primacy bias) koy — ikisini de test et. Uzun context’lerde, kritik talimatların prompt’un sonunda tekrarlanması (“hatırlatma” pattern’i) uyumu ölçülebilir şekilde artırır.
- Format-hassas görevlerde few-shot, zero-shot’tan üstündür. 2-5 çeşitli, gerçekçi örnek, uzun talimatlardan çok daha fazla format sapmasını azaltır.
- Uzun veya çok parçalı promptlarda XML/markdown yapısal etiketler kullan (
<context>,<task>,<constraints>,<examples>). Ağır RLHF ile eğitilmiş modeller yapısal ayraçlara iyi tepki verir — bölümler arası talimat sızmasını azaltır. - Chain-of-thought sadece maliyetini karşılıyorsa kullanılmalı. Classification/extraction görevlerinde çoğu zaman doğruluk kazanımı olmadan latency ve maliyeti artırır. Çok adımlı reasoning veya matematikte yardımcı olur. Varsayma — A/B test et.
- Her prompt’u kod gibi versiyonla. Promptları kod tabanına dağılmış inline string’ler yerine dosyalarda/config’te sakla. Git-diff edilebilir, changelog’lu olsun.
- Prompt regresyonlarını kod regresyonu gibi ele al — bir kelime değişikliği, üç servis ötedeki bir JSON parser’ı sessizce kırabilir.
Design pattern: Prompt Template Registry
from dataclasses import dataclass
from string import Template
from pathlib import Path
import hashlib
@dataclass(frozen=True)
class PromptVersion:
name: str
version: str
template: str
checksum: str
class PromptRegistry:
"""Tüm promptlar için merkezi, versiyonlanmış tek doğru kaynak.
Promptları asla business logic içinde inline formatlama."""
def __init__(self, prompt_dir: Path):
self._prompts: dict[str, PromptVersion] = {}
for file in prompt_dir.glob("*.tmpl"):
name, version = file.stem.rsplit("__", 1)
text = file.read_text()
checksum = hashlib.sha256(text.encode()).hexdigest()[:8]
self._prompts[f"{name}:{version}"] = PromptVersion(name, version, text, checksum)
def render(self, name: str, version: str, **kwargs) -> str:
key = f"{name}:{version}"
pv = self._prompts[key]
return Template(pv.template).substitute(**kwargs)
# Kullanım
registry = PromptRegistry(Path("prompts/"))
prompt = registry.render("support_triage", "v3", ticket_body="...", policy="...")
Anti-pattern’ler
- Prompt’ları business logic içine dağılmış nested f-string’lerle oluşturmak — diff almak, test etmek veya geri almak imkansız hale gelir.
- Sessiz prompt sapması: birinin prod’da hotfix ile prompt’u değiştirip onu doğrulayan eval dataset’ini güncellememesi.
- Tek bir prompt’a 15 talimat yüklemek — bunun yerine bir pipeline’a böl (classify → extract → generate).
1.2 Structured Generation
Prensip: Eğer downstream kod çıktıyı parse ediyorsa, model sadece parse edilebilir çıktı üretecek şekilde kısıtlanmalıdır — asla “lütfen valid JSON ile cevap ver” ifadesine tek başına güvenme.
Best practices
- Sadece prompt ile JSON istemek yerine native structured output / tool-forced JSON modlarını tercih et (ör. Anthropic tool_use + JSON schema, OpenAI
response_format=json_schema,strictmod). Grammar-constrained decoding, tüm bir parsing hata sınıfını ortadan kaldırır. - Pydantic (veya benzeri) şemalarını tek doğru kaynak olarak kullan — aynı şema hem modele gönderilen tool spec’ini, hem runtime validasyonunu, hem de type hint’lerini yönlendirsin.
- Şemaları mümkün olduğunca dar tut. Domain kapalıysa serbest string yerine enum kullan. Bir severity alanı için
stryerineLiteral["low","medium","high"]daha iyidir. - Her zaman doğrula, asla güvenme. Constrained decoding olsa bile, şemanın tek başına ifade edemeyeceği aralıkları, cross-field kısıtları ve business kurallarını doğrula.
- Stream ediyorsan partial/streamed structured output için tasarla — tam payload’ı beklemek yerine incremental JSON parsing destekleyen kütüphaneler kullan.
- Şemalarını versiyonla. Bir şema değişikliği, o agent’ın çıktısını tüketen her consumer için breaking API değişikliğidir.
Design pattern: Schema-first generation with self-repair
from pydantic import BaseModel, ValidationError, Field
from typing import Literal
import json
class TriageResult(BaseModel):
category: Literal["billing", "technical", "abuse", "other"]
severity: Literal["low", "medium", "high", "critical"]
summary: str = Field(max_length=280)
requires_human: bool
def generate_structured(client, prompt: str, schema: type[BaseModel], max_repairs: int = 2):
"""Üret + doğrula + kendini onar döngüsü.
Bozuk bir payload'ın sessizce downstream'e sızmasına asla izin verme."""
messages = [{"role": "user", "content": prompt}]
for attempt in range(max_repairs + 1):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[{
"name": "emit_result",
"input_schema": schema.model_json_schema(),
}],
tool_choice={"type": "tool", "name": "emit_result"},
messages=messages,
)
tool_call = next(b for b in response.content if b.type == "tool_use")
try:
return schema.model_validate(tool_call.input)
except ValidationError as e:
if attempt == max_repairs:
raise
# Validasyon hatasını geri besle — sadece kör kör retry değil, kendini onarım
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": f"Çıktın validasyonu geçemedi: {e}. Düzelt ve emit_result'ı tekrar çağır."
})
Anti-pattern’ler
- JSON’ı düz yazı olarak istemek ve markdown fence’lerden regex ile çekmek — herhangi bir model güncellemesine karşı kırılgan.
- Eksik alanları sessizce
.get()ile varsayılan değerlerle doldurmak, modelin onları gerçekten üretmediğini gizlemek. - Her şey için tek bir dev şema — göreve göre böl; 40 alanlı bir şema, alakasız alanlarda halüsinasyon riskini artırır.
1.3 Tool Calling
Prensip: Bir tool tanımı, üzerinde modelin akıl yürüttüğü bir kontrattır, insanlar için dokümantasyon değil. Spesifikasyondaki her belirsizlik, runtime’da bir hata moduna dönüşür.
Best practices
- Tool isimleri ve açıklamaları net ve eylem odaklı olmalı:
invoicesdeğilget_customer_invoices. Ne yaptığını, ne zaman kullanılacağını ve — kritik olarak — başka bir tool ile örtüşme varsa ne zaman kullanılmayacağını içersin. - Her çağrıda tool yüzeyini küçük tut. Tek bir context’te ~15-20’den fazla tool, seçim doğruluğunu ölçülebilir şekilde düşürür. Koşullu yüklenen tool grupları veya önce toolset’i daraltan bir router agent kullan.
- Tool’ları isimlerinde idempotent ve yan-etki-açık olacak şekilde tasarla:
create_ordervsget_or_create_order. Model, backend semantiğini isim söylemedikçe bilemez. - Yapısal, kompakt tool sonuçları döndür. Ham 50KB’lık bir API yanıtını doğrudan context’e dökme — modele döndürmeden önce sunucu tarafında özetle/sayfalandır.
- Hata durumlarını tool sonucunun içinde modelle (
{"status": "not_found"}opak bir exception string’i fırlatmak yerine) böylece model kurtarma üzerine akıl yürütebilir. - Tool call döngülerini açıkça sınırla — sadece tek bir çağrı için değil, agent turn’ü başına maksimum iterasyon sayısı ve maksimum wall-clock bütçesi belirle.
- Her tool çağrısı + sonuç çiftini correlation ID’lerle logla — bu senin birincil reliability debug yüzeyin (bkz. Observability).
- Bir tool’a görevin ihtiyacından fazla izin verme. “search_orders” tool’u aynı zamanda
delete_order‘ı da açığa çıkarmamalı. Blast radius’a göre böl.
Design pattern: Büyük toolset’ler için Tool Router
from typing import Protocol
class Tool(Protocol):
name: str
description: str
schema: dict
class ToolRouter:
"""İki aşamalı tool calling: önce ilgili tool *gruplarını* seç,
sonra yalnızca onları execution çağrısına aç. 100+ tool kayıtlı
olsa bile working-set'i küçük ve seçim doğruluğunu yüksek tutar."""
def __init__(self, groups: dict[str, list[Tool]]):
self.groups = groups
def route(self, client, user_intent: str) -> list[Tool]:
group_names = list(self.groups.keys())
response = client.messages.create(
model="claude-haiku-4-5", # routing için ucuz, hızlı model
max_tokens=50,
messages=[{
"role": "user",
"content": f"Şu talebe göre: '{user_intent}', hangi tool grupları "
f"ilgili? Seçenekler: {group_names}. JSON liste olarak cevap ver."
}],
)
selected = json.loads(response.content[0].text)
tools = []
for g in selected:
tools.extend(self.groups.get(g, []))
return tools
Anti-pattern’ler
- Serbest metin
actionparametresi alan tek bir monolitik tool — bu, structured tool calling’in tüm amacını boşa çıkarır. - Hata durumunda ham stack trace döndüren tool’lar.
- Tool-call döngülerinde üst sınır olmaması → “tekrar arayayım” spirallerinin sonsuza kadar token ve latency bütçesini tüketmesi.
1.4 Context Engineering
Prensip: Context window kıt, pahalı ve dikkati sulandıran bir kaynaktır. Daha fazla context, daha fazla doğruluk anlamına gelmez — alakasız context reasoning’i aktif olarak bozar (“lost in the middle” etkisi, gürültüden dikkat dağılması).
Best practices
- Enjekte etmeden önce retrieve et, sırala, kırp. RAG sadece bir embedding lookup’ı değil, bir context-engineering disiplinidir — prompt’a herhangi bir şey girmeden önce reranking, deduplication ve relevance eşikleri uygula.
- Karar açısından en önemli içeriği context’in ortasına gömmek yerine başına veya sonuna koy — modeller uzun input’larda context’in ortasındaki içeriğe daha güvenilir şekilde dikkat etmez.
- Konuşma geçmişini sonsuza kadar eklemek yerine özetle/sıkıştır. Uzun süren agent oturumları için ham transkript birleştirme yerine rolling summary + son-N-turn penceresi kullan.
- “Statik” context’i (system talimatları, tool spec’leri) “dinamik” context’ten (retrieve edilmiş dokümanlar, konuşma) ayır. Statik context prompt caching’ten faydalanır; dinamik context faydalanmaz — prompt’ları caching sınırları gerçekten değişen şeyle hizalanacak şekilde yapılandır.
- Retrieve edilen dokümanları metadata ile parçala (kaynak, timestamp, güven skoru) böylece model sadece içerik değil, köken üzerine de akıl yürütebilsin — citation ve halüsinasyon azaltma için kritik.
- Mevcut alt-göreve artık ilgili olmayan tool sonuçlarını ve eski turn’leri aktif olarak buda — context-sıkıştırma adımı, sonradan akla gelen bir şey değil, agent’ın birinci sınıf sorumluluğudur.
- Context “sinyal-gürültü oranını” ölç. Enjekte edilen token’ların, nihai yanıtta gerçekten referans alınan/kullanılan kısmını takip et — retrieval kalitesi için bir proxy.
Design pattern: Context Budgeter
from dataclasses import dataclass, field
@dataclass
class ContextBudget:
max_tokens: int
system_reserve: int
output_reserve: int
@property
def available_for_dynamic(self) -> int:
return self.max_tokens - self.system_reserve - self.output_reserve
class ContextAssembler:
"""Sıkı bir token bütçesi altında nihai prompt context'ini oluşturur,
relevance skoruna göre önceliklendirir, asla sessizce taşırmaz."""
def __init__(self, budget: ContextBudget, tokenizer):
self.budget = budget
self.tokenizer = tokenizer
def assemble(self, ranked_chunks: list[tuple[str, float]]) -> str:
# ranked_chunks: (text, relevance_score), azalan sırada
selected, used = [], 0
for text, score in ranked_chunks:
n = len(self.tokenizer.encode(text))
if used + n > self.budget.available_for_dynamic:
break
selected.append(text)
used += n
return "\n---\n".join(selected)
Anti-pattern’ler
- Saatlerce süren agent oturumları için “tüm konuşma geçmişini ekle” — context window taşması ve ondan önce kalite düşüşü garanti.
- “Ne olur ne olmaz” diye relevance filtresi olmadan ham retrieve edilmiş dokümanları dökmek.
- Sıkıştırma stratejisi olmaması → ne kadar uzun çalışırsa o kadar yavaşlayan ve aptallaşan agent’lar.
1.5 Model Selection
Prensip: Model seçimi, tek bir küresel karar değil, görev başına bir maliyet/latency/kalite optimizasyon problemidir. Principal mühendisler bir kere seçmez, yönlendirir (route eder).
Best practices
- Ürünleri değil, görevleri profille. Classification, extraction ve routing nadiren en pahalı modeline ihtiyaç duyar. Karmaşık çok adımlı reasoning, kod üretimi ve açık uçlu yazı genelde ihtiyaç duyar.
- Bir model router / tiering stratejisi kur: triage ve basit alt-görevler için küçük/hızlı model, zor adımlar için büyük model, ikisi arasında bir escalation yolu ile.
- Genel leaderboard’lar değil, kendi eval setinde benchmark yap. Public benchmark’lar senin spesifik görev dağılımın için zayıf bir proxy’dir.
- Model versiyon pinlemesini açıkça takip et. “Latest” model alias’ları sessizce davranışı değiştirebilir — production’da tam model ID’lerini pinle ve versiyon güncellemeden önce eval suite’ini tekrar çalıştır.
- Fine-tuning / distillation’ı sadece prompt + context engineering tükendikten sonra düşün — bu en pahalı ve en az geri alınabilir lever’dır.
- Model seçimini periyodik olarak yeniden değerlendir. Maliyet/kalite sınırı hızlı hareket eder; 6 ay önce pahalı bir modele pinlenmiş bir görev, artık daha ucuz bir modelle eşit kalitede yapılabilir olabilir.
Design pattern: Katmanlı Model Router
from enum import Enum
class TaskComplexity(Enum):
TRIVIAL = "trivial" # routing, basit classification
MODERATE = "moderate" # extraction, tek adımlı reasoning
COMPLEX = "complex" # çok adımlı reasoning, planlama, kod üretimi
MODEL_TIER = {
TaskComplexity.TRIVIAL: "claude-haiku-4-5-20251001",
TaskComplexity.MODERATE: "claude-sonnet-5",
TaskComplexity.COMPLEX: "claude-opus-4-8",
}
def select_model(complexity: TaskComplexity, escalate: bool = False) -> str:
if escalate:
# başarısızlık sonrası retry'da bir tier yukarı çık, tepede sınırla
tiers = list(TaskComplexity)
idx = min(tiers.index(complexity) + 1, len(tiers) - 1)
complexity = tiers[idx]
return MODEL_TIER[complexity]
Anti-pattern’ler
- “Güvenli olmak için” her çağrıda en yetenekli (ve en pahalı) modeli kullanmak — bu bir güvenlik önlemi değil, bir maliyet/latency bug’ıdır.
- Model deprecation bildiriminden sonra hiç yeniden benchmark yapmamak — otomatik migrate edilen alias’larda sessizce kalite düşüşü.
1.6 Token Economics
Prensip: Token’lar senin birincil birim maliyetin ve birincil latency sürücündür. Token bütçelerini dashboard’lu ve alarmlı bir cloud altyapı maliyet merkezi gibi ele al.
Best practices
- API çağrısı başına değil, başarılı görev sonucu başına maliyeti ölç. 3 retry gerektiren ucuz-ama-kararsız bir çağrı, ilk seferde başarılı olan premium bir çağrıdan daha pahalı olabilir.
- Statik system promptları, tool spec’leri ve uzun paylaşılan context (few-shot örnekleri, istekler arasında tekrar kullanılan bilgi tabanları) için prompt caching’i agresif kullan.
- Tool çıktılarını context’e girmeden önce kırp/sayfalandır — çoğu token şişkinliği prompt’ların kendisinden değil, ham tool sonuçlarından gelir.
- Etkileşimli olmayan iş yüklerini mevcutsa async/batch API’ler üzerinden batch’le — offline evaluation, toplu classification vb. için önemli maliyet düşüşü.
- Sert istek-başına ve oturum-başına token tavanları belirle — token tavanı olmayan kaçak bir agent döngüsü, gerçekleşmeyi bekleyen bir maliyet olayıdır.
- Sadece toplam harcamayı değil, feature/müşteri/tenant başına maliyeti takip et — doğru workflow’ları optimize etmek için hangilerinin pahalı olduğunu bilmelisin.
Design pattern: Token Budget Guard
class TokenBudgetExceeded(Exception):
pass
class SessionBudget:
"""Agent oturumu başına sert tavan — kaçak döngülerin
bir maliyet olayına dönüşmesini engeller."""
def __init__(self, max_tokens: int):
self.max_tokens = max_tokens
self.used = 0
def charge(self, input_tokens: int, output_tokens: int):
self.used += input_tokens + output_tokens
if self.used > self.max_tokens:
raise TokenBudgetExceeded(
f"Oturum {self.used} token kullandı, bütçe {self.max_tokens} idi"
)
@property
def remaining(self) -> int:
return max(0, self.max_tokens - self.used)
Anti-pattern’ler
- Oturum başına veya tenant başına token tavanı olmaması — tek bir hatalı davranan agent döngüsü, günlük faturanı sessizce 10 katına çıkarabilir.
- End-to-end başarı oranı etkisini ölçmeden prompt uzunluğunu optimize etmek — token tasarrufu için bir few-shot örneğini kesmek, retry’larda çok daha fazlasına mal olabilir.
1.7 Streaming
Prensip: Streaming sadece bir latency hilesi değil, bir UX ve reliability aracıdır — partial çıktıyı incremental olarak parse edilecek ve doğrulanacak birinci sınıf bir veri yapısı olarak ele al.
Best practices
- Beklenen latency ~1-2 saniyeyi geçen her şeyi kullanıcıya stream et — toplam süre aynı olsa bile algılanan latency dramatik şekilde düşer.
- JSON/tool call stream ediyorsan structured output’un incremental parse’ı için tasarla — karakter karakter değil, yapısal olarak tamamlanmış bir birim mevcut olduğunda güvenli şekilde buffer’la ve doğrula.
- Stream ortasında iptali temiz şekilde ele al — üretim ortasında bir sekmeyi kapatan kullanıcı, faturalandırmayı durdurmalı ve bağlantıyı serbest bırakmalı, arka planda bir completion sızdırmamalı.
- Error handling’inde “stream chunk” hatalarını “final validation” hatalarından ayır — bir stream transport seviyesinde başarıyla tamamlanabilir ama sonunda şema validasyonunda yine de başarısız olabilir.
- Tool-call streaming’i text streaming’den ayrı buffer’la — iç içe geçmiş content block’lar bağımsız incremental parser’lara ihtiyaç duyar.
- Sadece toplam-istek timeout’u değil, stream-seviyesinde timeout’lar belirle (time-to-first-token, chunk’lar arası zaman) — takılan bir stream, yavaş-ama-ilerleyen bir stream’den farklı bir hata modudur.
Design pattern: Incremental validation ile streaming
import json
async def stream_structured_response(client, prompt: str, schema: type[BaseModel]):
buffer = ""
async with client.messages.stream(
model="claude-sonnet-5",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
) as stream:
async for event in stream:
if event.type == "content_block_delta":
buffer += event.delta.text
yield {"type": "partial", "text": buffer} # UX: ilerlemeyi göster
final = await stream.get_final_message()
try:
parsed = schema.model_validate_json(buffer)
yield {"type": "final", "data": parsed}
except ValidationError as e:
yield {"type": "error", "detail": str(e)}
Anti-pattern’ler
- Tüm yanıtı sunucu tarafında buffer’layıp ancak ondan sonra client’a “stream” etmek — tüm amacı boşa çıkarır.
- Time-to-first-token timeout’u olmaması — bu sinyal olmadan takılı bir bağlantı, yavaş-ama-çalışan bir bağlantıyla aynı görünür.
1.8 Fallbacks
Prensip: Her dış çağrı (model API’si, tool, retrieval backend’i) production’da kesinlikle bir gün başarısız olacaktır. Soru asla “eğer” değil, “sonra ne olacak” ve bu doğaçlama değil, tasarlanmış olmalıdır.
Best practices
- Model çağrıları için multi-provider / multi-region fallback — birincil model API’in bozulur veya rate-limit’e girerse, kullanıcıya hata göstermek yerine ikincil bir sağlayıcıya veya bölgeye geç.
- Zarif bir şekilde düşür, sessizce değil. Daha küçük bir modele veya cache’lenmiş bir cevaba düşme durumu loglanmalı ve ilgiliyse gösterilmeli (“bu cevap bir yedek sistem kullandı”), hiçbir şey olmamış gibi davranılmamalı.
- Fallback kalite katmanlarını açıkça tanımla: tam agent → daha basit tek-çağrılık prompt → cache’lenmiş/template’li yanıt → insana devretme. Her zaman hangi katmanda olduğunu bil.
- Sadece retry yapma, circuit-break et — bir bağımlılığa N ardışık başarısızlıktan sonra onu dövmeyi bırak ve hemen fallback’e geç; birincil yolu bir soğuma süresinde tekrar dene.
- Fallback yollarını sadece mutlu yolda değil, CI’da test et — gerçek bir kesintiye kadar hiç çalıştırılmamış bir fallback, aslında sahip olmadığın bir fallback’tir.
- Fallback’lerin kendi değerlendirmesi olmalı — düşük moddaki bir cevap yine de (daha düşük ama sıfır olmayan) bir kalite barını geçmeli, aksi halde insana escalate ederek hızlıca başarısız olmak daha iyidir.
Design pattern: Katmanlı Fallback Zinciri
from typing import Callable, Any
class FallbackChain:
"""Her biri kendi circuit breaker'ına sahip sıralı strateji zinciri.
Başarısızlıkta bir sonraki katmana düşer, çağırana asla ham bir
exception göstermez."""
def __init__(self, strategies: list[Callable[..., Any]]):
self.strategies = strategies
def execute(self, *args, **kwargs) -> tuple[Any, int]:
last_error = None
for tier, strategy in enumerate(self.strategies):
try:
return strategy(*args, **kwargs), tier
except Exception as e: # noqa: kasıtlı olarak geniş — fallback sınırı
last_error = e
continue
raise RuntimeError(f"Tüm fallback katmanları tükendi: {last_error}")
# Kullanım
chain = FallbackChain([
lambda q: full_agent_answer(q), # katman 0: tam kalite
lambda q: single_call_answer(q), # katman 1: düşük kalite
lambda q: cached_template_answer(q), # katman 2: statik fallback
])
answer, tier_used = chain.execute(user_query)
if tier_used > 0:
logger.warning("degraded_response", tier=tier_used)
Anti-pattern’ler
- Devre dışı kalan aynı sağlayıcıyı failover yapmak yerine sonsuza kadar tekrar denemek.
- Hiç load-test veya eval-test edilmemiş fallback yolları — ihtiyaç duyulan ilk anda asıl bug haline gelirler.
2. Agent Reliability
2.1 Timeouts
Prensip: Her network çağrısı, model çağrısı ve tool çağrısının açık bir timeout’a ihtiyacı vardır — “timeout yok” nötr bir durum değil, sınırsız bir yükümlülüktür.
Best practices
- Her katmanda timeout belirle: LLM çağrısı başına, tool çağrısı başına, agent turn’ü başına, oturum başına. Tek bir eksik katman, bir takılmanın tüm bütçeyi tüketmesi anlamına gelir.
- Farklı çağrı tipleri için farklı timeout sınıfları kullan — bir retrieval çağrısı ile çok adımlı bir reasoning çağrısının kabul edilebilir latency’si çok farklıdır; tek bir global timeout sabiti bir koku (smell)‘dur.
- Timeout ≠ hata — timeout’u uygulama hatasından ayrı bir sonuç olarak ele al; kendi retry/fallback politikasına yönlendirilmeli, bir 500 ile karıştırılmamalı.
- Sadece lokal timeout’lar belirleme, deadline’ları ilet. Bir agent turn’ünün genel 30 saniyelik bir deadline’ı varsa, her alt çağrı kendi bağımsız 30 saniyesini değil, kalan bütçeyi almalı.
- Timeout’ta temiz iptal et — altta yatan bağlantıyı/thread’i serbest bırak, çağıran vazgeçtikten sonra hala compute/token tüketen sahipsiz arka plan işleri bırakma.
Design pattern: Deadline propagation
import time
import asyncio
class Deadline:
"""Her katmanın kendi bağımsız timeout'unu icat etmesi yerine, paylaşılan
bir wall-clock deadline'ı iç içe geçmiş async çağrılar boyunca iletir."""
def __init__(self, timeout_s: float):
self.deadline = time.monotonic() + timeout_s
@property
def remaining(self) -> float:
return max(0.0, self.deadline - time.monotonic())
def check(self):
if self.remaining <= 0:
raise TimeoutError("Deadline aşıldı")
async def run_agent_turn(deadline: Deadline, agent_step):
deadline.check()
return await asyncio.wait_for(agent_step(), timeout=deadline.remaining)
async def agent_loop(user_query: str):
deadline = Deadline(timeout_s=30)
for step in build_steps(user_query):
result = await run_agent_turn(deadline, step)
deadline.check()
Anti-pattern’ler
- Tool katmanında hiçbir yerde timeout olmayan tek bir global
requests.get(url)— sonunda bir thread pool’u tüketen klasik sessiz takılma. - Genel oturum deadline’ına saygı göstermeyen bağımsız çağrı-başına timeout’lar — beş alt çağrıda “başarıyla” timeout alabilir ve yine de kullanıcıya dönük SLA’yı patlatabilirsin.
2.2 Retries
Prensip: Retry’lar geçici hataları başarıya dönüştürür ve kalıcı hataları kademeli yüke dönüştürür. Retry politikası aradaki farkı bilmelidir.
Best practices
- Retry yapmadan önce hataları sınıflandır: retry edilebilir (rate limit, 5xx, timeout) vs retry edilemez (4xx validasyon, auth hatası, içerik politikası bloğu). Bir 400’ü sonsuza kadar retry etmek dayanıklılık değil, bir bug’dır.
- Her zaman jitter’lı exponential backoff. Sabit aralıklı retry’lar client’lar arasında senkronize olur ve bağımlılığında thundering-herd sıçramaları yaratır.
- İstek başına VE zaman penceresi başına toplam retry bütçesini sınırla — bir sağlayıcı kesintisi sırasındaki bir retry fırtınası, kesintinin kendisinden daha kötü olabilir.
- Retry’lar idempotency ile eşleştirilmeli (bkz. 2.3) — dedup key olmadan yan-etkili idempotent-olmayan bir tool çağrısını (ör.
charge_card) retry etmek, dayanıklılık değil, yinelenen yan etkiler yaratır. - Çağrı tipine göre farklı retry politikaları. Salt-okunur bir retrieval çağrısı agresif retry edebilir; bir ödeme tool çağrısı temkinli retry etmeli ve sadece bir idempotency key mevcutken.
- Retry tükenmesini ilk deneme başarısızlığından ayrı olarak logla/metrikle — “3 retry sonrası başarısız” ile “hemen başarısız” farklı reliability sinyalleridir.
Design pattern: Backoff+jitter ile politika güdümlü retry
import random
import time
from dataclasses import dataclass
@dataclass
class RetryPolicy:
max_attempts: int
base_delay_s: float
max_delay_s: float
retryable: Callable[[Exception], bool]
def with_retry(policy: RetryPolicy):
def decorator(fn):
def wrapped(*args, **kwargs):
last_exc = None
for attempt in range(policy.max_attempts):
try:
return fn(*args, **kwargs)
except Exception as e:
if not policy.retryable(e) or attempt == policy.max_attempts - 1:
raise
last_exc = e
delay = min(policy.base_delay_s * (2 ** attempt), policy.max_delay_s)
delay *= random.uniform(0.5, 1.5) # jitter
logger.warning("retrying", attempt=attempt, delay=delay, error=str(e))
time.sleep(delay)
raise last_exc
return wrapped
return decorator
RATE_LIMIT_POLICY = RetryPolicy(
max_attempts=4, base_delay_s=1.0, max_delay_s=20.0,
retryable=lambda e: isinstance(e, (RateLimitError, TimeoutError, ServerError)),
)
@with_retry(RATE_LIMIT_POLICY)
def call_llm(prompt): ...
Anti-pattern’ler
- Tüm bir agent turn’ünü saran genel bir
try/except: retry(), validasyon hatalarını ve business-logic hatalarını network sıçramalarıyla aynı şekilde retry etmek. - Jitter olmaması → kısmi kesinti sırasında eşzamanlı istekler arasında senkronize retry fırtınaları.
- Bir idempotency key olmadan yan-etkili bir tool çağrısını retry etmek.
2.3 Idempotency
Prensip: Dış durumu değiştiren herhangi bir tool çağrısı, aynı input ile birden fazla kez çalıştırılması güvenli olmalıdır — çünkü retry’lar, timeout’lar ve process çökmeleri kesinlikle yinelenen çağrılara neden olacaktır.
Best practices
- HTTP isteği başına değil, mantıksal eylem başına bir idempotency key üret — bunu agent’ın kararından türet (ör.
session_id + step_id + actionhash’i), böylece retry edilen bir çağrı aynı key’i yeniden kullanır. - Idempotency’yi sadece agent orchestration katmanına değil, tool/backend katmanına da it — agent framework’ünün “nazikçe” retry yapması, downstream ödeme API’si kendisi idempotent değilse yardımcı olmaz.
- Mümkün olduğunda tool’ları upsert olarak tasarla (
ensure_ticket_existsvscreate_ticket) — doğası gereği idempotent operasyonlar tüm bir hata sınıfını ortadan kaldırır. - Alınan eylemler için idempotency key’e göre anahtarlanmış, retry penceresine uygun TTL’li bir dedup ledger sakla — sadece backend’in halletmesini ummak yerine kontrol-et-sonra-yap.
- Idempotency ve exactly-once farklı garantilerdir — çoğu sistem gerçekçi olarak at-least-once teslimat + idempotent işleme = etkin olarak tek seferlik sonuç sunar. Yanlış bir “tam olarak bir kez” vaadi yerine buna göre tasarla.
Design pattern: Idempotency ledger
import hashlib
from datetime import datetime, timedelta
class IdempotencyLedger:
"""Yan etkili bir eylemi çalıştırmadan önce, bu key altında daha önce
yapılıp yapılmadığını kontrol et. Production'da Redis/DB tablosu;
burada gösterim için dict."""
def __init__(self, store: dict, ttl: timedelta = timedelta(hours=24)):
self.store = store
self.ttl = ttl
def make_key(self, session_id: str, step_id: str, action: str, payload: dict) -> str:
raw = f"{session_id}:{step_id}:{action}:{sorted(payload.items())}"
return hashlib.sha256(raw.encode()).hexdigest()
def execute_once(self, key: str, fn: Callable[[], Any]) -> Any:
existing = self.store.get(key)
if existing and existing["expires_at"] > datetime.utcnow():
return existing["result"] # zaten yapılmış — cache'lenmiş sonucu döndür, yan etkiyi tekrarlama
result = fn()
self.store[key] = {"result": result, "expires_at": datetime.utcnow() + self.ttl}
return result
# Bir tool çağrısı içinde kullanım
def charge_customer_tool(session_id, step_id, amount):
key = ledger.make_key(session_id, step_id, "charge_customer", {"amount": amount})
return ledger.execute_once(key, lambda: payment_api.charge(amount))
Anti-pattern’ler
- “Agent bu tool’u iki kez çağırmayacak” varsayımı — retry’lar ve timeout’lar altında çağıracaktır.
- Wall-clock zamanından türetilmiş idempotency key’leri (
datetime.now()) — her retry’da farklı olur, amacını boşa çıkarır. - Backend tarafında dedup olmadan sadece orchestration katmanı retry bastırmasına güvenmek.
2.4 State Persistence
Prensip: Bir agent’ın çalışma durumu (konuşma, plan, ara sonuçlar, tool çağrı geçmişi) process yeniden başlatmalarından, deploy’lardan ve çökmelerden hayatta kalmalıdır — bunu in-memory bir kolaylık değil, kalıcı veri olarak ele al.
Best practices
- Sadece sonunda değil, her anlamlı adımdan sonra durumu kalıcı hale getir — plan ortasında bir çökme, sıfırdan değil, son checkpoint’ten devam edilebilir olmalı.
- “Konuşma durumunu” “execution durumundan” ayır. Konuşma durumu (mesajlar) çoğunlukla append-only’dir; execution durumu (mevcut plan adımı, tool sonuçları, retry sayaçları) mutable’dır ve kendi şemasına ihtiyaç duyar.
- Durum şemanı versiyonla — agent’ın internal durum şeklini değiştiren bir deploy, eski şema altında oluşturulmuş uçan (in-flight) oturumları ele almalı (migrate et veya zarif bir şekilde kapalı olarak başarısız ol).
- Durumu dışarıdan incelenebilir hale getir — support/nöbetçi mühendislerin, debug kodu redeploy etmeden takılı bir oturumun durumuna bakabilmesi gerekir.
- Storage katmanını dayanıklılık ihtiyacına göre seç: sıcak, kısa ömürlü oturum durumu için Redis, altyapı arızasından hayatta kalması gereken veya audit geçmişi ihtiyacı olan her şey için gerçek bir veritabanı/event log.
- Uzun süren agent’ları tek, uzun ömürlü bir in-process döngü yerine devam ettirilebilir workflow’lar olarak modelle (state machine / durable execution) — process ölümü en fazla bir adımı kaybetmeli, tüm oturumu değil.
Design pattern: Checkpoint’li state machine
from enum import Enum, auto
from dataclasses import dataclass, field, asdict
import json
class AgentState(Enum):
PLANNING = auto()
EXECUTING_TOOL = auto()
AWAITING_HUMAN = auto()
DONE = auto()
FAILED = auto()
@dataclass
class AgentCheckpoint:
session_id: str
state: AgentState
plan: list[str]
current_step: int
tool_results: dict = field(default_factory=dict)
retry_counts: dict = field(default_factory=dict)
class CheckpointStore:
"""Agent durumu için dayanıklı kalıcılık — prod'da Postgres/Redis."""
def __init__(self, backend):
self.backend = backend
def save(self, checkpoint: AgentCheckpoint):
payload = asdict(checkpoint)
payload["state"] = checkpoint.state.name
self.backend.set(checkpoint.session_id, json.dumps(payload))
def load(self, session_id: str) -> AgentCheckpoint | None:
raw = self.backend.get(session_id)
if not raw:
return None
data = json.loads(raw)
data["state"] = AgentState[data["state"]]
return AgentCheckpoint(**data)
def resume_or_start(store: CheckpointStore, session_id: str, initial_plan):
checkpoint = store.load(session_id)
if checkpoint is None:
checkpoint = AgentCheckpoint(session_id, AgentState.PLANNING, initial_plan, 0)
store.save(checkpoint)
return checkpoint
Anti-pattern’ler
- Tam agent durumunu sadece process-lokal bir değişkende/kuyrukta tutmak — uzun bir agent çalışması sırasında bir pod restart’ı, tüm oturumu sessizce düşürür.
- Şema versiyonlaması olmaması → deploy, çalışan her oturumu ortasında bozar.
- “Takılı” oturumları sadece uygulama loglarıyla debug etmek, mevcut kalıcı durumu doğrudan inceleyecek bir yol olmadan.
2.5 Failure Recovery
Prensip: Failure recovery, try/except: log ve devam et değil, tasarlanmış bir state machine’dir. Her hata modu, açık ve test edilmiş bir kurtarma yoluna ihtiyaç duyar.
Best practices
- Hataları kurtarılabilirliğe göre sınıflandır: geçici (retry), düşürülebilir (fallback katmanı), kalıcı (başarısız ol + escalate). Üçünü de aynı şekilde ele alma.
- Kurtarma, tüm agent çalışmasını yeniden başlatmak yerine son iyi checkpoint’ten devam etmeli — pahalı çok adımlı planlar, tek bir başarısız alt-adım için çöpe atılmamalı.
- Kısmi başarıyı toplam başarısızlıktan ayır. Bir agent başarısız olmadan önce planlanan 5 eylemden 3’ünü tamamladıysa, kurtarma yolu ne yapılmış olduğunu bilmeli (idempotency + state persistence bunu mümkün kılar), tamamlanmış yan etkileri tekrar yapmak yerine.
- Kurtarılamaz hatalar için bir dead-letter yolu inşa et — bunların kimsenin okumadığı bir log satırında kaybolmasına izin verme; manuel triage için bir kuyruğa/dashboard’a yönlendir.
- Staging’de hata enjeksiyonu test et (hafif chaos engineering) — process’i tool-çağrısı ortasında öldür, stream ortasında bir timeout simüle et ve kurtarma yolunun kağıt üzerinde var olmasından öte gerçekten çalıştığını doğrula.
- Kurtarma eylemlerinin kendisi de başarısız olabilir — kurtarma yollarını kendi sınırlı retry’sıyla tasarla, sonsuz özyinelemeli kurtarma-üstüne-kurtarma değil.
Design pattern: Recovery-aware executor
class RecoverableExecutionError(Exception):
def __init__(self, checkpoint: AgentCheckpoint, cause: Exception):
self.checkpoint = checkpoint
self.cause = cause
def execute_plan(checkpoint: AgentCheckpoint, store: CheckpointStore):
try:
for i in range(checkpoint.current_step, len(checkpoint.plan)):
step = checkpoint.plan[i]
result = execute_step(step, checkpoint) # 2.3'e göre idempotent
checkpoint.tool_results[step] = result
checkpoint.current_step = i + 1
store.save(checkpoint) # her adımdan sonra checkpoint
checkpoint.state = AgentState.DONE
store.save(checkpoint)
return checkpoint
except Exception as e:
checkpoint.state = AgentState.FAILED
store.save(checkpoint)
raise RecoverableExecutionError(checkpoint, e)
def recover(session_id: str, store: CheckpointStore):
checkpoint = store.load(session_id)
if checkpoint is None:
raise ValueError("kurtarılacak bir checkpoint yok")
if checkpoint.state == AgentState.FAILED:
# checkpoint.current_step'ten devam eder, tamamlanmış adımları tekrarlamaz
checkpoint.state = AgentState.PLANNING
return execute_plan(checkpoint, store)
Anti-pattern’ler
- Herhangi bir hatadan sonra çok adımlı bir agent planını adım 0’dan yeniden başlatmak — token/maliyet israfı ve adımlar idempotent değilse yan etkilerin tekrarlanması.
- “Demoyu çalışır durumda tutmak için” çıplak bir
except: passile exception’ları yutmak — agent sistemlerinde sessiz production veri kaybının en yaygın tek nedeni budur. - Dead-letter/triage yolu olmaması — kurtarılamaz hatalar sadece kaybolur.
2.6 Guardrails
Prensip: Guardrail’ler, agent’ın talimat aldığı şeyden bağımsız olarak yapmasına izin verilen şeyi kısıtlar — prompt injection, model hatası ve kapsam genişlemesine karşı derinlemesine savunma.
Best practices
- Guardrail’leri modelin dışında, kodda uygula — “üretim verisini asla silme” diyen bir system prompt bir öneridir; o context’te sadece
delete_*tool’larını açığa çıkarmayan bir tool-izin katmanı bir garantidir. - Guardrail’leri katmanla: input filtreleme (retrieve edilmiş/güvenilmeyen içerik üzerinde prompt injection tespiti), output filtreleme (PII sızıntısı, politika ihlalleri) ve eylem filtreleme (tool çalıştırmadan önce izin kontrolleri).
- Agent’ın dış dünyadan okuduğu herhangi bir içeriği, user input ile aynı seviyede güvenilmeyen input olarak ele al — bir tool tarafından fetch edilen bir web sayfası, e-posta veya doküman, enjekte edilmiş talimatlar içerebilir (“önceki talimatları görmezden gel…”) ve bir güven sınırı olmadan doğrudan tool çağrılarını tetikleyememelidir.
- Yüksek riskli eylemler için block-list yerine allow-list kullan — her kötü eylemi saymaya çalışmak yerine neyin izin verildiğini (finansal limitler, izin verilen alıcılar, izin verilen dosya yolları) numaralandır.
- Eylemleri oturum/tenant başına rate-limitle ve kapsamlandır — tek bir ele geçirilmiş veya arızalı oturum, paylaşılan bir kaynağı tüketememeli veya diğer tenant’ları etkilememeli.
- Guardrail hataları kapalı başarısız olmalı, açık değil — guardrail kontrolünün kendisi hata verirse, riskli eylemi geçirmek yerine engelle.
Design pattern: Katmanlı guardrail pipeline’ı
from dataclasses import dataclass
@dataclass
class GuardrailViolation(Exception):
layer: str
reason: str
class GuardrailPipeline:
"""Input, output ve eylem guardrail'lerini bağımsız, birleştirilebilir
katmanlar olarak uygular. Internal hatada kapalı başarısız olur."""
def __init__(self, input_checks, output_checks, action_checks):
self.input_checks = input_checks
self.output_checks = output_checks
self.action_checks = action_checks
def check_input(self, untrusted_content: str):
for check in self.input_checks:
try:
if not check(untrusted_content):
raise GuardrailViolation("input", check.__name__)
except GuardrailViolation:
raise
except Exception:
raise GuardrailViolation("input", f"{check.__name__}_errored") # kapalı başarısız ol
def check_action(self, tool_name: str, tool_args: dict, session_scope: dict):
for check in self.action_checks:
if not check(tool_name, tool_args, session_scope):
raise GuardrailViolation("action", f"{tool_name}_denied")
def detect_prompt_injection(text: str) -> bool:
"""İçerik geçerse (güvenliyse) True döner."""
suspicious_markers = ["ignore previous instructions", "system:", "you are now"]
return not any(m in text.lower() for m in suspicious_markers)
def within_financial_limit(tool_name, args, scope):
if tool_name != "issue_refund":
return True
return args.get("amount", 0) <= scope.get("max_refund", 0)
Anti-pattern’ler
- Kod seviyesinde uygulama olmadan tehlikeli eylemleri önlemek için yalnızca system prompt’a güvenmek.
- Tool ile getirilen web/e-posta/doküman içeriğini kullanıcı chat input’u gibi güvenilir muamele etmek (sonuçta kullanıcı tarafından yazılmadı, ama incelenmedi de — güvenilmeyen olarak ele al).
- Kontrolün kendisi bir hata fırlattığında eylemi açık bırakan (izin veren) guardrail kontrolleri.
2.7 Validation
Prensip: Validation, “model token üretti” ile “sistem gerçekler üzerine hareket etti” arasındaki sınırdır. Her sınır geçişi bir validator’a ihtiyaç duyar — input’lar, tool argümanları ve output’lar aynı şekilde.
Best practices
- Her sınırda doğrula: user input → agent, retrieve edilmiş context → agent, tool argümanları → tool çalıştırma, model output’u → downstream consumer. Dört ayrı sınır, dört ayrı validator.
- Ad-hoc
ifkontrolleri yerine şema validasyonunu (Pydantic/JSON Schema) tercih et — deklaratif, test edilebilir ve kendi kendini dokümante eden. - Sadece şekli değil, semantiği doğrula. Bir
datealanı sözdizimsel olarak geçerli bir ISO tarih olabilir ama yine de anlamsız olabilir (ör. gelecekte bir iade tarihi, 250 yaşında bir yaş) — type-checking’in ötesinde business-kural validator’ları ekle. - Sadece nihai output’u değil, tool argümanlarını da çalıştırmadan önce doğrula — aralık dışı bir parametre halüsinasyonu gören bir agent, yan etkiden sonra değil önce yakalanmalı.
- Domain’ine özgü bilinen hata modları için “sanity check” validator’ları ekle — ör. cent/dolar karışıklığı yüzünden 100 kat yanlış olan bir amount alanı, açık bir sınır kontrolünü gerektirecek kadar yaygındır.
Design pattern: Çok-sınırlı validasyon
from pydantic import BaseModel, field_validator
from datetime import date
class RefundRequest(BaseModel):
order_id: str
amount_cents: int
reason: str
@field_validator("amount_cents")
@classmethod
def sane_amount(cls, v):
if v <= 0:
raise ValueError("miktar pozitif olmalı")
if v > 1_000_000: # $10.000 — sadece tip geçerliliği değil, business-kural tavanı
raise ValueError("miktar otomatik onay tavanını aşıyor")
return v
def validate_and_execute_tool(tool_name: str, raw_args: dict, schema: type[BaseModel]):
try:
validated = schema.model_validate(raw_args)
except ValidationError as e:
return {"status": "validation_error", "detail": e.errors()}
return execute_tool(tool_name, validated)
Anti-pattern’ler
- Sadece nihai kullanıcıya dönük output’u doğrulamak, tool argümanlarına körü körüne güvenmek — genelde daha tehlikeli sınır, yan etkiye daha yakın olandır.
- Business-kural kontrolleri olmadan sadece tip doğrulaması — “sözdizimsel olarak geçerli ama semantik olarak saçma” veri yine de production olaylarına yol açar.
- Bir self-repair döngüsüne düzeltecek hiçbir şey vermeyen belirsiz validasyon hataları (“geçersiz istek”).
2.8 Human Escalation
Prensip: İnsan-döngüde (human-in-the-loop) otomasyonun bir başarısızlığı değildir — diğer herhangi bir fallback kadar bilinçli tasarlanması gereken tasarlanmış bir güvenilirlik katmanıdır.
Best practices
- Baştan açık escalation tetikleyicileri tanımla: eşik-altı güven, guardrail ihlali, tekrarlanan başarısızlık, yüksek riskli eylem (geri alınamaz veya bir maliyet/etki eşiğinin üstünde) ve kullanıcı talepli escalation.
- Escalation, sadece “bir şeyler ters gitti” değil, tam context’i devretmelidir — insan, konuşmaya, agent’ın reasoning/planına, şimdiye kadarki tool sonuçlarına ve neden escalate ettiğine ihtiyaç duyar.
- Escalation UX’ini ham bir log dökümü değil, öncelikli bir kuyruk olarak tasarla — aciliyete göre grupla ve reviewer’a hızlı hareket etmek için gereken minimum context’i ver.
- Escalation oranını birinci sınıf bir ürün metriği olarak takip et — yükselen bir escalation oranı, genellikle açık hata metriklerinde görünmeden önce model/prompt sapmasının veya yeni bir hata modunun erken sinyalidir.
- Döngüyü kapat: insan çözümlerini evaluation verisine geri besle — insan tarafından çözülen her escalation, bir sonraki eval seti veya fine-tuning turu için etiketli bir örnektir.
- Mümkün olduğunda escalation’ı geri döndürülebilir yap — insan kararının, sonsuza kadar tekrarlanan tek seferlik manuel bir düzeltme yerine, bir politika güncellemesi (yeni bir guardrail kuralı, yeni bir few-shot örneği) olarak yakalanmasına izin ver.
Design pattern: Tam context devri ile escalation
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class EscalationTicket:
session_id: str
reason: str
confidence: float | None
conversation: list[dict]
agent_plan: list[str]
tool_results: dict
created_at: datetime = field(default_factory=datetime.utcnow)
priority: str = "normal"
class EscalationQueue:
def __init__(self, backend):
self.backend = backend
def escalate(self, checkpoint: AgentCheckpoint, reason: str, confidence: float | None = None):
priority = "high" if reason in {"guardrail_violation", "financial_high_risk"} else "normal"
ticket = EscalationTicket(
session_id=checkpoint.session_id,
reason=reason,
confidence=confidence,
conversation=checkpoint.tool_results.get("conversation", []),
agent_plan=checkpoint.plan,
tool_results=checkpoint.tool_results,
priority=priority,
)
self.backend.enqueue(ticket)
return ticket
def should_escalate(confidence: float, threshold: float = 0.6) -> bool:
return confidence < threshold
Anti-pattern’ler
- Sadece bir hata mesajıyla, konuşma/plan context’i olmadan escalate etmek — insanı durumu sıfırdan yeniden inşa etmeye zorlar.
- Güven sinyali veya açık tetikleyici olmaması — escalation ad hoc/tutarsız gerçekleşir veya bir müşteri şikayet edene kadar hiç gerçekleşmez.
- Her insan çözümünü, eval setlerine ve guardrail kurallarına geri beslemek yerine tek kullanımlık olarak ele almak.
3. Evaluation
Prensip: “Çalışıyor” bir metrik değil. Bir agent sistemi, ship edilmeden önce, ilk olaydan sonra değil, ölçülebilir, tekrarlanabilir, versiyonlanmış bir evaluation harness’ine ihtiyaç duyar.
3.1 Ne ölçülmeli
Agent
│
┌───────┴───────┐
▼ ▼
Başarı Başarısızlık
│ │
┌──────┼──────┐ ┌────┴────┐
▼ ▼ ▼ ▼ ▼
kalite latency maliyet tür sıklık
- Kalite: göreve özel doğruluk (exact match, rubrik-skorlu, LLM-judged, insan-review’lu) — görev için hala geçerli olan en ucuz yöntemi seç.
- Latency: p50/p90/p99 ve stream edilen yanıtlar için time-to-first-token’ı time-to-completion’dan ayrı ölç.
- Maliyet: başarılı sonuç başına token ve $ (çağrı başına değil — retry edilen bir çağrı, token sayısının ima ettiğinden daha pahalıdır).
- Hata taksonomisi: hataları sınıflandır (halüsinasyon, tool hatası, timeout, guardrail bloğu, validasyon hatası, escalation) — toplu bir “hata oranı” hangi hata modunun önce düzeltileceğini gizler.
3.2 Best practices
- Agent’ı inşa etmeden önce altın (golden) bir dataset inşa et, sonra değil. Elle etiketlenmiş 30-50 temsili vaka bile çoğu regresyonu yakalar; zamanla production’daki ucuz atlatmalardan büyüt.
- Unit-seviyesi eval’leri end-to-end eval’lerden ayır. Unit: extraction tool çağrısı, bu input verildiğinde doğru argümanları alıyor mu? End-to-end: tüm çok-turlu oturum doğru nihai sonuca ulaşıyor mu? İkisine de ihtiyaç var; sadece end-to-end, nerede koptuğunu gizler.
- LLM-as-judge’ı dikkatle kullan: önce judge’ı insan-etiketli örneklere karşı kalibre et, “1-10 puanla” yerine bir rubrik kullan ve periyodik olarak judge/insan uyumunu denetle — kalibre edilmemiş bir judge, önyargıyı objektif görünen bir sayıya sadece aklamış olur.
- Eval metriklerini sadece rolling toplu olarak değil, prompt/model versiyonu başına takip et — bir kalite değişikliğini onu yaratan değişikliğe atfetmen gerekir.
- Her prompt veya agent-logic değişikliğinde eval suite’ini CI’da çalıştır — bir prompt değişikliği bir kod değişikliğidir ve bir kod değişikliğiyle aynı regresyon kapısına ihtiyaç duyar.
- Her production olayından regresyon testleri inşa et — her gerçek başarısızlık kalıcı bir eval vakası haline gelir, böylece bir daha asla sessizce regresyon yapamaz.
- Offline eval’i (dataset güdümlü, deploy öncesi) online eval’den (canlı trafik örneklemesi, deploy sonrası) ayır — offline bilinen regresyonları hızlıca yakalar; online, offline verinin kapsamadığı dağılım kaymasını ve uzun-kuyruk hatalarını yakalar.
- Model/prompt değişikliklerini tam rollout’tan önce bir flag arkasında production’da A/B test et, eval metrikleri otomatik geri alma için guardrail olarak.
Design pattern: Eval harness iskeleti
from dataclasses import dataclass
from typing import Callable, Any
@dataclass
class EvalCase:
id: str
input: Any
expected: Any
tags: list[str]
@dataclass
class EvalResult:
case_id: str
passed: bool
score: float
latency_ms: float
cost_tokens: int
failure_mode: str | None = None
class EvalHarness:
def __init__(self, cases: list[EvalCase], scorer: Callable[[Any, Any], float],
pass_threshold: float = 0.8):
self.cases = cases
self.scorer = scorer
self.pass_threshold = pass_threshold
def run(self, agent_fn: Callable[[Any], Any]) -> list[EvalResult]:
results = []
for case in self.cases:
start = time.monotonic()
try:
output = agent_fn(case.input)
score = self.scorer(output, case.expected)
results.append(EvalResult(
case_id=case.id,
passed=score >= self.pass_threshold,
score=score,
latency_ms=(time.monotonic() - start) * 1000,
cost_tokens=getattr(output, "token_usage", 0),
))
except Exception as e:
results.append(EvalResult(
case_id=case.id, passed=False, score=0.0,
latency_ms=(time.monotonic() - start) * 1000,
cost_tokens=0, failure_mode=type(e).__name__,
))
return results
def summary(self, results: list[EvalResult]) -> dict:
n = len(results)
return {
"pass_rate": sum(r.passed for r in results) / n,
"avg_score": sum(r.score for r in results) / n,
"p90_latency_ms": sorted(r.latency_ms for r in results)[int(n * 0.9)],
"total_cost_tokens": sum(r.cost_tokens for r in results),
"failure_modes": {r.failure_mode for r in results if r.failure_mode},
}
Anti-pattern’ler
- Tüm evaluation stratejisi olarak “birkaç kez elle test ettim” ile bir agent’ı ship etmek.
- Hata-modu dökümü olmayan tek bir toplu doğruluk sayısı — “%38 hata oranı” için bir düzeltmeyi önceliklendiremezsin, ama “%22 tool-argüman halüsinasyonu, %10 timeout, %6 guardrail bloğu” için edebilirsin.
- Hiç büyümeyen eval dataset’i — eval vakalarına dönüştürülmeyen production olayları tekrarlanacaktır.
- İnsan yargısına karşı hiç kontrol edilmeden güvenilen LLM-judge skorları.
4. Observability
Prensip: Herhangi bir tek istek için sonradan tam olarak ne olduğunu — her prompt’u, her tool çağrısını, her model yanıtını, her retry’ı — yeniden inşa edemiyorsan, bir production agent sistemine değil, bir UI’lı kara kutuya sahipsin.
4.1 Trace hiyerarşisi
request
↓
graph (bu istek için genel agent workflow'u / DAG)
↓
node (graph içindeki bir adım — plan, retrieve, generate, act)
↓
agent (multi-agent ise belirli bir alt-agent veya rol)
↓
tool (bireysel tool çağrısı)
↓
LLM (bireysel model API çağrısı — prompt, yanıt, token, latency)
4.2 Best practices
- Hiyerarşinin her katmanı, paylaşılan bir trace ID’li bir span alır — tek bir
request_id, tracing backend’inde (ideal olarak OpenTelemetry uyumlu) üst-seviye istekten bireysel LLM çağrılarına kadar tüm ağacı yeniden inşa etmene izin vermelidir. - Her LLM çağrısı için kısaltılmış bir özet değil, tam prompt’u ve tam yanıtı logla — onu üreten tam input olmadan kötü bir çıktıyı debug edemezsin. PII’yi loglama katmanında redakte et, loglamayı tamamen atlama.
- Her span’e yapısal metadata ekle: model versiyonu, prompt versiyonu, token sayıları (input/output/cached), latency, maliyet, retry sayısı, kullanılan katman (bkz. fallback’ler).
- Agent kararlarını sonuçlarla ilişkilendir — her span’i genel isteğin nihai başarı/başarısızlığıyla etiketle, böylece “extraction tool’unun çağrıldığı ve genel isteğin başarısız olduğu tüm trace’leri göster” diye sorgulayabilirsin.
- Altyapı observability’sini (latency, hata oranı, uptime) davranışsal observability’den (agent ne karar verdi, neden, doğru muydu) ayır — ikisi de gerekli, hiçbiri diğerinin yerine geçmez.
- Uniform değil, akıllıca örnekle — başarısızlıkları, escalation’ları ve guardrail tetiklemelerini her zaman tam olarak trace et (nadir olaylar, yüksek değer); başarıları maliyet/hacmi kontrol etmek için daha düşük oranda örnekle.
- Bir session replay görünümü inşa et — support mühendisleri, takılı/başarısız bir oturumu ham JSON logları olarak değil, okunabilir bir zaman çizelgesi olarak (prompt → yanıt → tool çağrısı → tool sonucu → sonraki prompt) görebilmeli.
- Sadece altyapı metriklerinde değil, davranışsal metriklerde alarm kur — yükselen escalation oranı, yükselen halüsinasyon oranı (otomatik kontrollerden) veya canlı trafikte düşen eval-proxy skorları, altyapı metriklerinin tamamen kaçırdığı öncü göstergelerdir.
Design pattern: Hiyerarşik tracing
from contextvars import ContextVar
from dataclasses import dataclass, field
from datetime import datetime
import uuid
current_trace: ContextVar[str] = ContextVar("current_trace", default="")
@dataclass
class Span:
span_id: str
parent_id: str | None
trace_id: str
layer: str # request | graph | node | agent | tool | llm
name: str
start: datetime = field(default_factory=datetime.utcnow)
end: datetime | None = None
metadata: dict = field(default_factory=dict)
class Tracer:
"""Minimal hiyerarşik tracer — production'da backend'i
OpenTelemetry/Honeycomb/Datadog ile değiştir."""
def __init__(self, sink: Callable[[Span], None]):
self.sink = sink
self._stack: list[Span] = []
def start_span(self, layer: str, name: str, **metadata) -> Span:
trace_id = current_trace.get() or str(uuid.uuid4())
current_trace.set(trace_id)
parent = self._stack[-1].span_id if self._stack else None
span = Span(str(uuid.uuid4()), parent, trace_id, layer, name, metadata=metadata)
self._stack.append(span)
return span
def end_span(self, span: Span, **result_metadata):
span.end = datetime.utcnow()
span.metadata.update(result_metadata)
self._stack.pop()
self.sink(span) # logging/tracing backend'e gönder
# Kullanım
tracer = Tracer(sink=lambda s: logger.info("span", **s.__dict__))
def call_llm_traced(tracer, prompt, model):
span = tracer.start_span("llm", model, prompt_preview=prompt[:200])
response = client.messages.create(model=model, messages=[{"role": "user", "content": prompt}], max_tokens=1024)
tracer.end_span(span,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
cached_tokens=getattr(response.usage, "cache_read_input_tokens", 0),
)
return response
Anti-pattern’ler
- Ara span verisi olmadan sadece “agent tamamlandı” / “agent başarısız oldu” loglamak — belirli bir hata modunun kök nedeninin bulunması gerektiği ilk anda tamamen debug edilemez.
- “Log hacmini azaltmak için” gerçek prompt/yanıt içeriğini kırpmak veya atlamak — bir olay sırasında en çok ihtiyaç duyduğun veri tam olarak budur.
- request → graph → node → agent → tool → LLM hiyerarşisi boyunca paylaşılan bir trace ID olmaması → her katmanın logları birleştirilemeyen bir ada dönüşür.
- Çoğu başarısızlık trace’ini başarıların gürültüsüyle birlikte düşüren uniform örnekleme.
5. Hepsini Bir Araya Getirmek
Production seviyesinde bir agent isteği, prensipte, yukarıdaki her katmandan geçmelidir:
Kullanıcı isteği
│
▼
[Guardrail: input kontrolü] ──başarısız──► reddet / sanitize et
│ geçti
▼
[Context Assembler: bütçelenmiş, sıralanmış retrieval]
│
▼
[Model Router: görev karmaşıklığına göre katman seç]
│
▼
[Deadline-sınırlı agent döngüsü]
│
├─► [Tool çağrısı] ──idempotency key──► [Validation] ──► çalıştır ──► [Trace: tool span]
│ │
│ └─başarısız──► [Retry politikası] ──tükendi──► [Fallback zinciri]
│
▼
[Structured output üretimi] ──validasyon başarısız──► [Self-repair döngüsü] ──tükendi──► [Escalation]
│ geçti
▼
[Guardrail: output kontrolü] ──başarısız──► [Escalation]
│ geçti
▼
[Checkpoint durumu kalıcı hale getirildi] ──► yanıt kullanıcıya stream edildi
│
▼
[Tam trace gönderildi: request → graph → node → agent → tool → LLM]
│
▼
[Sonuç kaydedildi] ──► [Eval dataset / regresyon suite güncellendi]
Bu dokümanın tek cümlelik özeti: bir agent, bir kere doğru bir cevap ürettiğinde “bitmiş” değildir — güvenilir, ucuz, gözlemlenebilir ve kurtarılabilir şekilde doğru bir cevabı, yaptığı her dış çağrının er ya da geç başarısız olacağı varsayımı altında ürettiğinde bitmiştir.