replay.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. 'use strict';
  2. /**
  3. * 智能问数 · 已验证打法缓存(replay)与 few-shot 检索
  4. * ----------------------------------------------------------------------------
  5. * 业界实证对查询准确率/成本最有效的两件事:
  6. * · 打法缓存 —— 成功运行(数字全部溯源、工具全成)的查询序列沉淀为语料;
  7. * 相似问命中后**直接重跑既有查询序列**(缓存的是打法不是数字,数据按当前湖重算),
  8. * 省掉多轮规划,只保留一次结论收敛调用。
  9. * · few-shot 检索 —— 未达复用阈值但相近的问题,把语料里的打法作为参考注入 prompt。
  10. * 相似度:字符二元组 Dice 系数(零依赖,问句短文本足够)。
  11. * 语料文件:data/agent/<entity>/corpus.json(cap 50,同题去重保留最新)。
  12. */
  13. const fs = require('fs');
  14. const path = require('path');
  15. /** 打法缓存只收只读取数类工具(不含体检类——体检结果不构成「打法」) */
  16. const ALLOW_TOOLS = new Set(['query_report', 'compare_periods', 'peer_benchmark', 'cross_entity', 'build_report']);
  17. const MAX_ITEMS = 50;
  18. function normalize(q) {
  19. return String(q || '').toLowerCase().replace(/[\s,。?!、:;"'()()?!,.:;~~—\-_/\\]/g, '');
  20. }
  21. function bigrams(s) {
  22. const out = new Set();
  23. for (let i = 0; i < s.length - 1; i++) out.add(s.slice(i, i + 2));
  24. if (s.length === 1) out.add(s);
  25. return out;
  26. }
  27. /** 字符二元组 Dice 系数:0(完全不同)~ 1(相同) */
  28. function similarity(a, b) {
  29. const x = normalize(a);
  30. const y = normalize(b);
  31. if (!x || !y) return 0;
  32. if (x === y) return 1;
  33. const bx = bigrams(x);
  34. const by = bigrams(y);
  35. let inter = 0;
  36. for (const g of bx) if (by.has(g)) inter += 1;
  37. return (2 * inter) / (bx.size + by.size);
  38. }
  39. function loadCorpus(file) {
  40. try { return JSON.parse(fs.readFileSync(file, 'utf8')).items || []; }
  41. catch { return []; }
  42. }
  43. function saveCorpus(file, items) {
  44. fs.mkdirSync(path.dirname(file), { recursive: true });
  45. const temp = `${file}.${process.pid}.tmp`;
  46. fs.writeFileSync(temp, JSON.stringify({ items }), 'utf8');
  47. fs.renameSync(temp, file);
  48. }
  49. /**
  50. * 沉淀一条已验证打法(同题去重保留最新)。
  51. * steps = 运行里成功且在白名单内的 {tool, args}(args 已是钳制后的参数)。
  52. */
  53. function record(file, { question, steps }) {
  54. const q = String(question || '').trim();
  55. const cleanSteps = (Array.isArray(steps) ? steps : [])
  56. .filter((s) => s && ALLOW_TOOLS.has(s.tool) && s.args && typeof s.args === 'object')
  57. .map((s) => ({ tool: s.tool, args: s.args }));
  58. if (!q || !cleanSteps.length) return false;
  59. const items = loadCorpus(file);
  60. const norm = normalize(q);
  61. const filtered = items.filter((it) => it.norm !== norm);
  62. filtered.unshift({ norm, question: q, steps: cleanSteps, at: new Date().toISOString() });
  63. saveCorpus(file, filtered.slice(0, MAX_ITEMS));
  64. return true;
  65. }
  66. /** 相似问命中:返回可重跑的打法(含原问题),不达阈值返回 null */
  67. function findPlan(file, question, threshold = 0.82) {
  68. const items = loadCorpus(file);
  69. let best = null;
  70. let bestSim = 0;
  71. for (const it of items) {
  72. const sim = similarity(question, it.question);
  73. if (sim > bestSim) { bestSim = sim; best = it; }
  74. }
  75. if (!best || bestSim < threshold) return null;
  76. return { question: best.question, steps: best.steps, similarity: +bestSim.toFixed(3) };
  77. }
  78. /** few-shot 检索:相近问题的打法参考(不含自身完全命中——那走 findPlan) */
  79. function fewShot(file, question, k = 2, min = 0.6) {
  80. return loadCorpus(file)
  81. .map((it) => ({ it, sim: similarity(question, it.question) }))
  82. .filter((x) => x.sim >= min && x.sim < 0.999)
  83. .sort((a, b) => b.sim - a.sim)
  84. .slice(0, k)
  85. .map((x) => ({ question: x.it.question, steps: x.it.steps, similarity: +x.sim.toFixed(3) }));
  86. }
  87. module.exports = { ALLOW_TOOLS, normalize, similarity, record, findPlan, fewShot, MAX_ITEMS };