Üretime çıkan bir AI agent’ın test raporu yeşil görünür ama kimse çıktının iyi olup olmadığını bilmez. Araç çağrıları doğru sırayla yapılmıştır, API 200 döner, state makinesi beklenen geçişi yapar — ve model kullanıcıya yanlış bir fiyat söyler. Klasik test altyapısı bu hatayı görmez çünkü hata bir istisna değil, bir cümle.
Bu yazı, deterministik test ile eval katmanının nerede ayrıldığını, eval’i hangi sırayla kurmanız gerektiğini ve agent sayısı arttığında neyin kırıldığını anlatıyor. Kalite sorumluluğu sizdeyse, eval’i yeni bir disiplin değil, test stratejinizin eksik katmanı olarak düşünün.
AI agent’ın test edilemeyen kısmı hangisi
Bir agent’ın büyük kısmı sıradan yazılımdır. Kullanıcı girdisinin şema doğrulaması, prompt şablonunun değişkenlerle doldurulması, model yanıtındaki tool_call nesnesinin parse edilmesi, o araca karşılık gelen API isteğinin atılması, dönen veriyle state’in güncellenmesi, konuşma geçmişinin saklanması. Bunların hepsi deterministiktir ve hepsi klasik testle kapsanır. Kapsanmadığını görüyorsanız sorun AI değil, eksik test.
Kapsanamayan tek şey model çağrısının kendisidir. Aynı girdiye aynı modelden iki farklı cümle gelir; ikisi de doğru olabilir, biri yanlış olabilir, ikisi de sözdizimsel olarak geçerlidir. assert response == expected yazamazsınız. assert "iade" in response yazarsanız test, anlamı değil kelimeyi doğrular — ve ilk prompt değişikliğinde kırılır.
Buradaki boşluğu kapatan şeye eval deniyor. Eval, test piramidinin katmanlarından biri değil; piramidin yanında duran ayrı bir yapı. Çünkü ikili sonuç üretmiyor, farklı bir veri sahipliği istiyor ve CI’da farklı davranması gerekiyor. Bunu baştan ayrı bir şey olarak konumlandırmazsanız, eval’i test suite’inin içine sıkıştırıp sürekli kırmızı yanan bir job elde edersiniz ve üç hafta sonra kimse bakmaz.
Eval nedir, testten farkı nerede başlar
Test ikili sonuç verir: geçer veya kalır. Eval skor verir, skoru bir eşikle karşılaştırır ve kararı eşik üretir. Bu küçük fark üç şeyi değiştirir.
CI davranışı. Test kalırsa build kırmızıdır, tartışma yok. Eval skoru 0.91’den 0.88’e düştüğünde bunun regresyon mu gürültü mü olduğu karar gerektirir. Eşiği ve kapı politikasını siz tanımlarsınız.
Hata raporlama. Düşen test bir stack trace verir. Düşen eval bir örnek listesi verir: hangi girdilerde skor düştü, önceki çıktı neydi, şimdiki ne. Raporun birimi “test adı” değil, “örnek kimliği”.
Sahiplik. Eval’in üç bileşeni var ve üçü farklı kişilerin sorumluluğundadır:
- Veri kümesi — ürün sahibi ve domain uzmanı sahiplenir. Hangi davranış doğru, bunu test uzmanı tek başına karara bağlayamaz.
- Puanlayıcı (scorer) — test uzmanı/SDET sahiplenir. Kod budur, versiyonlanır, test edilir.
- Eşik ve kapı politikası — ekip birlikte karar verir, ama sayıyı CI’a yazan kişi sahibidir.
Bu üçünü aynı kişiye yıkarsanız veri kümesi mühendis hayal gücünün ürünü olur ve eval, modelin değil mühendisin varsayımlarını ölçer.
Agent mimarisini test edilebilir katmanlara ayırmak
Agent’ı dört katmana bölün ve her katmanı kendi test türüyle eşleyin.

