TypeScript 98.3%
CSS 0.9%
Shell 0.7%
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