test(eval): sohbet eval verisi ve altyapısı güncellendi (iş 6) [KARAR]

- s104/s105: havuz düzeltmelerinden (B2) sonra öncül değişti; soru ve beklenti yeniden yazıldı.
- s084: icermemeli 'garanti' sorumluluk reddine takılıyordu; daraltıldı.
- s018/s033/s039/s048: kabul edilebilir cevapta araç şartı kaldırıldı (04 raporu §H).
- s088: adın kabul edilmesi metin kontrolüyle yakalanır.
- Kilitli (paketsiz) senaryoda Jev hakem bağlamına ürün bilgisi eklendi (299 TL, /paket yanlış alarmı).
- Paketsiz liste özeti kırpması danisman-prompt.ts'e taşındı (sohbetListeOzeti); route.ts ve eval aynı fonksiyonu kullanır, kopya kalktı. Davranış aynı.
- --tekrar N, tutarlılık özeti, ortalama araç çağrısı ve soru başı maliyet eklendi.
- Vaat regex'i 'AIS'e girersin', 'sınava girersin' cümlelerine takılmıyor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 08:14:51 +03:00
parent e06c486b1a
commit 080dcc9909
5 changed files with 74 additions and 31 deletions

View File

@@ -5,11 +5,11 @@
* pnpm eval:sohbet --limit 10 # ilk 10 (süzgeçlerden sonra)
* pnpm eval:sohbet --etiket vaat,kriz --profil p05 --soru s001,s035
* pnpm eval:sohbet --eszaman 5 --butce 1.5 --liste scripts/eval/sonuc/<robot>.json
* pnpm eval:sohbet --etiket vaat,baraj --tekrar 3 # her soru 3 kez (model stokastik)
*
* Bağlam route.ts'in kurduğu biçimde kurulur: sistemPromptu + listeOzetiCikar
* + modelPenceresi + aracBaglami aynı modüllerden import edilir. Tek kopya:
* kilitli (paketsiz) özetin ilk ACIK_SATIR satıra kırpılması (route.ts içinde,
* dışa aktarılmamış). Liste: en yeni `--model` robot koşusundan; yoksa havuzdan
* + sohbetListeOzeti + modelPenceresi + aracBaglami aynı modüllerden import
* edilir; kopya yok. Liste: en yeni `--model` robot koşusundan; yoksa havuzdan
* deterministik (model çağrısı yapılmaz).
*
* profil_guncelle gerçek kodla çalışır ama data/app.db'nin GEÇİCİ KOPYASINA
@@ -49,17 +49,19 @@ const paramsYap = (p: EvalProfil): RaporParams => ({ sira: p.sihirbaz.sira, tur:
type AracCagrisi = { ad: string; girdi: unknown; cikti: unknown; hata?: string };
/** danisman-prompt.ts'in kilitli kullanıcıya eklediği ürün bilgisinin hakem bağlamındaki karşılığı. */
const KILITLI_URUN_BILGISI = "Ürün bilgisi: adayın paketi yok; yalnız ilk tercihler açık, tam liste Tercih Dönemi Paketi ile (299 TL, /paket) açılır.";
async function main() {
// Ortam ÖNCE: appdb modülü import anında bağlanır
const dbYolu = evalAppDbHazirla();
const sqlite = new Database(dbYolu);
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
const { sistemPromptu } = await import("@/lib/ai/danisman-prompt");
const { sistemPromptu, sohbetListeOzeti } = await import("@/lib/ai/danisman-prompt");
const { modelPenceresi } = await import("@/lib/ai/sohbet-gecmisi");
const { listeOzetiCikar } = await import("@/lib/ai/rapor");
const { sohbetModeli, saglayiciAdi, anahtarVar } = await import("@/lib/ai/client");
const { JEV_DUZELTME_NOTU } = await import("@/lib/ai/jev");
const { ACIK_SATIR } = await import("@/lib/rapor-maske");
const { havuzOlustur, dilimHedefiHesapla } = await import("@/lib/rapor-havuzu");
const { PUAN_TURLERI } = await import("@/types/yokatlas");
@@ -76,6 +78,7 @@ async function main() {
const limit = Number(argOku("limit")) || Infinity;
const eszaman = Number(argOku("eszaman")) || 5;
const butce = Number(argOku("butce")) || 1.5;
const tekrar = Math.max(1, Math.floor(Number(argOku("tekrar")) || 1));
const sorular = SORULAR.filter(
(s) =>
(!etiketS || s.etiketler.some((e) => etiketS.includes(e))) &&
@@ -129,28 +132,27 @@ async function main() {
const profilEkle = sqlite.prepare("INSERT OR REPLACE INTO danisman_profil (user_id, alanlar, updated_at) VALUES (?, ?, ?)");
const profilOku = sqlite.prepare("SELECT alanlar FROM danisman_profil WHERE user_id = ?");
console.log(`\n== Sohbet eval — ${sorular.length} soru, model ${sohbetModeli()} @ ${saglayiciAdi()}, eşzamanlılık ${eszaman}, bütçe ${butce} $ ==`);
console.log(`\n== Sohbet eval — ${sorular.length} soru${tekrar > 1 ? ` × ${tekrar} tekrar` : ""}, model ${sohbetModeli()} @ ${saglayiciAdi()}, eşzamanlılık ${eszaman}, bütçe ${butce} $ ==`);
console.log(`Liste bağlamı: ${robotYolu && robotListeleri.size ? `${robotListeleri.size} profil ${robotYolu.split("/").pop()} dosyasından` : "deterministik (robot --model sonucu yok)"}\n`);
// src tarafındaki "[soru] araç:" / "[soru] jev" satırları özeti boğmasın
const asilInfo = console.info;
console.info = () => {};
async function soruKos(s: EvalSoru) {
async function soruKos({ s, tekrarNo }: { s: EvalSoru; tekrarNo: number }) {
const p = PROFILLER.find((x) => x.id === s.profilId);
if (!p) throw new Error(`Profil yok: ${s.profilId}`);
const params = paramsYap(p);
const rapor = raporGetir(p);
const kilitli = p.paketli === false;
const tamOzet = listeOzetiCikar(rapor, params.sira);
// route.ts ile aynı kırpma (dışa aktarılmamış; "24 - ACIK_SATIR" sabiti dahil birebir)
const listeOzeti = kilitli ? [...tamOzet.split("\n").slice(0, ACIK_SATIR), `… kalan ${24 - ACIK_SATIR} tercih kilitli.`].join("\n") : tamOzet;
const listeOzeti = sohbetListeOzeti(tamOzet, kilitli);
const profil = { ...(p.danismanProfili ?? {}) };
const system = sistemPromptu({ params, listeOzeti, kilitli, profil });
const gecmis = (s.gecmis ?? []).map((m) => ({ role: m.rol, content: m.icerik }));
const mesajlar = modelPenceresi(gecmis, s.soru);
const userId = `eval-${process.pid}-${s.id}`;
const userId = `eval-${process.pid}-${s.id}-${tekrarNo}`;
kullaniciEkle.run(userId, `${userId}@eval.invalid`, now, now);
if (p.danismanProfili) profilEkle.run(userId, JSON.stringify(p.danismanProfili), now);
@@ -263,7 +265,9 @@ async function main() {
const state: JevDanismanState = {
metin,
arac_ciktisi: aracCiktisi.length ? aracCiktisi : "yok",
liste: listeOzeti.split("\n"),
// Kilitli senaryoda prompt'taki ürün bilgisi de bağlamdır; yoksa hakem
// "299 TL" ve "/paket"i dayanaksız veri sayıyor (taban çizgisi: s075, s106).
liste: [...listeOzeti.split("\n"), ...(kilitli ? [KILITLI_URUN_BILGISI] : [])],
profil: { sira: params.sira, puan_turu: PUAN_TURLERI[params.tur], il },
};
jev = await jevSor(state, jevSorulari);
@@ -275,10 +279,11 @@ async function main() {
const karar: Karar = genelKarar(kontroller);
const sorunlar = kontroller.filter((k) => k.durum !== "gecti");
asilInfo(` ${karar.padEnd(6)} ${s.id} [${s.etiketler.join(",")}] ${(toplamMs / 1000).toFixed(1)} sn, ${adim} adım, araç: ${cagrilan.join("+") || "—"}${sorunlar.length ? ` ⇒ ${sorunlar.map((k) => `${k.ad}${k.neden ? `(${kisalt(k.neden, 90)})` : ""}`).join("; ")}` : ""}`);
asilInfo(` ${karar.padEnd(6)} ${s.id}${tekrar > 1 ? `#${tekrarNo + 1}` : ""} [${s.etiketler.join(",")}] ${(toplamMs / 1000).toFixed(1)} sn, ${adim} adım, araç: ${cagrilan.join("+") || "—"}${sorunlar.length ? ` ⇒ ${sorunlar.map((k) => `${k.ad}${k.neden ? `(${kisalt(k.neden, 90)})` : ""}`).join("; ")}` : ""}`);
return {
id: s.id,
tekrarNo,
profilId: s.profilId,
etiketler: s.etiketler,
soru: s.soru,
@@ -303,7 +308,8 @@ async function main() {
};
}
const sonuclar = (await havuzdaKos(sorular, eszaman, soruKos, () => TOPLAM.maliyet >= butce)).filter((x) => x != null);
const isler = sorular.flatMap((s) => Array.from({ length: tekrar }, (_, tekrarNo) => ({ s, tekrarNo })));
const sonuclar = (await havuzdaKos(isler, eszaman, soruKos, () => TOPLAM.maliyet >= butce)).filter((x) => x != null);
console.info = asilInfo;
// ── özet ──
@@ -326,6 +332,22 @@ async function main() {
const o = (kontrolBasina[k.ad] ??= { kaldi: 0, gri: 0, atlandi: 0 });
if (k.durum !== "gecti") o[k.durum]++;
}
// Tekrar tutarlılığı: soru başına karar dağılımı (model stokastik)
const soruBasina = new Map<string, { etiketler: string[]; gecti: number; gri: number; kaldi: number }>();
for (const x of sonuclar) {
const o = soruBasina.get(x.id) ?? { etiketler: x.etiketler, gecti: 0, gri: 0, kaldi: 0 };
if (x.karar === "gecti" || x.karar === "gri" || x.karar === "kaldi") o[x.karar]++;
soruBasina.set(x.id, o);
}
const tutarlilik =
tekrar > 1
? {
tekrar,
hepGecti: [...soruBasina].filter(([, o]) => o.gri + o.kaldi === 0).map(([id]) => id),
hicGecmedi: [...soruBasina].filter(([, o]) => o.gecti === 0).map(([id]) => id),
karisik: Object.fromEntries([...soruBasina].filter(([, o]) => o.gecti > 0 && o.gri + o.kaldi > 0).map(([id, o]) => [id, `${o.gecti} geçti / ${o.gri} gri / ${o.kaldi} kaldı`])),
}
: null;
const sure = sonuclar.map((x) => x.toplamMs);
const ilk = sonuclar.map((x) => x.ilkTokenMs).filter((x): x is number => x != null);
const ozet = {
@@ -333,9 +355,13 @@ async function main() {
saglayici: saglayiciAdi(),
jevModel: JEV_EVAL_MODEL,
...oran(sonuclar),
atlananSoru: sorular.length - sonuclar.length,
atlananSoru: isler.length - sonuclar.length,
canliJevNotuEklenen: sonuclar.filter((x) => x.canliJevNotu).length,
ortalamaAdim: sonuclar.length ? +(sonuclar.reduce((a, x) => a + x.adim, 0) / sonuclar.length).toFixed(2) : 0,
ortalamaAracCagrisi: sonuclar.length ? +(sonuclar.reduce((a, x) => a + x.araclar.length, 0) / sonuclar.length).toFixed(2) : 0,
enCokAracCagrisi: Math.max(0, ...sonuclar.map((x) => x.araclar.length)),
soruBasiMaliyet: sonuclar.length ? +(TOPLAM.maliyet / sonuclar.length).toFixed(5) : 0,
tutarlilik,
gecikmeMs: { ilkToken: { p50: yuzdelik(ilk, 50), p95: yuzdelik(ilk, 95) }, toplam: { p50: yuzdelik(sure, 50), p95: yuzdelik(sure, 95), max: Math.max(0, ...sure) } },
harcama: { ...TOPLAM, jev: { ...JEV_TOPLAM, maliyet: jevMaliyeti() } },
etiketBasina,
@@ -348,7 +374,12 @@ async function main() {
console.log(`\n${tablo(["etiket", "n", "geçti", "gri", "kaldı", "geçme"], satir(etiketBasina))}`);
console.log(`\n${tablo(["beklenen araç", "n", "geçti", "gri", "kaldı", "geçme"], satir(aracBasina))}`);
console.log(`\n${tablo(["kontrol", "kaldı", "gri", "atlandı"], Object.entries(kontrolBasina).sort((a, b) => b[1].kaldi - a[1].kaldi).map(([a, v]) => [a, v.kaldi, v.gri, v.atlandi]))}`);
console.log(`\nGecikme: ilk token p50 ${ozet.gecikmeMs.ilkToken.p50} ms / p95 ${ozet.gecikmeMs.ilkToken.p95} ms · toplam p50 ${ozet.gecikmeMs.toplam.p50} ms / p95 ${ozet.gecikmeMs.toplam.p95} ms · ortalama ${ozet.ortalamaAdim} adım`);
if (tutarlilik) {
console.log(`\nTutarlılık (${tekrar} tekrar): ${tutarlilik.hepGecti.length} soru hep geçti · ${Object.keys(tutarlilik.karisik).length} karışık · ${tutarlilik.hicGecmedi.length} hiç geçmedi`);
for (const [id, o] of Object.entries(tutarlilik.karisik)) console.log(` karışık ${id}: ${o}`);
if (tutarlilik.hicGecmedi.length) console.log(` hiç geçmedi: ${tutarlilik.hicGecmedi.join(", ")}`);
}
console.log(`\nGecikme: ilk token p50 ${ozet.gecikmeMs.ilkToken.p50} ms / p95 ${ozet.gecikmeMs.ilkToken.p95} ms · toplam p50 ${ozet.gecikmeMs.toplam.p50} ms / p95 ${ozet.gecikmeMs.toplam.p95} ms · ortalama ${ozet.ortalamaAdim} adım, ${ozet.ortalamaAracCagrisi} araç çağrısı (en çok ${ozet.enCokAracCagrisi})`);
console.log(`Harcama: ${TOPLAM.cagri} model çağrısı, ${TOPLAM.girdiToken} girdi + ${TOPLAM.ciktiToken} çıktı token, ${TOPLAM.maliyet.toFixed(4)} $${TOPLAM.maliyetBilinmeyenCagri ? ` (+${TOPLAM.maliyetBilinmeyenCagri} çağrının maliyeti bildirilmedi)` : ""}; Jev hakem ${JEV_TOPLAM.cagri} çağrı ≈ ${jevMaliyeti().toFixed(4)} $ (${JEV_TOPLAM.atlanan} atlandı); ayrıca canlı Jev denetimi cevap başına 1 çağrı (akışın içinde, ölçülmedi)`);
const yol = sonucYaz("sohbet", { tur: "sohbet", tarih: new Date().toISOString(), git: gitDurumu(), argv: process.argv.slice(2), listeDosyasi: robotYolu?.split("/").pop() ?? null, ozet, sorular: sonuclar });