1. Giriş doğrulama ve prompt oluşturma. Saf fonksiyon. Girdi nesnesi verip çıkan prompt metnini assert edersiniz. Şablon değişkeni eksik kaldığında, kullanıcı girdisi prompt sınırını aştığında, enjeksiyon denemesi geldiğinde ne olduğu burada test edilir. Birim testi, milisaniye.
2. Model çağrısı. Tek stokastik katman. Diğer katmanların testlerinde bu katman mock’lanır veya stub’lanır — kaydedilmiş model yanıtlarıyla çalışan bir fake yeterlidir. Gerçek modeli yalnızca eval çağırır.
3. Araç/fonksiyon yürütme. Agent’ın dış dünyaya dokunduğu yer ve üretimdeki en pahalı hataların kaynağı. Model refund(order_id, amount) çağırdığında amount’un negatif olamayacağını model değil kodunuz garanti etmeli. Bu katman API test otomasyonunun doğrudan konusudur; araç şemaları birer sözleşmedir ve sözleşme testiyle kapsanır.
4. Çıktı işleme ve sunum. JSON parse, markdown render, kaynak linklerinin eklenmesi, güvenlik filtreleri. Bozuk model çıktısı geldiğinde uygulamanın çökmemesi bu katmanın testidir — ve bozuk çıktı istisna değil, beklenen durumdur.
Kural basit: model çağrısı dışındaki her şey deterministik testle kapsanabilir ve kapsanmalıdır. Eval’e kalan yüzey ne kadar küçükse eval o kadar ucuz ve o kadar okunur olur.
Altın veri kümesini kurmak: 30 örnekle başla
Eval veri kümesi üretim loglarından türetilir, hayal gücünden değil. Mühendisin masa başında yazdığı örnekler, mühendisin agent’tan beklediği kullanımı yansıtır; kullanıcıların yaptığını değil. Agent zaten üretimdeyse elinizde gerçek dağılım var.
Üç kategoriden örnek toplayın:
- Tipik kullanım. En sık görülen niyetler. Log’lardaki ilk girdileri frekansa göre kümeleyin, her kümeden birkaç tane alın.
- Sınır durumlar. Eksik bilgi, çok adımlı istek, dil karışımı, agent’ın yetkisi dışındaki talep, kullanıcının fikir değiştirdiği konuşma.
- Bilinen üretim hataları. Destek ticket’ına dönmüş, geri alınmış veya şikâyet almış her vaka doğrudan veri kümesine girer. Bunlar en değerli örneklerdir çünkü başarısızlıkları kanıtlanmıştır.
30 iyi seçilmiş örnek, 500 rastgele örnekten daha iyi sinyal verir. Sebep basit: 30 örneğin her birinin beklenen davranışı bir insan tarafından yazılmış ve doğrulanmıştır. 500 örneğin beklenen çıktısını kimse okumadıysa, eval’iniz modelin bugünkü davranışını “doğru” ilan eden bir snapshot testinden ibarettir.
İki operasyonel detay ihmal edilmesin. Veri kümesi kod reposunda, versiyonlu bir dosyada durur — golden/v3/cases.jsonl gibi. Skor karşılaştırmaları ancak aynı veri kümesi sürümü içinde anlamlıdır; veri kümesi değiştiyse baseline da sıfırlanır. İkincisi: üretim log’u PII içerir. Örnekleri dataset’e almadan önce isim, e-posta, sipariş numarası, adres alanlarını değiştirin; bunu elle değil, dataset’e yazan script’in içinde zorunlu bir adım olarak yapın. Test verisi üretimi konusundaki aynı disiplin burada da geçerli.
Puanlayıcı seçimi: deterministik, model tabanlı ve insan

