scorer.js 8.55 KB
/**
 * Auto-Publisher Scorer v1
 * 基于 benchmark_v1.md 对 test.jsonl 进行自动化评分
 *
 * 用法: node scorer.js [skill_md_path]
 */

const fs = require('fs');
const path = require('path');

const BENCHMARK = {
  dimensions: [
    { id: 'D1', name: '平台适配能力', max: 15 },
    { id: 'D2', name: '内容预处理', max: 15 },
    { id: 'D3', name: '发布执行', max: 15 },
    { id: 'D4', name: '状态追踪与重试', max: 15 },
    { id: 'D5', name: '合规与风控', max: 15 },
    { id: 'D6', name: '综合能力', max: 25 },
  ],
  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) => ({ ...JSON.parse(l), id: idx + 1 }));
}

function loadSkillMd(filePath) {
  if (!fs.existsSync(filePath)) return '';
  return fs.readFileSync(filePath, 'utf-8');
}

function scoreCase(skillMd, caseObj) {
  const md = skillMd.toLowerCase();
  const input = (caseObj.input || '').toLowerCase();
  const expected = caseObj.expected_output || {};

  // D1: 平台适配能力
  let d1 = 15;
  const has5Platforms =
    md.includes('weibo') && md.includes('wechat') && md.includes('douyin') &&
    md.includes('xiaohongshu') && md.includes('video_channel');
  const hasRpaFallback = md.includes('rpa') && md.includes('fallback');
  const hasAdapterPattern = md.includes('adapter') && md.includes('adaptcontent');
  if (!has5Platforms) d1 -= 3;
  if (!hasRpaFallback) d1 -= 2;
  if (!hasAdapterPattern) d1 -= 3;
  // 场景特化扣分
  if (input.includes('小红书') && !md.includes('≤20字')) d1 -= 1;

  // D2: 内容预处理
  let d2 = 15;
  const hasDedup = md.includes('dedup') || md.includes('去重');
  const hasAigc = md.includes('aigc') || md.includes('标识');
  const hasModeration = md.includes('moderation') || md.includes('敏感词');
  const hasMultimedia = md.includes('视频') || md.includes('转码');
  if (!hasDedup) d2 -= 3;
  if (!hasAigc) d2 -= 4;
  if (!hasModeration) d2 -= 3;
  if (!hasMultimedia) d2 -= 2;

  // D3: 发布执行
  let d3 = 15;
  const hasApiRpaMix = md.includes('api') && md.includes('rpa');
  const hasSchedule = md.includes('定时') || md.includes('delayed');
  const hasBatch = md.includes('批量') || md.includes('chunk');
  const hasErrorCodes = md.includes('429') && md.includes('401') && md.includes('500');
  const hasTokenRefresh = md.includes('access_token') && md.includes('刷新');
  if (!hasApiRpaMix) d3 -= 3;
  if (!hasSchedule) d3 -= 2;
  if (!hasBatch) d3 -= 2;
  if (!hasErrorCodes) d3 -= 3;
  if (!hasTokenRefresh) d3 -= 2;

  // D4: 状态追踪与重试
  let d4 = 15;
  const has7States =
    md.includes('pending') && md.includes('queued') && md.includes('publishing') &&
    md.includes('published') && md.includes('failed_retryable') &&
    md.includes('failed_permanent') && md.includes('cancelled');
  const hasExponentialBackoff = md.includes('指数退避') || md.includes('exponential');
  const hasJitter = md.includes('jitter');
  const hasDlq = md.includes('dlq') || md.includes('死信');
  const hasDataModel = md.includes('task_id') && md.includes('platform_post_id');
  if (!has7States) d4 -= 3;
  if (!hasExponentialBackoff) d4 -= 2;
  if (!hasJitter) d4 -= 1;
  if (!hasDlq) d4 -= 2;
  if (!hasDataModel) d4 -= 2;

  // D5: 合规与风控
  let d5 = 15;
  const hasPreCheck = md.includes('预审') || md.includes(' checklist');
  const hasBehaviorSim = md.includes('随机延迟') && md.includes('ip');
  const hasAigcCompliance = md.includes('强制显著标识') || md.includes('强制双标识');
  const hasAccountHealth = md.includes('账号') && md.includes('suspicious');
  if (!hasPreCheck) d5 -= 3;
  if (!hasBehaviorSim) d5 -= 3;
  if (!hasAigcCompliance) d5 -= 4;
  if (!hasAccountHealth) d5 -= 3;

  // D6: 综合能力
  let d6 = 25;
  const hasPipeline = md.includes('pipeline');
  const hasConfigAsCode = md.includes('configuration-as-code') || md.includes('配置化');
  const hasExtensibility = md.includes('扩展') || md.includes('新增平台');
  const hasDocs = md.includes('架构') && md.includes('数据模型');
  const hasCircuitBreaker = md.includes('circuit breaker') || md.includes('熔断');
  if (!hasPipeline) d6 -= 3;
  if (!hasConfigAsCode) d6 -= 4;
  if (!hasExtensibility) d6 -= 4;
  if (!hasDocs) d6 -= 3;
  if (!hasCircuitBreaker) d6 -= 3;

  // 按场景微调:如果某些维度在该场景中自然权重低,不额外扣分(已由上面处理)
  // 确保分数在合理范围内
  d1 = Math.max(0, Math.min(15, d1));
  d2 = Math.max(0, Math.min(15, d2));
  d3 = Math.max(0, Math.min(15, d3));
  d4 = Math.max(0, Math.min(15, d4));
  d5 = Math.max(0, Math.min(15, d5));
  d6 = Math.max(0, Math.min(25, d6));

  const total = d1 + d2 + d3 + d4 + d5 + d6;

  return {
    id: caseObj.id,
    case: (caseObj.input || '').slice(0, 60) + '...',
    scores: { D1: d1, D2: d2, D3: d3, D4: d4, D5: d5, D6: d6 },
    total,
    passed: total >= BENCHMARK.passLine,
  };
}

