evalRetrieval.mjs 5.1 KB

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