fix(danisman): liste revizyonu koda bağlandı — model artık revizyon başlatamaz (QA B1)

Revizyon sohbet modelinin gönüllü liste_revize araç seçimine bağlıydı
(8 Eki eval: 8 istekte 5 çağrı, 3 sahte beyan; tool_choice required
OpenRouter'da 80-112 sn). Şimdi:

- Niyet kodda: revizyonNiyetiVar (regex) + paketli → lib/ai/revizyon-niyeti.ts
  yapılandırılmış çıkarım (yapilandirilmisUret, json_schema; eski liste_revize
  parametre şeması ve revizyonKuraliKur aynen yeniden kullanılır, kopya yok).
  Model niyetModeli() (OPENROUTER_NIYET_MODEL, yoksa rapor modeli), 20 sn,
  OpenInference bu çağrıda atlanır (11-12,5 sn ölçüldü).
- Dallanma kodda: kural geçerli → listeRevizeKoddan (paket/kilit/kredi kapıları
  aynı); kural eksik → soru formu koddan (modelin formu kod doğrular, yoksa
  varsayılan form); niyet soru/diger → normal sohbet; çıkarım başarısız →
  koddan "okuyamadım" cevabı + kredi iadesi (normal sohbete düşünce model
  uydurma adlarla "kurdum" yazıyordu).
- Cevap da kodda: revizyon/form turunda model hiç çağrılmaz; sayı ve program
  adları sunucu sonucundan şablonla (sabitCevap). Model sonuç önündeyken
  dağılımı 8'de 2 yanlış yazdı. Ekrana eski araç parçaları (tool-liste_revize /
  tool-soru_sor) aynen gider; istemci ve kayıt değişmez. Başarısız revizyon
  artık ekranda "kuramadım" hapı (output-error).
- liste_revize modelin araç listesinden çıktı; prompt revizyon bölümü
  "kurdum deme, net istek iste ya da soru_sor" diye sadeleşti.
- Güvenlik ağı: beyan regex'i kaldı; beyan var + revizyon yok → not + mesajın
  sohbet kredisi iade (grantCredits refund, refId=mesajId, idempotent), not
  bunu söyler. liste-revizyon iade refId'si "<mesajId>:revizyon" oldu:
  (reason, refId) tekilliği sohbet iadesiyle çakışıyordu.
- Geçmiş penceresi: "[Araç kaydı: …]" etiketi modelce kopyalanıyordu;
  yerine doğal tek cümle.
- Kaldırıldı: ilkAdimAracZorunlu / SOHBET_REVIZYON_ARAC_ZORUNLU.

Ölçüm (deepseek-v4-flash @ openrouter, pnpm eval:revizyon): koddan revizyon
8/8, kural doğru 8/8, sahte beyan 0, 10/10; revizyon turu 1 küçük çağrı
(ort. 1-3 sn, 0,00005 $) + liste üretimi; sohbet çağrısı yok.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-09 02:10:59 +03:00
parent 128c175327
commit cee4bf38af
10 changed files with 632 additions and 217 deletions

View File