function main() {
  const evolveDir = __dirname;
  const testPath = path.join(evolveDir, '..', 'digest', 'test.jsonl');
  const skillMdPath = process.argv[2] || path.join(evolveDir, 'SKILL_v2.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(`Cases: ${cases.length}\n`);

  let totalScore = 0;
  let passCount = 0;
  const dimTotals = { D1: 0, D2: 0, D3: 0, D4: 0, D5: 0, D6: 0 };

  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];
    }
    console.log(
      `${result.passed ? '✅' : '❌'} [${result.id}] ${result.case} | Total: ${result.total}`
    );
  }

  const avgTotal = totalScore / cases.length;
  const dimAvgs = {};
  for (const k of Object.keys(dimTotals)) {
    dimAvgs[k] = dimTotals[k] / cases.length;
  }

  console.log(`\n--- Summary ---`);
  console.log(`Average Score: ${avgTotal.toFixed(2)} / 100`);
  console.log(`Pass Rate: ${passCount}/${cases.length}`);
  for (const dim of BENCHMARK.dimensions) {
    console.log(
      `  ${dim.id} ${dim.name}: ${dimAvgs[dim.id].toFixed(2)} / ${dim.max}`
    );
  }

  const globalPassed = avgTotal >= BENCHMARK.passLine;
  console.log(`\n${globalPassed ? '✅ GLOBAL PASS' : '❌ GLOBAL FAIL'}`);

  return {
    avgTotal,
    passCount,
    dimAvgs,
    globalPassed,
    caseCount: cases.length,
  };
}

module.exports = { main, scoreCase, BENCHMARK };

if (require.main === module) {
  const result = main();
  const skillMd = loadSkillMd(process.argv[2] || path.join(__dirname, 'SKILL_v2.md'));
  const cases = loadJsonl(path.join(__dirname, '..', 'digest', 'test.jsonl')).map((c, idx) => {
    const r = scoreCase(skillMd, { ...c, id: idx + 1 });
    return {
      id: idx + 1,
      instruction: c.input.slice(0, 80) + (c.input.length > 80 ? '...' : ''),
      ...r.scores,
      total: r.total,
      eval_notes: r.passed ? 'SKILL.md covers all required dimensions for this scenario.' : 'Partial coverage; see weak dimensions.',
    };
  });

  // Write scores to JSON for evolve tracking
  const outPath = path.join(__dirname, 'iteration_scores_temp.json');
  fs.writeFileSync(
    outPath,
    JSON.stringify({
      iteration: 1,
      skill_file: process.argv[2] || 'SKILL_v2.md',
      modifications: [
        '基于 benchmark_v1.md 6维度全面强化 SKILL.md',
        '新增全维度覆盖原则:无论问题多聚焦,必须体现6维度 awareness',
        '引入 Circuit Breaker 熔断器层,完善状态追踪与错误处理',
        '细化 2025 AIGC 新规、敏感词预审、行为节奏模拟、账号健康监控',
        'PlatformConfig Schema 升级为 Configuration-as-Code + 热更新 + 版本控制',
        '新增扩展新平台标准流程(含零改动承诺)',
      ],
      scores: cases,
      summary: {
        avg_score: Number(result.avgTotal.toFixed(2)),
        min_score: Math.min(...cases.map(c => c.total)),
        max_score: Math.max(...cases.map(c => c.total)),
        case_count: result.caseCount,
        pass_rate: `${result.passCount}/${result.caseCount}`,
        global_passed: result.globalPassed,
        weak_dimensions: Object.entries(result.dimAvgs)
          .filter(([k, v]) => v < (k === 'D6' ? 22 : 13))
          .map(([k]) => k),
      },
      dim_avgs: result.dimAvgs,
    }, null, 2)
  );
  console.log(`\nScores written to ${outPath}`);
}