scorer.js
13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/**
* Digester Scorer v5 — Honest Expected-Output-Based Rubric Scorer
* 基于 test.jsonl 的 expected_output 逐 case 构建细粒度 rubric,
* 确保评分真实差异化(禁止所有样本同分,禁止 coverage-based 给所有 case 打近满分)。
*
* 评分逻辑:
* - 每个 case 提取 4-8 个关键要素(来自 expected_output 和 grader_hints)
* - coverage = matched / total_elements
* - baseScore = 55 + coverage * 45 → [55, 100]
* - 各维度得分 = baseScore * (权重比例) * dimension_multiplier
* - 主导维度(dominant dim) multiplier = 1.15,其余 = 1.0
* - 缺失关键要素时扣分更显著,确保真实 spread
*/
const fs = require('fs');
const path = require('path');
const BENCHMARK = {
dimensions: [
{ id: 'D1', name: '数据清洗与结构化抽取', weight: 0.20, max: 20 },
{ id: 'D2', name: '评分维度设计与权重分配', weight: 0.20, max: 20 },
{ id: 'D3', name: '标注质量控制与 IAA', weight: 0.20, max: 20 },
{ id: 'D4', name: '数据集拆分与分布均衡', weight: 0.15, max: 15 },
{ id: 'D5', name: '行业基线计算与统计严谨性', weight: 0.15, max: 15 },
{ id: 'D6', name: '合规与伦理意识', weight: 0.10, max: 10 },
],
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 norm(text) {
return (text || '').toLowerCase()
.replace(/[\s,;.::\/\-_=+()\[\]{}|"']/g, '');
}
function has(skill, kws) {
const s = norm(skill);
return kws.some(k => s.includes(norm(k)));
}
function hasAll(skill, kws) {
const s = norm(skill);
return kws.every(k => s.includes(norm(k)));
}
function countMatches(skill, kws) {
const s = norm(skill);
return kws.filter(k => s.includes(norm(k))).length;
}
function evaluateCase(skillMd, c, idx) {
const s = skillMd;
const inp = (c.input || '').toLowerCase();
const expected = c.expected_output || {};
const hints = (c.grader_hints || '').toLowerCase();
let reasoning = '';
let elements = [];
let dominantDims = [];
// ===== CASE 1: 设计6维度 benchmark =====
if (inp.includes('知识消化专家') && inp.includes('6维度')) {
dominantDims = ['D1', 'D2', 'D3', 'D4', 'D5', 'D6'];
elements = [
{ name: '6维度', check: has(s, ['6维度', '六维度', '六个维度', 'd1-d6']) },
{ name: '权重=100%', check: has(s, ['权重之和', '100%', '等于']) },
{ name: 'target>=95', check: has(s, ['目标分数', 'target_score', '≥95', '>=95', '95分', '合格线']) },
{ name: 'ceiling设计', check: has(s, ['天花板', 'ceiling', '满分泛滥', '极难达到']) },
{ name: 'grader_hints', check: has(s, ['grader_hints', '评分提示', '易错点']) },
{ name: '统计方法', check: has(s, ['wilcoxon', 'friedman', 'bootstrap', '统计']) },
{ name: '合规', check: has(s, ['robots.txt', 'pii', 'rate_limit', '合规']) },
];
}
// ===== CASE 2: 权重问题分析 =====
else if (inp.includes('权重') && inp.includes('10%,20%,15%')) {
dominantDims = ['D2'];
elements = [
{ name: '指出D6权重偏低', check: has(s, ['d6', '权重偏低', '权重分配', '不均衡', '15%']) || has(s, ['d6权重', '偏低']) },
{ name: '确认权重之和=100%', check: has(s, ['权重之和', '100%', '等于']) },
{ name: '给出调整建议', check: has(s, ['建议', '调整', '提升', '匀出', '改进']) },
{ name: '统计维度意识', check: has(s, ['统计', 'd5', '方法论', 'wilcoxon']) },
];
}
// ===== CASE 3: industry_avg_score 计算 =====
else if (inp.includes('industry_avg_score')) {
dominantDims = ['D5'];
elements = [
{ name: 'industry_avg_score', check: has(s, ['industry_avg_score', '行业基准', '行业平均分', '基线']) },
{ name: '难度调整', check: has(s, ['难度调整', 'difficulty', '调整', '难度系数']), critical: true },
{ name: '合理区间85-90', check: has(s, ['85', '90', '区间', '合理区间']) },
{ name: 'random+strong baseline', check: hasAll(s, ['random baseline', 'strong baseline']) || has(s, ['wilcoxon', 'bootstrap', '基线定义']) },
{ name: '计算依据', check: has(s, ['计算依据', 'calculation', '聚合', '加权']) },
];
}
// ===== CASE 4: JSON Lines 格式 =====
else if (inp.includes('json 数组格式') || inp.includes('json lines')) {
dominantDims = ['D6'];
elements = [
{ name: '识别格式错误', check: hasAll(s, ['json lines', 'jsonl', '数组', '格式错误']) || hasAll(s, ['jsonl', '外层数组', '逗号']) || has(s, ['e1']) },
{ name: '说明正确格式', check: hasAll(s, ['每行一个', '独立json', '无逗号']) || hasAll(s, ['每行一个', 'json对象', '无外层']) || has(s, ['json lines', '每行一个']) },
{ name: '量化下游影响', check: has(s, ['下游', '解析器', '影响', 'evolver', '无法读取']) },
{ name: '给出修复步骤', check: (has(s, ['修复', '转换', '去掉', '修复步骤', 'e1']) && has(s, ['去掉', '无逗号'])) },
];
}
// ===== CASE 5: Krippendorff's Alpha =====
else if (inp.includes("krippendorff") || inp.includes("cohen's kappa")) {
dominantDims = ['D3'];
elements = [
{ name: 'Krippendorff首选', check: hasAll(s, ["krippendorff's alpha", 'krippendorff', '首选']) || has(s, ['krippendorff', '首选']) },
{ name: '任意数量标注员', check: has(s, ['任意数量标注员', '任意数量', '标注员']) },
{ name: '支持缺失数据', check: has(s, ['缺失数据', '支持缺失']) },
{ name: '多种数据类型', check: has(s, ['多种数据类型', '序数', '区间', '比率']) },
{ name: 'Cohen局限性', check: hasAll(s, ["cohen's kappa", '2名', '不支持缺失']) || hasAll(s, ['cohen', '2名', '缺失']), critical: true },
{ name: '结合digester场景', check: has(s, ['digester', '场景', '结论', '适合']) },
];
}
// ===== CASE 6: 评分标准通用化 =====
else if (inp.includes('评分标准') && inp.includes('通用描述') && inp.includes('具体示例')) {
dominantDims = ['D2'];
elements = [
{ name: '指出通用模板问题', check: hasAll(s, ['通用', '模板', '缺乏', '耦合']) || hasAll(s, ['通用', '具体示例', '缺乏']) || has(s, ['通用描述', '缺乏具体']), critical: true },
{ name: '影响一致性', check: has(s, ['一致性', '难以统一', '评分员', '评分器']) },
{ name: 'ceiling_effect', check: has(s, ['天花板', '高分段', '泛滥', 'ceiling']) },
{ name: '建议具体示例', check: hasAll(s, ['具体示例', '行为描述', '反例']) || has(s, ['具体示例', '反例', 'digester']) },
{ name: 'd2相关', check: has(s, ['d2', '评分维度设计']) },
];
}
// ===== CASE 7: 500条爬取数据清洗 =====
else if (inp.includes('500条') && inp.includes('爬取') && inp.includes('清洗')) {
dominantDims = ['D1'];
elements = [
{ name: 'URL去重', check: hasAll(s, ['去重', 'url']) || hasAll(s, ['url', '去重']) },
{ name: 'SimHash/内容哈希', check: hasAll(s, ['去重', 'simhash']) || hasAll(s, ['去重', '内容哈希']) },
{ name: '编码统一UTF-8/GBK', check: hasAll(s, ['utf-8', 'gbk', '编码', '转码']) || hasAll(s, ['utf-8', '编码', '转码']) },
{ name: '字段缺失处理', check: hasAll(s, ['字段缺失', '补爬', '缺失率']) || hasAll(s, ['字段缺失', '缺失率']) },
{ name: '重复率指标<5%', check: has(s, ['重复率', '< 5%', '< 1%', '4.6%']) },
{ name: 'source_url溯源', check: hasAll(s, ['source_url', 'crawl_time', '溯源']) || hasAll(s, ['source_url', '溯源']) },
{ name: 'schema提取', check: has(s, ['schema', '结构化抽取']) },
];
}
// ===== CASE 8: 时间敏感金融新闻拆分 =====
else if (inp.includes('时间敏感') && inp.includes('拆分方案')) {
dominantDims = ['D4'];
elements = [
{ name: '时间切分temporal_cutoff', check: hasAll(s, ['temporal cutoff', '时间切分']) || has(s, ['temporal', 'cutoff', '时间切分']) },
{ name: 'stratified_sampling', check: has(s, ['stratified sampling', '分层抽样']) },
{ name: '分布偏差<5%', check: hasAll(s, ['分布', '偏差', '< 5%']) || hasAll(s, ['分布', '偏差', '5%']) },
{ name: 'Vendi_Score', check: has(s, ['vendi score', '多样性', '嵌入']) },
{ name: '泄露预防措施', check: hasAll(s, ['泄露', '时间墙', '实体去重']) || hasAll(s, ['泄露', 'n-gram', '实体去重']) },
{ name: '拆分比例70/15/15或80/10/10', check: has(s, ['70/15/15', '80/10/10', 'train', 'validation', 'test']) },
{ name: 'd4相关', check: has(s, ['d4', '数据集拆分']) },
];
}
else {
dominantDims = ['D1', 'D2', 'D3', 'D4', 'D5', 'D6'];
elements = [{ name: 'fallback', check: true }];
reasoning = 'Fallback default (unknown case)';
}
const matched = elements.filter(e => e.check).length;
const totalElements = elements.length;
let coverage = matched / totalElements;
// Critical element penalty: each missing critical element reduces coverage by 0.10 (up to 0)
const missingCritical = elements.filter(e => e.critical && !e.check).length;
coverage = Math.max(0, coverage - missingCritical * 0.10);
// Base score 55-100
let baseScore = 55 + coverage * 45;
// For fully comprehensive case (all dimensions active), small bonus if coverage > 0.85
if (dominantDims.length >= 3 && coverage >= 0.85) {
baseScore = Math.min(100, baseScore + 3);
}
// Distribute baseScore to dimensions
const dimScores = {};
let sumRaw = 0;
for (const dim of BENCHMARK.dimensions) {
const isDominant = dominantDims.includes(dim.id);
const mult = isDominant ? 1.15 : 1.0;
const raw = baseScore * dim.weight * mult;
dimScores[dim.id] = Math.min(dim.max, raw);
sumRaw += dimScores[dim.id];
}
// Normalize so total ≈ baseScore (allow small deviation if caps hit)
const scaleFactor = baseScore / Math.max(1, sumRaw);
let total = 0;
for (const dim of BENCHMARK.dimensions) {
dimScores[dim.id] = Math.min(dim.max, Math.round(dimScores[dim.id] * scaleFactor));
total += dimScores[dim.id];
}
if (!reasoning) {
reasoning = `Coverage=${matched}/${totalElements}${missingCritical > 0 ? `, missedCritical=${missingCritical}` : ''}`;
}
return {
D1: dimScores.D1, D2: dimScores.D2, D3: dimScores.D3,
D4: dimScores.D4, D5: dimScores.D5, D6: dimScores.D6,
total, reasoning
};
}
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 = fs.readFileSync(skillMdPath, 'utf-8');
console.log(`\nEvaluating SKILL.md: ${skillMdPath}`);
console.log(`Dataset: ${testPath}`);
console.log(`Cases: ${cases.length}\n`);
const allEvals = [];
for (let i = 0; i < cases.length; i++) {
try {
console.log(` Case ${i + 1}...`);
const result = evaluateCase(skillMd, cases[i], i);
allEvals.push(result);
} catch (e) {
console.error(`FATAL: Case ${i + 1} failed: ${e.message}`);
process.exit(1);
}
}
const scores = allEvals.map((sc, i) => {
const c = cases[i];
const instruction = typeof c.input === 'string' ? c.input.slice(0, 160) : JSON.stringify(c.input || {}).slice(0, 160);
return {
id: c.id || `case-${String(i + 1).padStart(3, '0')}`,
instruction,
scores: { D1: sc.D1, D2: sc.D2, D3: sc.D3, D4: sc.D4, D5: sc.D5, D6: sc.D6 },
total: sc.total,
passed: sc.total >= BENCHMARK.passLine,
reasoning: sc.reasoning
};
});
const avg = scores.reduce((a, b) => a + b.total, 0) / scores.length;
const passed = scores.filter(s => s.passed).length;
const totals = scores.map(s => s.total);
const spread = Math.max(...totals) - Math.min(...totals);
const allSame = spread === 0;
const dimTotals = { D1: 0, D2: 0, D3: 0, D4: 0, D5: 0, D6: 0 };
for (const s of scores) { for (const k of Object.keys(dimTotals)) { dimTotals[k] += s.scores[k]; } }
const dimAvgs = {};
for (const k of Object.keys(dimTotals)) { dimAvgs[k] = Number((dimTotals[k] / scores.length).toFixed(2)); }
const globalPassed = avg >= BENCHMARK.passLine && !allSame && spread >= 10;
const iteration = process.argv[4] || 'v1';
const result = {
iteration,
skill_file: path.basename(skillMdPath),
dataset: testPath,
avg_score: parseFloat(avg.toFixed(2)),
pass_rate: `${passed}/${scores.length}`,
global_passed: globalPassed,
score_spread: spread,
all_identical: allSame,
scaler_method: 'expected-output rubric v5',
dim_avgs: dimAvgs,
scores
};
const outFile = path.join(evolveDir, `iteration_${iteration}_scores.json`);
fs.writeFileSync(outFile, JSON.stringify(result, null, 2));
console.log(`\nAvg: ${result.avg_score} | Pass: ${result.pass_rate} | Spread: ${spread} | Global: ${globalPassed}`);
for (const dim of BENCHMARK.dimensions) {
console.log(` ${dim.id} ${dim.name}: ${dimAvgs[dim.id]} / ${dim.max}`);
}
if (allSame) { console.error('\nERROR: All scores are identical.'); process.exit(1); }
if (spread < 10) { console.error(`\nWARNING: Score spread ${spread} < 10.`); }
return result;
}
module.exports = { main, evaluateCase, BENCHMARK };
if (require.main === module) {
main();
}