'use strict'; /** * 智能问数 · 已验证打法缓存(replay)与 few-shot 检索 * ---------------------------------------------------------------------------- * 业界实证对查询准确率/成本最有效的两件事: * · 打法缓存 —— 成功运行(数字全部溯源、工具全成)的查询序列沉淀为语料; * 相似问命中后**直接重跑既有查询序列**(缓存的是打法不是数字,数据按当前湖重算), * 省掉多轮规划,只保留一次结论收敛调用。 * · few-shot 检索 —— 未达复用阈值但相近的问题,把语料里的打法作为参考注入 prompt。 * 相似度:字符二元组 Dice 系数(零依赖,问句短文本足够)。 * 语料文件:data/agent//corpus.json(cap 50,同题去重保留最新)。 */ const fs = require('fs'); const path = require('path'); /** 打法缓存只收只读取数类工具(不含体检类——体检结果不构成「打法」) */ const ALLOW_TOOLS = new Set(['query_report', 'compare_periods', 'peer_benchmark', 'cross_entity', 'build_report']); const MAX_ITEMS = 50; function normalize(q) { return String(q || '').toLowerCase().replace(/[\s,。?!、:;"'()()?!,.:;~~—\-_/\\]/g, ''); } function bigrams(s) { const out = new Set(); for (let i = 0; i < s.length - 1; i++) out.add(s.slice(i, i + 2)); if (s.length === 1) out.add(s); return out; } /** 字符二元组 Dice 系数:0(完全不同)~ 1(相同) */ function similarity(a, b) { const x = normalize(a); const y = normalize(b); if (!x || !y) return 0; if (x === y) return 1; const bx = bigrams(x); const by = bigrams(y); let inter = 0; for (const g of bx) if (by.has(g)) inter += 1; return (2 * inter) / (bx.size + by.size); } function loadCorpus(file) { try { return JSON.parse(fs.readFileSync(file, 'utf8')).items || []; } catch { return []; } } function saveCorpus(file, items) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, JSON.stringify({ items }), 'utf8'); fs.renameSync(temp, file); } /** * 沉淀一条已验证打法(同题去重保留最新)。 * steps = 运行里成功且在白名单内的 {tool, args}(args 已是钳制后的参数)。 */ function record(file, { question, steps }) { const q = String(question || '').trim(); const cleanSteps = (Array.isArray(steps) ? steps : []) .filter((s) => s && ALLOW_TOOLS.has(s.tool) && s.args && typeof s.args === 'object') .map((s) => ({ tool: s.tool, args: s.args })); if (!q || !cleanSteps.length) return false; const items = loadCorpus(file); const norm = normalize(q); const filtered = items.filter((it) => it.norm !== norm); filtered.unshift({ norm, question: q, steps: cleanSteps, at: new Date().toISOString() }); saveCorpus(file, filtered.slice(0, MAX_ITEMS)); return true; } /** 相似问命中:返回可重跑的打法(含原问题),不达阈值返回 null */ function findPlan(file, question, threshold = 0.82) { const items = loadCorpus(file); let best = null; let bestSim = 0; for (const it of items) { const sim = similarity(question, it.question); if (sim > bestSim) { bestSim = sim; best = it; } } if (!best || bestSim < threshold) return null; return { question: best.question, steps: best.steps, similarity: +bestSim.toFixed(3) }; } /** few-shot 检索:相近问题的打法参考(不含自身完全命中——那走 findPlan) */ function fewShot(file, question, k = 2, min = 0.6) { return loadCorpus(file) .map((it) => ({ it, sim: similarity(question, it.question) })) .filter((x) => x.sim >= min && x.sim < 0.999) .sort((a, b) => b.sim - a.sim) .slice(0, k) .map((x) => ({ question: x.it.question, steps: x.it.steps, similarity: +x.sim.toFixed(3) })); } module.exports = { ALLOW_TOOLS, normalize, similarity, record, findPlan, fewShot, MAX_ITEMS };