provenance.js 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. 'use strict';
  2. /**
  3. * 智能问数智能体 · 数字溯源 post-check
  4. * ----------------------------------------------------------------------------
  5. * final 返回后的零 token 规则校验(turn-assessment 同哲学):从回答文本抽取数值,
  6. * 逐一在本次运行工具结果的数值集合中做容差匹配;匹配不到的记为「未溯源」并随
  7. * 运行留痕展示(警示不阻断)。换算形态自动覆盖:百分比显示值(8.21 vs 0.0821)、
  8. * 万元/亿元换算(4.2万 vs 42000)。日期、TopN、题干序号等模式先行剔除降噪。
  9. */
  10. function stripNoise(text) {
  11. return String(text || '')
  12. .replace(/\d{4}-\d{1,2}-\d{1,2}/g, ' ') // 2026-10-01
  13. .replace(/\b\d{8}\b/g, ' ') // 20261001
  14. .replace(/top\s*\d+/gi, ' ') // Top5
  15. .replace(/turn\s*\d+/gi, ' ') // Turn 2
  16. .replace(/v\d+/gi, ' '); // verdict 引用 v3
  17. }
  18. function extractNumbers(text) {
  19. const out = [];
  20. const re = /-?\d+(?:,\d{3})*(?:\.\d+)?/g;
  21. const cleaned = stripNoise(text);
  22. let m;
  23. while ((m = re.exec(cleaned))) {
  24. const n = Number(m[0].replace(/,/g, ''));
  25. if (Number.isFinite(n)) out.push(n);
  26. }
  27. return out;
  28. }
  29. /** 递归收集工具产物里的数值(深度/广度有界,防大产物拖慢) */
  30. function collectFromData(data, into, depth = 0) {
  31. if (data == null || typeof data !== 'object' || depth > 4) return;
  32. if (Array.isArray(data)) {
  33. for (const item of data.slice(0, 60)) collectFromData(item, into, depth + 1);
  34. return;
  35. }
  36. for (const v of Object.values(data)) {
  37. if (typeof v === 'number' && Number.isFinite(v)) into.push(v);
  38. else if (v && typeof v === 'object') collectFromData(v, into, depth + 1);
  39. }
  40. }
  41. function collectCandidates(steps) {
  42. const cands = [];
  43. for (const step of steps || []) {
  44. if (step.type !== 'tool' || !step.ok) continue;
  45. collectFromData(step.data, cands);
  46. for (const n of extractNumbers(step.summary || '')) cands.push(n);
  47. }
  48. return cands;
  49. }
  50. function matches(cands, n) {
  51. return cands.some((c) => {
  52. if (Math.abs(c - n) <= Math.max(Math.abs(c) * 0.005, 0.01)) return true;
  53. // 展示换算:百分比(×100)、万元(×1e4)、亿元(×1e8),以及反向
  54. for (const k of [100, 1e4, 1e8]) {
  55. if (Math.abs(c - n * k) <= Math.max(Math.abs(c) * 0.005, 0.01)) return true;
  56. if (Math.abs(c * k - n) <= Math.max(Math.abs(n) * 0.005, 0.01)) return true;
  57. }
  58. return false;
  59. });
  60. }
  61. /**
  62. * @returns {{total:number, untraced:number[], ratio:number}}
  63. */
  64. function checkProvenance(answer, steps) {
  65. const nums = extractNumbers(answer);
  66. if (!nums.length) return { total: 0, untraced: [], ratio: 1 };
  67. const cands = collectCandidates(steps);
  68. if (!cands.length) return { total: nums.length, untraced: nums.slice(0, 12), ratio: 0 };
  69. const untraced = [];
  70. for (const n of nums) {
  71. if (!matches(cands, n)) untraced.push(n);
  72. }
  73. return { total: nums.length, untraced: untraced.slice(0, 12), ratio: nums.length ? 1 - untraced.length / nums.length : 1 };
  74. }
  75. module.exports = { extractNumbers, collectCandidates, checkProvenance };