Sıra önemlidir. Önce deterministik kontrolleri tüketin:
- Çıktı beklenen JSON şemasına uyuyor mu?
- Zorunlu alanlar dolu mu, tip doğru mu?
- Yasaklı içerik var mı (rakip adı, fiyat taahhüdü, yasal uyarı eksikliği)?
- Doğru araç, doğru argümanlarla çağrıldı mı?
- Yanıt kaynak gösteriyorsa, gösterilen kaynak gerçekten getirilen dokümanlar arasında mı?
Bu kontroller agent kalite sorunlarının önemli bir bölümünü yakalar ve hiç token harcamaz. “Model doğru şeyi söyledi mi” sorusunu sormadan önce “model sözleşmeye uydu mu” sorusunu sorun.
LLM-as-judge yalnızca deterministik kontrolün yetmediği yerde devreye girer: ton, yardımcı olma derecesi, verilen bağlama sadakat, özetin eksiksizliği. Judge’ı kalibre etmeden kullanmak, kalibre edilmemiş bir ölçü aletiyle rapor yazmaktır.
İnsan değerlendirmesi örnekleme ile yapılır: her sürüm öncesi 20-30 çıktıyı domain uzmanı okur. Amaç tam kapsama değil, otomatik puanlayıcıların kaçırdığı sistematik sapmayı görmek.
Deterministik puanlayıcıyı CI’a bağlamak
Ayrı bir eval platformu şart değil. Pytest yeter: parametrize ile her örneği çalıştırın, skoru toplayın, session sonunda eşiği uygulayın ve sonucu artefakt olarak yazın.
golden/cases.jsonl:
{"id": "refund-001", "input": "12345 numaralı siparişimi iade etmek istiyorum", "expect_tool": "create_refund", "expect_args": {"order_id": "12345"}, "forbidden": ["kesinlikle", "garanti ediyorum"]}
{"id": "status-002", "input": "Kargom nerede?", "expect_tool": "get_order_status", "expect_args": {}, "forbidden": []}
{"id": "outofscope-003", "input": "Bana bir şiir yaz", "expect_tool": null, "expect_args": {}, "forbidden": ["create_refund"]}
conftest.py:
import json
import pathlib
import statistics
ARTIFACT_DIR = pathlib.Path("eval-artifacts")
BASELINE_FILE = pathlib.Path("eval-baseline.json")
MAX_DROP = 0.03 # baseline'a göre izin verilen düşüş
def pytest_configure(config):
config.eval_records = []
def pytest_sessionfinish(session, exitstatus):
records = getattr(session.config, "eval_records", [])
if not records:
return
by_case = {}
for record in records:
by_case.setdefault(record["case_id"], []).append(record["score"])
case_means = {cid: statistics.mean(s) for cid, s in by_case.items()}
case_stdev = {
cid: (statistics.pstdev(s) if len(s) > 1 else 0.0)
for cid, s in by_case.items()
}
overall = statistics.mean(case_means.values())
ARTIFACT_DIR.mkdir(exist_ok=True)
(ARTIFACT_DIR / "results.json").write_text(
json.dumps(
{
"overall": round(overall, 4),
"case_means": case_means,
"case_stdev": case_stdev,
"runs": records,
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
print(f"\nEval skoru: {overall:.3f}")
if not BASELINE_FILE.exists():
print("Baseline dosyasi yok. Bu kosu baseline olarak kaydedilebilir.")
return
baseline = json.loads(BASELINE_FILE.read_text(encoding="utf-8"))["overall"]
drop = baseline - overall
print(f"Baseline: {baseline:.3f} | Delta: {-drop:+.3f}")
if drop > MAX_DROP:
worst = sorted(case_means.items(), key=lambda kv: kv[1])[:5]
print("Esik asildi. En dusuk skorlu ornekler:")
for cid, score in worst:
print(f" {cid}: {score:.2f}")
session.exitstatus = 1
test_eval.py:
import json
import pathlib
import pytest
from app.agent import run_agent # -> {"text": str, "tool_calls": [{"name","args"}]}
RUNS_PER_CASE = 3
CASES = [
json.loads(line)
for line in pathlib.Path("golden/cases.jsonl").read_text(encoding="utf-8").splitlines()
if line.strip()
]
def score_schema(result):
if not isinstance(result, dict):
return 0.0
if not isinstance(result.get("text"), str) or not result["text"].strip():
return 0.0
if not isinstance(result.get("tool_calls"), list):
return 0.0
return 1.0
def score_tool(result, case):
names = [call.get("name") for call in result.get("tool_calls", [])]
expected = case["expect_tool"]
if expected is None:
return 1.0 if not names else 0.0
if expected not in names:
return 0.0
call = next(c for c in result["tool_calls"] if c.get("name") == expected)
args = call.get("args", {})
for key, value in case["expect_args"].items():
if str(args.get(key)) != str(value):
return 0.5
return 1.0
def score_forbidden(result, case):
haystack = json.dumps(result, ensure_ascii=False).lower()
hits = [t for t in case["forbidden"] if t.lower() in haystack]
return 0.0 if hits else 1.0
SCORERS = {
"schema": (lambda r, c: score_schema(r), 0.2),
"tool": (score_tool, 0.6),
"forbidden": (score_forbidden, 0.2),
}
@pytest.mark.parametrize("case", CASES, ids=[c["id"] for c in CASES])
@pytest.mark.parametrize("run_index", range(RUNS_PER_CASE))
def test_eval_case(case, run_index, request):
result = run_agent(case["input"])
breakdown = {name: fn(result, case) for name, (fn, _) in SCORERS.items()}
total = sum(breakdown[name] * weight for name, (_, weight) in SCORERS.items())
request.config.eval_records.append(
{
"case_id": case["id"],
"run": run_index,
"score": round(total, 4),
"breakdown": breakdown,
"output": result,
}
)
Tek örnek düştüğünde test kalmıyor; karar session sonunda toplu skordan veriliyor. CI’da eval-artifacts/results.json dosyasını artefakt olarak yükleyin — düşüşü incelemek için gereken tek şey o.
Model tabanlı puanlayıcıyı kalibre etmek
LLM-as-judge’ı güvenilir kılan model değil, rubriktir. “Bu yanıt iyi mi? 1-5 arası puanla” diyen bir judge gürültü üretir. İşe yarayan rubrik, her puan seviyesinin gözlemlenebilir bir tanımını verir: “3 = kullanıcının sorusunu yanıtlıyor ama verilen bağlamda olmayan bir bilgi ekliyor.”
Kalibrasyon yordamı şudur: insan etiketli 50 örnek alın, judge’ı bu 50 örnek üzerinde çalıştırın, judge etiketi ile insan etiketinin uyum oranını ölçün. İkili kararlarda (kabul/ret) basit uyum yüzdesi yeterlidir; çok seviyeli ölçekte “bir puandan fazla sapma” oranına bakın. Pratik eşik %85 civarıdır. Uyum bunun altındaysa refleks judge modelini değiştirmek olur — yanlış refleks. Önce rubriğe bakın. Tipik olarak uyumu bozan şey, rubrikte aynı anda iki şeyi ölçen bir kriterdir: “doğru ve kibar”. Bunu iki ayrı kritere bölmek, judge modelini büyütmekten daha çok iş görür. Anlaşmazlıkların yaşandığı örnekleri rubriğe few-shot olarak eklemek de uyumu belirgin biçimde yükseltir.
Judge prompt’u, rubrik ve kullanılan model sürümü birlikte versiyonlanan bir artefakttır. Judge prompt’unu değiştirdiyseniz, agent değişmemiş olsa bile baseline geçersizdir.
Eşik, flaky eval ve regresyon kapısı
Model çıktısı stokastik olduğu için tek koşuda düşen skor regresyon değildir. Her örneği n kez çalıştırın (3 pratik bir başlangıç) ve ortalamayı değil skorun dağılımını izleyin. Bir örnekte ortalama 0.9 ama standart sapma yüksekse, o örnek üç koşudan birinde başarısız oluyor demektir — kullanıcıların üçte biri için bozuk bir özellik, ortalama tarafından gizlenmiş durumda. Yüksek varyanslı örnekleri ayrı bir listede raporlayın. Bu, kararsız testin kök nedenini bulma refleksinin eval’deki karşılığıdır: varyansı tolere etmek değil, kaynağını bulmak.
Mutlak eşik (“skor 0.90’ın altına düşmesin”) kırılgandır; veri kümesine zor örnek ekleyince skor doğal olarak düşer ve kapı sürekli kırmızı yanar. Bunun yerine baseline’a göre delta kapısı kurun: önceki sürümün skoruna kıyasla X puandan fazla düşüş PR’ı bloklar. Yukarıdaki conftest.py‘de MAX_DROP tam olarak bunu yapıyor.
Sabit seed ve temperature=0 varyansı azaltır ama sıfırlamaz. Sağlayıcı tarafında batching, donanım farkları, model sürümünün sessiz güncellenmesi ve araç sonuçlarındaki zaman damgaları çıktıyı değiştirir. Determinizmi varsayıp n=1 ile koşarsanız, varyansı regresyon sanıp duran bir kapı elde edersiniz.
Kapının bloklayıcı mı uyarı mı olacağı ekibin eval’e duyduğu güvene bağlıdır ve bu güven ölçülebilir. Pratikte işleyen sıra şudur: kapı iki-üç hafta uyarı modunda çalışır, bu sürede eval’in verdiği her alarm elle incelenir. Alarmların büyük kısmı gerçek bir davranış değişikliğine işaret ediyorsa kapı bloklayıcıya çevrilir. Yanlış alarm oranı yüksekse sorun kapıda değil, veri kümesinde veya puanlayıcıdadır; kapıyı bloklayıcı yapmak bu durumda sadece ekibin kapıyı atlamayı öğrenmesini sağlar.
Üretimde değerlendirme: offline eval yetmez
Offline eval, veri kümesinin kapsadığı kadarını görür. Kullanıcılar dün gece denemediğiniz bir şey yapıyor ve o davranış dataset’inizde yok.
Üretim tarafında dört parça gerekir:
- Trace toplama. Her agent çalıştırması için girdi, oluşturulan prompt, model yanıtı, araç çağrıları ve sonuçları, nihai çıktı, gecikme ve token sayısı. Trace yoksa hata ayıklama tahmine dönüşür.
- Örnekleme ile online skorlama. Trafiğin küçük bir yüzdesini deterministik puanlayıcılardan geçirin. Şema uyumu ve yasaklı içerik kontrolü üretimde de bedavaya yakındır.
- Kullanıcı geri bildirim sinyali. Açık (başparmak) ve örtük (konuşmayı terk etme, aynı soruyu tekrarlama, insana aktarma talebi) sinyaller. Örtük sinyaller genelde daha çok veri üretir.
- Geri besleme. Düşük skorlu veya olumsuz geri bildirim almış trace’ler bir incelemeye düşer, insan beklenen davranışı yazar, örnek golden dataset’e girer.

Döngünün sağlığını tek bir sayıyla izleyebilirsiniz: bu hafta üretimden dataset’e kaç örnek girdi? Sıfırsa döngü kapalı değil, kopuk.
Maliyet ve koşu süresi: evalleri ne sıklıkta çalıştırmalı
Her PR’da tam eval çalıştırmak hem pahalı hem yavaş. Üç katmanlı strateji işi görür:
- Her PR’da: deterministik hızlı set. 50-100 örnek, n=1, yalnızca kod tabanlı puanlayıcılar. Dakikalar mertebesinde kalmalı; geliştirici beklerken koşuyor.
- Nightly: tam set, n=3, judge dahil. Süresi önemli değil, paralellik ve rate limit önemli.
- Sürüm öncesi: insan örneklemesi, 20-30 çıktı.
Maliyeti tahmin etmek için koşudan önce basit bir hesap yapın: örnek sayısı × tekrar sayısı × (agent token'ı + judge token'ı) × birim fiyat. Judge genelde agent’tan daha ucuz görünür ama her örnek için ayrı bir çağrı olduğundan toplamda yarı yarıya paya ulaşabilir. Bu hesabı CI job’ının çıktısına yazdırın; aylık fatura sürpriz olmasın.
Paralel koşu rate limit ile çatışır. 100 örneği 20 worker ile aynı anda çalıştırırsanız sağlayıcı 429 döndürür, retry’lar gecikmeyi büyütür ve koşu süresi tahmin edilemez hale gelir. Worker sayısını rate limit’e göre sabitleyin, retry’ları exponential backoff ile yapın ve 429 sayısını da artefakta yazın — koşu süresi uzadığında nedenini bilmek istersiniz.
Ölçeklerken kırılan şeyler
Tek agent’ta işleyen kurulum, agent sayısı arttığında dört yerden kırılır.
Veri kümesi bakım borcu. Her agent kendi dataset’ini taşır ve ürün davranışı değiştiğinde beklenen çıktılar bayatlar. Önlem: her örneğe son_dogrulama tarihi ve sahip alanı ekleyin; altı aydan eski örnekleri raporlayan bir job koşturun.
Judge model sürümü değişince baseline’ların kayması. Sağlayıcı judge modelini güncellediğinde bütün tarihsel skorlar karşılaştırılamaz hale gelir. Önlem: judge modelini sürüm sabitleyerek çağırın, sürüm değişimini planlı bir migrasyon olarak ele alın ve geçişte eski-yeni judge’ı aynı 50 insan etiketli örnek üzerinde yan yana ölçün.
Rate limit kuyruğu. On agent’ın nightly eval’i aynı saatte aynı API anahtarını tüketir. Önlem: evalleri zaman dilimine yayın, mümkünse ayrı kota/anahtar kullanın, koşuları merkezi bir kuyruktan sırayla alın.
Raporlama sahipsizliği. Eval sonucu kimseye gitmiyorsa eval yoktur. Önlem: her agent’ın bir sahibi olsun, skor düşüşü doğrudan o ekibe bildirilsin; organizasyon düzeyinde tek bir pano yeterli değildir çünkü genel ortalama tek bir agent’ın çöküşünü gizler.
AI destekli test araçları bu işlerin bir kısmını kolaylaştırır, ama veri kümesinin doğruluğu ve eşik kararı hâlâ insan işi.
Bu hafta ne yapacaksın
Sırayı bozmayın; her adım bir öncekine dayanıyor.
- Üretim loglarından 30 örnek çıkar. Tipik kullanım, sınır durum, bilinen hata. PII’yi temizle. Beklenen davranışı bir domain uzmanıyla birlikte yaz. Dosyayı repoya, versiyonlu bir klasöre koy.
- Deterministik puanlayıcıyla başla. Şema uyumu, zorunlu alanlar, yasaklı içerik, araç çağrısı doğruluğu. Judge yok, token harcaması yok.
- CI’a nightly job olarak bağla. Yukarıdaki pytest kurulumu yeterli. Sonucu artefakt olarak yaz, kapıyı ilk haftalarda uyarı modunda tut.
- Baseline’ı kaydet. Veri kümesi sürümü, model sürümü, puanlayıcı sürümü ile birlikte. Bu üçünden biri değişirse baseline da yenilenir.
- Ancak sonra judge ekle. Önce 50 örnek insan etiketi topla, rubriği yaz, uyumu ölç. Uyum yeterli değilse rubriği düzelt.
İlk sürümde LLM-as-judge ile başlamayın. Kalibre edilmemiş bir judge, ölçmediğiniz bir şeyin ölçüldüğü hissini verir — ve bu, hiç ölçmemekten daha zararlıdır, çünkü yanlış bir güvenle üretime çıkarsınız.