evalRetrieval.mjs 5.3 KB

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