@@ -11,10 +11,12 @@ import {
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
import { kullanicininRaporu, sohbetGecmisiGetir } from "@/features/rapor/rapor-queries";
import { barajTablosu, type AracBaglami } from "@/lib/ai/araclar";
import { danismanAkisi } from "@/lib/ai/sohbet-akisi";
import { barajTablosu, listeRevizeKoddan, soruFormuSonucu, type AracBaglami } from "@/lib/ai/araclar";
import { danismanAkisi, type KoddanIslem } from "@/lib/ai/sohbet-akisi";
import { sistemPromptu, sohbetListeOzeti } from "@/lib/ai/danisman-prompt";
import { kayitliMesajModelMetni, modelPenceresi, onayMesajiMi, revizyonNiyetiVar } from "@/lib/ai/sohbet-gecmisi";
import { revizyonNiyetiCikar } from "@/lib/ai/revizyon-niyeti";
import { kuralOzeti } from "@/lib/revizyon-kurali";
import {
anketCevabiDogrula,
anketCevabiMetni,
@@ -282,22 +284,55 @@ export async function POST(request: NextRequest) {
createdAt: new Date(),
});
// Liste revizyonu KODA BAĞLI (QA B1, 9 Eki 2026): adayın mesajı liste
// değişikliği istiyorsa (regex) ve aday paketliyse niyet ve kural küçük bir
// yapılandırılmış çağrıyla çıkarılır (lib/ai/revizyon-niyeti.ts); işlem ve
// cevap koddan, sohbet modeli o turda çağrılmaz. Paketsizde çağrı
// yok: liste değiştirilemez, model /paket'e yönlendirir. Çıkarım adayın
// kredisine dokunmaz; başarısızsa normal sohbet (model listeyi zaten
// değiştiremez; "kurdum" derse güvenlik ağı not ekler ve kredi iade edilir).
let islem: KoddanIslem | undefined;
if (paketli && revizyonNiyetiVar(mesaj)) {
const t0 = Date.now();
const niyet = await revizyonNiyetiCikar({ mesaj, params: raporParams, oncekiForm: anketCevabiHam ? sonAnket : null });
console.info("[soru] revizyon niyeti:", niyet.niyet, "kural" in niyet ? "kural" : "form" in niyet ? `form (eksik: ${niyet.eksik.join(", ") || "—"})` : "neden" in niyet ? niyet.neden : "", `${Date.now() - t0} ms`);
if (niyet.niyet === "revizyon" && "kural" in niyet) {
const kural = niyet.kural;
islem = {
ad: "liste_revize",
girdi: kural,
calistir: () => listeRevizeKoddan(aracBaglami, kural),
anlatim: { ozet: kuralOzeti(kural), ilkN: Boolean(kural.ilkTercihler) },
};
} else if (niyet.niyet === "revizyon") {
const sonuc = soruFormuSonucu(niyet.form);
islem = { ad: "soru_sor", girdi: {}, calistir: async () => sonuc };
} else if (niyet.niyet === "belirsiz") {
// Çıkarım yapılamadı (zaman aşımı / sağlayıcı hatası). Normal sohbete
// düşülmez: 9 Eki eval r04'te model bu durumda uydurma adlarla
// "yeniden kurdum" yazdı (güvenlik ağı yakaladı). Cevap koddan, kredi
// iade (bedelsiz mesajda zaten düşmedi); aday tekrar yazar.
const iade = bedelsiz
? false
: (await grantCredits({ userId: session.user.id, delta: 1, reason: "refund", refId: kullaniciMesajId }).catch(() => ({ granted: false }))).granted;
islem = {
ad: "liste_revize",
girdi: {},
calistir: async () => ({ hata: `isteğini şu an okuyamadım (geçici bir sorun). Birazdan aynı isteği tekrar yazar mısın?${iade ? " Bu mesajın kredisi iade edildi." : ""}` }),
};
}
}
// Cevap AI SDK'nın UI mesaj akışıyla gider (lib/ai/sohbet-akisi.ts;
// istemci: useChat). Kayıt ve kredi iadesi burada kalır.
const stream = danismanAkisi({
system,
mesajlar,
aracBaglami,
islem,
// Cevaptaki /rehber/<slug> adresleri gerçek yazı listesiyle doğrulanır
// (prod'da cache'li, lib/rehber.ts)
rehberler: getRehberAramaKayitlari(),
// Liste değişikliği isteğinde ilk adım araçsız bitemez (QA B1). VARSAYILAN
// KAPALI: tool_choice "required" OpenRouter'da isteği Baidu yerine
// OpenInference'a yönlendirdi; cevap 80-112 sn sürdü ve onlarca paralel
// araç çağrısı geldi (8 Eki 2026 eval, scripts/eval/revizyon-arac.ts).
// Açmak Bilal'in kararı (SOHBET_REVIZYON_ARAC_ZORUNLU=1); paketsizde hiç
// zorlanmaz — liste_revize zaten paket ister, model sözle yönlendirir.
ilkAdimAracZorunlu: process.env.SOHBET_REVIZYON_ARAC_ZORUNLU === "1" && paketli && revizyonNiyetiVar(mesaj),
onAracSonucu: (ad, cikti) => {
const c = cikti as Record<string, unknown>;
if (ad === "soru_sor" && c.anket) cevapMeta.anket = c.anket as SohbetMeta["anket"];
@@ -307,6 +342,15 @@ export async function POST(request: NextRequest) {
cevapMeta.revizyon = ozet as RevizyonOzeti;
}
},
// Güvenlik ağı: model "yeniden kurdum" dedi ama bu turda revizyon yok →
// mesajın sohbet kredisi iade edilir (güvenlik-uyum önerisi, 7 Eki 2026).
// Aynı refId ile idempotent: yarım kalma iadesiyle çakışmaz (ikisi aynı
// turda olamaz), ağ tekrarında ikinci kez düşmez. Bedelsiz mesajda yok.
onRevizeBeyani: async () => {
if (bedelsiz) return false;
const r = await grantCredits({ userId, delta: 1, reason: "refund", refId: kullaniciMesajId });
return r.granted;
},
onTamam: async (metin) => {
await cevabiKaydet(metin);
},