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