guard.js 2.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. 'use strict';
  2. /**
  3. * 智能问数智能体 · 护栏
  4. * ----------------------------------------------------------------------------
  5. * 预算常量(轮次/工具调用/修复次数/墙钟,零配置起步)、图表规格钳制
  6. * (复用语义层白名单,防模型捏造指标 key)、循环侦测(重复调用先提醒后强制收敛,
  7. * 对齐 ai-fde runtime「回注重复提醒而非硬杀」的 Codex/Claude Code 同款思路)。
  8. */
  9. function clampChart(raw, svc) {
  10. if (!raw || typeof raw !== 'object') return null;
  11. const validKeys = new Set((svc.REPORT_COLUMNS || []).map((c) => c.key));
  12. const type = ['bar', 'line'].includes(raw.type) ? raw.type : null;
  13. if (!type) return null;
  14. const x = raw.x === 'date' ? 'date' : 'shopName';
  15. const y = (Array.isArray(raw.y) ? raw.y : []).map(String).filter((k) => validKeys.has(k)).slice(0, 2);
  16. return {
  17. type,
  18. x,
  19. y: y.length ? y : ['grossAmt'],
  20. title: String(raw.title || '').slice(0, 60),
  21. };
  22. }
  23. const BUDGET = {
  24. maxTurns: 10, // LLM 调用次数上限(含 final)
  25. maxToolCalls: 16, // 工具执行次数上限
  26. maxRepairs: 2, // 解析失败/非法动作的连续回注修复次数
  27. llmTimeoutMs: 45000, // 单次对话模型超时(与 nl-query 同量级)
  28. runTimeoutMs: 120000, // 单次运行墙钟
  29. };
  30. /** 循环侦测:规范化(tool + 排序后的参数)做指纹;同调用第 2 次提醒、第 3 次强制收敛 */
  31. class LoopDetector {
  32. constructor() { this.lastKey = null; this.repeatCount = 0; }
  33. static fingerprint(tool, args) {
  34. const norm = {};
  35. for (const k of Object.keys(args || {}).sort()) norm[k] = args[k];
  36. return tool + '|' + JSON.stringify(norm);
  37. }
  38. record(tool, args) {
  39. const key = LoopDetector.fingerprint(tool, args);
  40. if (key === this.lastKey) {
  41. this.repeatCount += 1;
  42. return this.repeatCount >= 2 ? 'loop' : 'repeat';
  43. }
  44. this.lastKey = key;
  45. this.repeatCount = 0;
  46. return 'ok';
  47. }
  48. }
  49. module.exports = { BUDGET, clampChart, LoopDetector };