evalRetrieval.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { config } from "#config/index.js";
  2. import { searchDocs } from "#services/searchService.js";
  3. import { rewriteQuery, generateHydeDocument } from "#chat/chatChain.js";
  4. function parseArgs(argv) {
  5. const flags = { hyde: false, rewrite: false, minScore: 0, topK: 5, label: "" };
  6. for (const arg of argv) {
  7. if (arg === "--hyde") flags.hyde = true;
  8. else if (arg === "--no-hyde") flags.hyde = false;
  9. else if (arg === "--rewrite") flags.rewrite = true;
  10. else if (arg === "--no-rewrite") flags.rewrite = false;
  11. else if (arg.startsWith("--min-score=")) flags.minScore = Number(arg.split("=")[1]);
  12. else if (arg.startsWith("--top-k=")) flags.topK = Number(arg.split("=")[1]);
  13. else if (arg.startsWith("--label=")) flags.label = arg.split("=").slice(1).join("=");
  14. }
  15. return flags;
  16. }
  17. const CASES = [
  18. { query: "como ver os usuários conectados no equipamento", expectedSourceIncludes: "Huawei", type: "positive" },
  19. { query: "comando para exibir usuários de acesso", expectedSourceIncludes: "Huawei", type: "positive" },
  20. { query: "como configurar Eth-Trunk", expectedSourceIncludes: "Huawei", type: "positive" },
  21. { query: "como configurar OSPF no equipamento", expectedSourceIncludes: "Huawei", type: "positive" },
  22. { query: "comando display access-user", expectedSourceIncludes: "Huawei", type: "positive" },
  23. { query: "como resetar o roteador TP-Link", expectedSourceIncludes: "TL-WR940N", type: "positive" },
  24. { query: "como instalar o roteador TP-Link pela primeira vez", expectedSourceIncludes: "TL-WR940N", type: "positive" },
  25. { query: "qual o endereço padrão de acesso ao roteador", expectedSourceIncludes: "TL-WR940N", type: "positive" },
  26. { query: "qual o valor do vale refeição da empresa", expectedSourceIncludes: null, type: "negative" },
  27. { query: "quantos dias de férias tenho direito por ano", expectedSourceIncludes: null, type: "negative" }
  28. ];
  29. function sourceMatches(hit, expectedIncludes) {
  30. if (!expectedIncludes) return false;
  31. return String(hit?.source ?? "").toLowerCase().includes(expectedIncludes.toLowerCase());
  32. }
  33. async function resolveEmbeddingQuery(originalQuery, flags) {
  34. const searchQuery = flags.rewrite ? await rewriteQuery(originalQuery, []) : originalQuery;
  35. if (!flags.hyde) return { searchQuery, embeddingQuery: searchQuery, embedRole: "query" };
  36. const hydeText = await generateHydeDocument(searchQuery);
  37. return hydeText
  38. ? { searchQuery, embeddingQuery: hydeText, embedRole: "passage" }
  39. : { searchQuery, embeddingQuery: searchQuery, embedRole: "query" };
  40. }
  41. async function runCase(testCase, flags) {
  42. const { searchQuery, embeddingQuery, embedRole } = await resolveEmbeddingQuery(testCase.query, flags);
  43. const hits = await searchDocs({
  44. query: embeddingQuery,
  45. topK: flags.topK,
  46. embedRole,
  47. minScore: flags.minScore
  48. });
  49. const top1 = hits[0] ?? null;
  50. const top3 = hits.slice(0, 3);
  51. const top1Match = testCase.type === "positive" ? sourceMatches(top1, testCase.expectedSourceIncludes) : null;
  52. const top3Match = testCase.type === "positive" ? top3.some((h) => sourceMatches(h, testCase.expectedSourceIncludes)) : null;
  53. return {
  54. query: testCase.query,
  55. searchQuery: searchQuery !== testCase.query ? searchQuery : "",
  56. type: testCase.type,
  57. top1_score: top1 ? top1.score.toFixed(4) : "-",
  58. top1_source: top1?.source ?? "-",
  59. top1_match: testCase.type === "positive" ? (top1Match ? "OK" : "MISS") : "-",
  60. top3_match: testCase.type === "positive" ? (top3Match ? "OK" : "MISS") : "-"
  61. };
  62. }
  63. async function main() {
  64. const flags = parseArgs(process.argv.slice(2));
  65. console.log(`\n=== evalRetrieval ${flags.label ? `[${flags.label}] ` : ""}===`);
  66. console.log(
  67. `modelo=${config.ollama.embeddingsModel} minScore=${flags.minScore} topK=${flags.topK} ` +
  68. `rewrite=${flags.rewrite} hyde=${flags.hyde} colecao=${config.qdrant.collection}\n`
  69. );
  70. const rows = [];
  71. for (const testCase of CASES) {
  72. rows.push(await runCase(testCase, flags));
  73. }
  74. console.table(rows);
  75. const positives = rows.filter((r) => r.type === "positive");
  76. const negatives = rows.filter((r) => r.type === "negative");
  77. const positiveScores = positives.map((r) => Number(r.top1_score)).filter((n) => !Number.isNaN(n));
  78. const negativeScores = negatives.map((r) => Number(r.top1_score)).filter((n) => !Number.isNaN(n));
  79. const avg = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : NaN);
  80. const hitRate = (arr, key) => (arr.length ? arr.filter((r) => r[key] === "OK").length / arr.length : NaN);
  81. console.log("\n--- resumo ---");
  82. console.log(`positivos: score médio top-1 = ${avg(positiveScores).toFixed(4)} | hit-rate top-1 = ${(hitRate(positives, "top1_match") * 100).toFixed(0)}% | hit-rate top-3 = ${(hitRate(positives, "top3_match") * 100).toFixed(0)}%`);
  83. console.log(`positivos: score mínimo top-1 (piso de segurança) = ${positiveScores.length ? Math.min(...positiveScores).toFixed(4) : "-"}`);
  84. console.log(`negativos: score máximo top-1 (piso de ruído) = ${negativeScores.length ? Math.max(...negativeScores).toFixed(4) : "-"}`);
  85. console.log("");
  86. }
  87. main()
  88. .then(() => process.exit(0))
  89. .catch((err) => {
  90. console.error("[evalRetrieval] erro:", err);
  91. process.exit(1);
  92. });