SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
3.7 KB · 80 lines typescript
Raw Blame History
1// Évaluation RAG hors-ligne : rappel des sources attendues, refus corrects hors corpus,2// isolation stricte entre les cours. Produit evaluation/rag-evaluation.md.3import { readFileSync, writeFileSync } from "node:fs";4import { resolve } from "node:path";5import { hybridSearch, isConfidentEnough } from "../lib/rag/search.ts";67type TestSet = {8  course: string;9  questions: { q: string; expectDoc?: string; expectDocAny?: string[] }[];10  outOfScope: string[];11  isolationProbes?: { q: string; mustNotContainDoc: string; note: string }[];12};1314const sets: TestSet[] = ["imm1003-test-set.json", "imm1033-test-set.json"].map((f) =>15  JSON.parse(readFileSync(resolve(process.cwd(), "evaluation", f), "utf8"))16);1718const lines: string[] = [19  "# Évaluation RAG — Immbot AI",20  "",21  `Exécutée le ${new Date().toISOString().slice(0, 16).replace("T", " ")} (recherche hybride hors-ligne, sans appel LLM).`,22  "",23  "Critère de rappel : le document attendu figure dans les sources récupérées (top-k).",24  "",25];2627let totalRecall = 0, totalQuestions = 0, totalRefusals = 0, totalOOS = 0, isolationOK = 0, isolationTotal = 0;2829for (const set of sets) {30  const space = `official-${set.course.toLowerCase()}`;31  lines.push(`## ${set.course}`, "", "| Question | Attendu | Trouvé | Rappel |", "|---|---|---|---|");32  for (const q of set.questions) {33    const results = await hybridSearch({ query: q.q, spaces: [space], k: 10 });34    const files = [...new Set(results.map((r) => r.filename))];35    const expected = q.expectDoc ? [q.expectDoc] : (q.expectDocAny ?? []);36    const hit = expected.some((e) => files.includes(e));37    totalQuestions++;38    if (hit) totalRecall++;39    lines.push(40      `| ${q.q.slice(0, 70)} | ${expected.join(" ou ")} | ${files.slice(0, 3).join(", ")} | ${hit ? "✅" : "❌"} |`41    );42  }43  lines.push("", "### Refus hors corpus (mode « Cours uniquement »)", "");44  for (const q of set.outOfScope) {45    const results = await hybridSearch({ query: q, spaces: [space], k: 10 });46    const confident = isConfidentEnough(results) &&47      results[0].score > 2 / 61; // seuil : présence dans le top des DEUX recherches48    totalOOS++;49    if (!confident) totalRefusals++;50    lines.push(`- « ${q} » → ${confident ? "⚠ contexte jugé pertinent (à surveiller)" : "✅ refus (aucun contexte suffisant)"}`);51  }52  for (const probe of set.isolationProbes ?? []) {53    const results = await hybridSearch({ query: probe.q, spaces: [space], k: 10 });54    const leaked = results.some((r) => r.filename === probe.mustNotContainDoc || (r.course_code && r.course_code !== set.course));55    isolationTotal++;56    if (!leaked) isolationOK++;57    lines.push("", `### Isolation inter-cours`, "", `- ${probe.note} → ${leaked ? "❌ FUITE DÉTECTÉE" : "✅ aucun fragment de l'autre cours"}`);58  }59  lines.push("");60}6162const recallPct = ((totalRecall / totalQuestions) * 100).toFixed(0);63lines.push(64  "## Synthèse",65  "",66  `| Mesure | Résultat |`,67  `|---|---|`,68  `| Rappel des sources attendues | ${totalRecall}/${totalQuestions} (${recallPct} %) |`,69  `| Refus corrects hors corpus | ${totalRefusals}/${totalOOS} |`,70  `| Isolation inter-cours | ${isolationOK}/${isolationTotal} |`,71  "",72  "La validation stricte des citations (toute balise [Sx] hors contexte est neutralisée) est couverte par les tests unitaires (`tests/citations.test.ts`)."73);7475writeFileSync(resolve(process.cwd(), "evaluation/rag-evaluation.md"), lines.join("\n"));76console.log(lines.join("\n"));77const ok = totalRecall / totalQuestions >= 0.8 && isolationOK === isolationTotal;78console.log(ok ? "\n✅ Évaluation RAG : critères atteints" : "\n❌ Évaluation RAG : sous les critères (rappel ≥ 80 % exigé, isolation parfaite exigée)");79process.exit(ok ? 0 : 1);80