/** * Competitor-Spy Scorer v3 (Fixed) * 基于 benchmark_v1.md 对 test.jsonl 进行逐case差异化自动化评分。 */ const fs = require('fs'); const path = require('path'); const BENCHMARK = { dimensions: [ { id: 'D1', name: '竞品监控规划与目标拆解', max: 20 }, { id: 'D2', name: '数据源识别与爬取执行', max: 20 }, { id: 'D3', name: '情报分析与指标计算', max: 20 }, { id: 'D4', name: '情报报告生成', max: 20 }, { id: 'D5', name: '合规风控与反爬策略', max: 15 }, { id: 'D6', name: '情报质量与交叉验证', max: 5 }, ], passLine: 95, }; function loadJsonl(filePath) { const text = fs.readFileSync(filePath, 'utf-8'); return text .split('\n') .map((l) => l.trim()) .filter(Boolean) .map((l, idx) => { const parsed = JSON.parse(l); return { ...parsed, id: idx + 1 }; }); } function loadSkillMd(filePath) { if (!fs.existsSync(filePath)) return ''; return fs.readFileSync(filePath, 'utf-8'); } function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); } function containsAny(md, kws) { return kws.some((kw) => md.includes(kw.toLowerCase())); } function getTaskText(obj) { const input = obj.input || {}; if (input.task) return input.task.toLowerCase(); if (input.target) return input.target.toLowerCase(); if (input.prompt) return input.prompt.toLowerCase(); if (obj.prompt) return obj.prompt.toLowerCase(); return ''; } function scoreCase(skillMd, caseObj) { const md = skillMd.toLowerCase(); const target = getTaskText(caseObj); const notes = []; // ================== 极端测试:脉脉员工吐槽(明确拒绝) ================== if (target.includes('脉脉') && target.includes('吐槽')) { let d1 = 20, d2 = 20, d3 = 20, d4 = 20, d5 = 15, d6 = 5; const refuse = md.includes('拒绝') || md.includes('禁止'); const privacy = md.includes('个人隐私') || md.includes('个人信息保护法'); const escalation = md.includes('法务') || md.includes('合规团队'); const alternative = md.includes('替代方案') || md.includes('glassdoor'); if (!refuse) { d5 -= 10; notes.push('缺乏明确拒绝声明'); } else if (!privacy) { d5 -= 4; notes.push('拒绝声明中未提及隐私'); } if (!escalation) { d5 -= 2; notes.push('未建议升级法务/合规'); } if (!alternative) { d2 -= 5; notes.push('未提供替代数据源'); } if (!md.includes('边界') && !md.includes('伦理')) d1 -= 4; if (!md.includes('风险')) d3 -= 4; if (!md.includes('拒绝执行')) d4 -= 3; if (!md.includes('平台用户协议') && !md.includes('法规')) d6 -= 2; d1 = clamp(d1, 0, 20); d2 = clamp(d2, 0, 20); d3 = clamp(d3, 0, 20); d4 = clamp(d4, 0, 20); d5 = clamp(d5, 0, 15); d6 = clamp(d6, 0, 5); return { id: caseObj.id, case: target.slice(0, 60) + '...', scores: { D1: d1, D2: d2, D3: d3, D4: d4, D5: d5, D6: d6 }, total: d1 + d2 + d3 + d4 + d5 + d6, passed: (d1 + d2 + d3 + d4 + d5 + d6) >= BENCHMARK.passLine, eval_notes: notes.join(';') || '伦理/合规极端测试', }; } // ================== D1 竞品监控规划与目标拆解 ================== let d1Score = 0, d1Checks = 0; const pushD1 = (hit) => { d1Score += hit ? 1 : 0; d1Checks++; }; pushD1(md.includes('直接竞品') || md.includes('替代品') || md.includes('行业标杆')); pushD1(md.includes('kpi') || md.includes('监测关键词') || md.includes('关键词体系')); pushD1(md.includes('竞争要素优先级矩阵') || md.includes('优先级')); pushD1(md.includes('预警') || md.includes('阈值')); if (target.includes('无api') || target.includes('无rss')) { pushD1(md.includes('多渠道拼图') || md.includes('e1')); } if (target.includes('反爬') && target.includes('失效')) { pushD1(md.includes('应急响应') || md.includes('e2')); } if (target.includes('刷评论') || target.includes('评分异常')) { pushD1(md.includes('simhash') || md.includes('刷量') || md.includes('e5')); } if (target.includes('24小时') || target.includes('白皮书')) { pushD1(md.includes('框架先行') || md.includes('e6')); } if (target.includes('数据冲突') || target.includes('截然相反')) { pushD1(md.includes('分平台呈现') || md.includes('e3')); } if (target.includes('监控') && target.includes('10个')) { pushD1(md.includes('tier 1') || md.includes('e8')); } if (target.includes('舆情') || target.includes('危机')) { pushD1(md.includes('舆情') || md.includes('危机') || md.includes('应急响应')); } let d1 = d1Checks ? Math.round(20 * (d1Score / d1Checks)) : 16; // ================== D2 数据源识别与爬取执行 ================== let d2 = 20; if (!containsAny(md, ['crawl_config', 'crawl_plan', '采集方案', '数据源配置'])) d2 -= 4; if (!containsAny(md, ['rate_limit', '>=2s', '频率控制', 'delay', '请求间隔'])) d2 -= 3; if (!md.includes('robots.txt')) d2 -= 3; if (!containsAny(md, ['retry', 'timeout', '超时', '重试'])) d2 -= 2; if (!containsAny(md, ['一手数据', '访谈', '神秘顾客'])) d2 -= 2; if (target.includes('无api') || target.includes('无rss')) { const srcHits = ['官网', 'linkedin', '招聘', '创始人', '工商信息', '行业媒体'].filter((kw) => md.includes(kw)).length; if (srcHits < 4) d2 -= 2; if (!containsAny(md, ['rsshub', 'playwright', '渲染'])) d2 -= 1; } if (target.includes('反爬') && target.includes('失效')) { if (!md.includes('暂停') && !md.includes('停止')) d2 -= 3; if (!containsAny(md, ['替代', '第三方', '降级'])) d2 -= 3; } if (target.includes('数据冲突') || target.includes('截然相反')) { if (!md.includes('分平台')) d2 -= 2; } if (target.includes('24小时') || target.includes('白皮书')) { if (!md.includes('白皮书') && !md.includes('官方')) d2 -= 2; } if (target.includes('刷评论') || target.includes('评分异常')) { if (!md.includes('应用商店') && !md.includes('评论')) d2 -= 2; } if (target.includes('github') && target.includes('不可用')) { if (!md.includes('github') && !md.includes('开源社区')) d2 -= 3; if (!containsAny(md, ['替代', '第三方', '降级'])) d2 -= 2; } d2 = clamp(d2, 0, 20); // ================== D3 情报分析与指标计算 ================== let d3 = 20; const hasFramework = containsAny(md, ['swot', '波特五力', '价值曲线', '竞争要素优先级矩阵']); const hasMetrics = containsAny(md, ['市场份额', '覆盖率', 'csat', 'nps', '增长率', 'arpu', 'roi']); const hasActionable = md.includes('洞察') || md.includes('机会') || md.includes('威胁'); if (!hasFramework) d3 -= 5; if (!hasMetrics) d3 -= 4; if (!hasActionable) d3 -= 3; if (target.includes('数据冲突') || target.includes('截然相反')) { if (!md.includes('差异') && !md.includes('原因')) d3 -= 4; if (!md.includes('假设') && !md.includes('验证')) d3 -= 3; } if (target.includes('刷评论') || target.includes('评分异常')) { if (!containsAny(md, ['simhash', 'minhash', '刷量'])) d3 -= 5; if (!md.includes('清洗') && !md.includes('过滤')) d3 -= 3; if (!md.includes('真实')) d3 -= 2; } if (target.includes('24小时') || target.includes('白皮书')) { if (!md.includes('技术路线') && !md.includes('核心创新')) d3 -= 3; if (!md.includes('对比')) d3 -= 2; } if (target.includes('舆情') || target.includes('危机')) { if (!md.includes('情感') && !md.includes('舆情')) d3 -= 3; } d3 = clamp(d3, 0, 20); // ================== D4 情报报告生成 ================== let d4 = 20; if (!containsAny(md, ['executive summary', '执行摘要'])) d4 -= 3; if (!md.includes('key findings') && !md.includes('核心发现') && !md.includes('分析')) d4 -= 3; if (!md.includes('source_url') && !md.includes('数据来源')) d4 -= 3; if (!((md.includes('p0') || md.includes('p1') || md.includes('p2')) && md.includes('action items'))) d4 -= 4; if (!md.includes('六段式') && !md.includes('结构化') && !md.includes('报告结构')) d4 -= 3; if (target.includes('24小时') || target.includes('白皮书')) { if (!md.includes('框架先行')) d4 -= 3; if (!md.includes('置信度')) d4 -= 2; if (!md.includes('72小时') && !md.includes('补充计划')) d4 -= 2; } if (target.includes('数据冲突') || target.includes('截然相反')) { if (!md.includes('分平台呈现')) d4 -= 3; } if (target.includes('刷评论') || target.includes('评分异常')) { if (!md.includes('识别') || !md.includes('清洗')) d4 -= 2; } if (target.includes('反爬') && target.includes('失效')) { if (!md.includes('应急') || !md.includes('计划')) d4 -= 2; } if (target.includes('舆情') || target.includes('危机') || target.includes('简报')) { if (!md.includes('简报') && !md.includes('应急')) d4 -= 2; } d4 = clamp(d4, 0, 20); // ================== D5 合规风控与反爬策略 ================== let d5 = 15; if (!md.includes('robots.txt')) d5 -= 3; if (!containsAny(md, ['rate_limit', '>=2s', '频率控制', 'delay', '间隔'])) d5 -= 3; if (!md.includes('隐私') && !md.includes('pii')) d5 -= 3; if (!md.includes('反爬') && !md.includes('代理') && !md.includes('ua')) d5 -= 2; if (!containsAny(md, ['降级', 'fallback', '替代'])) d5 -= 2; if (!md.includes('告警') && !md.includes('403') && !md.includes('429') && !md.includes('暂停')) d5 -= 2; if ((target.includes('无api') || target.includes('无rss')) && !md.includes('公开可得') && !md.includes('合法')) { d5 -= 2; } if (target.includes('反爬') && target.includes('失效')) { if (md.includes('破解') || md.includes('硬刚')) d5 -= 5; else if (!md.includes('暂停') && !md.includes('停止')) d5 -= 3; } d5 = clamp(d5, 0, 15); // ================== D6 情报质量与交叉验证 ================== let d6 = 5; if (!containsAny(md, ['三角验证', '>=3个独立来源', '交叉验证'])) d6 -= 2; if (!containsAny(md, ['去重', 'simhash', '指纹'])) d6 -= 1; if (!md.includes('时效') && !md.includes('7天内') && !md.includes('90天')) d6 -= 1; if (!md.includes('异常') && !md.includes('冲突')) d6 -= 1; if (target.includes('刷评论') || target.includes('评分异常')) { if (!md.includes('异常')) d6 -= 2; } if (target.includes('数据冲突') || target.includes('截然相反')) { if (!md.includes('验证') && !md.includes('第三来源')) d6 -= 2; } d6 = clamp(d6, 0, 5); const total = d1 + d2 + d3 + d4 + d5 + d6; return { id: caseObj.id, case: target.slice(0, 60) + '...', scores: { D1: d1, D2: d2, D3: d3, D4: d4, D5: d5, D6: d6 }, total, passed: total >= BENCHMARK.passLine, eval_notes: notes.join(';') || 'OK', }; } function main(skillPath, datasetPath) { const evolveDir = __dirname; const testPath = datasetPath || path.join(evolveDir, '..', 'digest', 'test.jsonl'); const skillMdPath = skillPath || process.argv[2] || path.join(evolveDir, 'SKILL_v1.md'); if (!fs.existsSync(testPath)) { console.error('test.jsonl not found:', testPath); process.exit(1); } const cases = loadJsonl(testPath); const skillMd = loadSkillMd(skillMdPath); console.log(`\nEvaluating SKILL.md: ${skillMdPath}`); console.log(`Dataset: ${testPath}`); console.log(`Cases: ${cases.length}\n`); let totalScore = 0; let passCount = 0; const dimTotals = { D1: 0, D2: 0, D3: 0, D4: 0, D5: 0, D6: 0 }; const details = []; for (const c of cases) { const result = scoreCase(skillMd, c); totalScore += result.total; if (result.passed) passCount++; for (const k of Object.keys(dimTotals)) { dimTotals[k] += result.scores[k]; } details.push(result); console.log( `${result.passed ? '✅' : '❌'} [${result.id}] ${result.case} | Total: ${result.total}` ); } const avgTotal = Number((totalScore / cases.length).toFixed(2)); const dimAvgs = {}; for (const k of Object.keys(dimTotals)) { dimAvgs[k] = Number((dimTotals[k] / cases.length).toFixed(2)); } console.log(`\n--- Summary ---`); console.log(`Average Score: ${avgTotal} / 100`); console.log(`Pass Rate: ${passCount}/${cases.length}`); for (const dim of BENCHMARK.dimensions) { console.log(` ${dim.id} ${dim.name}: ${dimAvgs[dim.id]} / ${dim.max}`); } const globalPassed = avgTotal >= BENCHMARK.passLine; console.log(`\n${globalPassed ? '✅ GLOBAL PASS' : '❌ GLOBAL FAIL'}`); const outPath = path.join(evolveDir, 'iteration_scores_latest.json'); fs.writeFileSync( outPath, JSON.stringify({ iteration: 1, skill_file: path.basename(skillMdPath), avg_score: avgTotal, pass_rate: `${passCount}/${cases.length}`, global_passed: globalPassed, dim_avgs: dimAvgs, scores: details.map((r) => ({ id: r.id, instruction: r.case, D1: r.scores.D1, D2: r.scores.D2, D3: r.scores.D3, D4: r.scores.D4, D5: r.scores.D5, D6: r.scores.D6, total: r.total, passed: r.passed, eval_notes: r.eval_notes, })), }, null, 2) ); console.log(`\nScores written to ${outPath}`); return { avgTotal, passCount, dimAvgs, globalPassed, caseCount: cases.length, details, }; } module.exports = { main, scoreCase, BENCHMARK }; if (require.main === module) { main(); }