'use strict'; /** * 智能问数智能体 · 数字溯源 post-check * ---------------------------------------------------------------------------- * final 返回后的零 token 规则校验(turn-assessment 同哲学):从回答文本抽取数值, * 逐一在本次运行工具结果的数值集合中做容差匹配;匹配不到的记为「未溯源」并随 * 运行留痕展示(警示不阻断)。换算形态自动覆盖:百分比显示值(8.21 vs 0.0821)、 * 万元/亿元换算(4.2万 vs 42000)。日期、TopN、题干序号等模式先行剔除降噪。 */ function stripNoise(text) { return String(text || '') .replace(/\d{4}-\d{1,2}-\d{1,2}/g, ' ') // 2026-10-01 .replace(/\b\d{8}\b/g, ' ') // 20261001 .replace(/top\s*\d+/gi, ' ') // Top5 .replace(/turn\s*\d+/gi, ' ') // Turn 2 .replace(/v\d+/gi, ' '); // verdict 引用 v3 } function extractNumbers(text) { const out = []; const re = /-?\d+(?:,\d{3})*(?:\.\d+)?/g; const cleaned = stripNoise(text); let m; while ((m = re.exec(cleaned))) { const n = Number(m[0].replace(/,/g, '')); if (Number.isFinite(n)) out.push(n); } return out; } /** 递归收集工具产物里的数值(深度/广度有界,防大产物拖慢) */ function collectFromData(data, into, depth = 0) { if (data == null || typeof data !== 'object' || depth > 4) return; if (Array.isArray(data)) { for (const item of data.slice(0, 60)) collectFromData(item, into, depth + 1); return; } for (const v of Object.values(data)) { if (typeof v === 'number' && Number.isFinite(v)) into.push(v); else if (v && typeof v === 'object') collectFromData(v, into, depth + 1); } } function collectCandidates(steps) { const cands = []; for (const step of steps || []) { if (step.type !== 'tool' || !step.ok) continue; collectFromData(step.data, cands); for (const n of extractNumbers(step.summary || '')) cands.push(n); } return cands; } function matches(cands, n) { return cands.some((c) => { if (Math.abs(c - n) <= Math.max(Math.abs(c) * 0.005, 0.01)) return true; // 展示换算:百分比(×100)、万元(×1e4)、亿元(×1e8),以及反向 for (const k of [100, 1e4, 1e8]) { if (Math.abs(c - n * k) <= Math.max(Math.abs(c) * 0.005, 0.01)) return true; if (Math.abs(c * k - n) <= Math.max(Math.abs(n) * 0.005, 0.01)) return true; } return false; }); } /** * @returns {{total:number, untraced:number[], ratio:number}} */ function checkProvenance(answer, steps) { const nums = extractNumbers(answer); if (!nums.length) return { total: 0, untraced: [], ratio: 1 }; const cands = collectCandidates(steps); if (!cands.length) return { total: nums.length, untraced: nums.slice(0, 12), ratio: 0 }; const untraced = []; for (const n of nums) { if (!matches(cands, n)) untraced.push(n); } return { total: nums.length, untraced: untraced.slice(0, 12), ratio: nums.length ? 1 - untraced.length / nums.length : 1 }; } module.exports = { extractNumbers, collectCandidates, checkProvenance };