benchmark-runner.js
12.9 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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/**
* Benchmark Runner — 6 维度评分 + ELO 排名引擎
*
* MVP 版本: 使用 heuristic-based 评分(基于 Skill 元数据质量)
* 后续 Phase 4: 接入 Innovator 做真实沙盒执行评测
*
* 评分维度:
* successRate — 成功率 (元数据完整性 + 状态)
* accuracy — 准确性 (描述质量 + 依赖匹配)
* speed — 速度 (代码量 → 推断执行耗时)
* cost — 成本 (层级 + 依赖数 → 推断资源消耗)
* robustness — 鲁棒性 (错误处理关键词 + 依赖丰富度)
* completeness — 完成度 (L3 独有, 端到端覆盖度)
*/
const path = require('path');
const fs = require('fs');
const benchmarkDB = require('./benchmark-db');
const config = require('../config');
const skillsDir = config.skillsCodeDir;
const skillDb = require('./skill-db');
// ═══════════════════════════════════════════
// 配置
// ═══════════════════════════════════════════
// 权重矩阵 [L1, L2, L3]
const WEIGHTS = {
successRate: [0.40, 0.30, 0.25],
accuracy: [0.30, 0.25, 0.20],
speed: [0.20, 0.15, 0.10],
cost: [0.05, 0.15, 0.15],
robustness: [0.05, 0.10, 0.10],
completeness: [0.00, 0.05, 0.20],
};
// K-factor 衰减
function getKFactor(benchmarkRuns) {
if (benchmarkRuns < 10) return 40;
if (benchmarkRuns < 50) return 20;
return 10;
}
// ═══════════════════════════════════════════
// Skill 列表读写
// ═══════════════════════════════════════════
function loadSkills() {
return skillDb.listSkills();
}
function saveSkills(skills) {
for (const skill of skills) {
const updates = { ...skill };
delete updates.skillId;
delete updates.skillName;
skillDb.updateSkill(skill.skillName, updates);
}
}
// ═══════════════════════════════════════════
// Heuristic-Based 评分引擎
// ═══════════════════════════════════════════
/**
* 为单个 Skill 生成 6 维度分数(基于元数据质量的 heuristic)
*/
function scoreSkill(skill) {
const name = skill.skillName || '';
const desc = skill.description || '';
const deps = skill.dependencies || [];
const tags = skill.tags || skill.keywords || [];
const layer = skill.layer || 1;
const status = skill.status || 'enable';
// 伪随机种子 (基于技能名,确保一致性)
let seed = 0;
for (let i = 0; i < name.length; i++) {
seed = ((seed << 5) - seed + name.charCodeAt(i)) | 0;
}
const seededRandom = (min, max) => {
seed = (seed * 16807 + 12345) & 0x7fffffff;
return min + (seed / 0x7fffffff) * (max - min);
};
// === successRate (0-100) ===
let successRate = 70; // 基础分
if (status === 'enable') successRate += 10;
if (desc.length > 20) successRate += 5;
if (desc.length > 80) successRate += 5;
if (tags.length > 0) successRate += 3;
if (skill.version) successRate += 2;
successRate += seededRandom(-5, 5);
successRate = Math.max(50, Math.min(100, successRate));
// === accuracy (0-100) ===
let accuracy = 65;
if (desc.length > 30) accuracy += 8;
if (desc.length > 100) accuracy += 7;
if (tags.length >= 3) accuracy += 5;
if (deps.length > 0 && layer >= 2) accuracy += 5;
if (skill.domain) accuracy += 3;
accuracy += seededRandom(-8, 8);
accuracy = Math.max(40, Math.min(100, accuracy));
// === speed (seconds, lower is better) ===
let speed = 1.0; // base seconds
if (layer === 2) speed = 2.0;
if (layer === 3) speed = 4.0;
speed += deps.length * 0.3;
speed += seededRandom(-0.3, 0.8);
speed = Math.max(0.1, speed);
// === cost (dollars per run, lower is better) ===
let cost = 0.001;
if (layer === 2) cost = 0.005;
if (layer === 3) cost = 0.02;
cost += deps.length * 0.002;
cost += seededRandom(-0.001, 0.005);
cost = Math.max(0.0001, cost);
// === robustness (0-100) ===
let robustness = 60;
if (deps.length > 0) robustness += 5;
if (tags.length >= 2) robustness += 5;
if (desc.includes('error') || desc.includes('retry') || desc.includes('timeout')) robustness += 10;
if (desc.includes('validate') || desc.includes('check')) robustness += 5;
if (skill.version && skill.version !== '1.0.0') robustness += 5; // iterated
robustness += seededRandom(-8, 8);
robustness = Math.max(30, Math.min(100, robustness));
// === completeness (0-100, mainly for L3) ===
let completeness = 0;
if (layer >= 2) {
completeness = 50;
completeness += deps.length * 8;
completeness += (desc.length > 100 ? 10 : 0);
completeness += seededRandom(-10, 10);
completeness = Math.max(20, Math.min(100, completeness));
}
// 模拟测试用例
const testCasesTotal = 5 + Math.floor(seededRandom(0, 10));
const passRate = successRate / 100;
const testCasesPassed = Math.floor(testCasesTotal * passRate);
return {
successRate: parseFloat(successRate.toFixed(1)),
accuracy: parseFloat(accuracy.toFixed(1)),
speed: parseFloat(speed.toFixed(2)),
cost: parseFloat(cost.toFixed(4)),
robustness: parseFloat(robustness.toFixed(1)),
completeness: layer >= 2 ? parseFloat(completeness.toFixed(1)) : null,
testCasesTotal,
testCasesPassed,
};
}
/**
* 计算加权综合分 (0-100 scale)
*/
function computeWeightedScore(scores, layer) {
const layerIdx = Math.max(0, Math.min(2, (layer || 1) - 1));
let total = 0;
// 正向维度 (越高越好)
total += (scores.successRate || 0) * WEIGHTS.successRate[layerIdx];
total += (scores.accuracy || 0) * WEIGHTS.accuracy[layerIdx];
total += (scores.robustness || 0) * WEIGHTS.robustness[layerIdx];
total += (scores.completeness || 0) * WEIGHTS.completeness[layerIdx];
// 反向维度 (越低越好) → 转换为 0-100 分
const speedScore = Math.max(0, 100 - scores.speed * 10); // 10s → 0分, 0s → 100分
total += speedScore * WEIGHTS.speed[layerIdx];
const costScore = Math.max(0, 100 - scores.cost * 1000); // $0.1 → 0分, $0 → 100分
total += costScore * WEIGHTS.cost[layerIdx];
return parseFloat(total.toFixed(2));
}
// ═══════════════════════════════════════════
// ELO 计算
// ═══════════════════════════════════════════
/**
* 计算 ELO 预期得分
*/
function expectedScore(eloA, eloB) {
return 1 / (1 + Math.pow(10, (eloB - eloA) / 400));
}
/**
* 更新 ELO (返回 [newEloA, newEloB])
*/
function updateElo(eloA, eloB, scoreA, scoreB, kA, kB) {
const expA = expectedScore(eloA, eloB);
const expB = expectedScore(eloB, eloA);
// 实际结果: 1=赢, 0=输, 0.5=平
const actualA = scoreA > scoreB ? 1 : scoreA === scoreB ? 0.5 : 0;
const actualB = 1 - actualA;
const newA = Math.round(eloA + kA * (actualA - expA));
const newB = Math.round(eloB + kB * (actualB - expB));
return [newA, newB];
}
/**
* 在同场景 + 同 Layer 内做全量 ELO 更新
*/
function runEloTournament(skills) {
// 按 scene + layer 分组
const groups = {};
for (const s of skills) {
const key = `${s.scene || 'unknown'}_L${s.layer || 0}`;
if (!groups[key]) groups[key] = [];
groups[key].push(s);
}
let totalComparisons = 0;
for (const [groupKey, group] of Object.entries(groups)) {
if (group.length < 2) continue;
// 两两比较
for (let i = 0; i < group.length; i++) {
for (let j = i + 1; j < group.length; j++) {
const a = group[i];
const b = group[j];
const scoreA = a.arena?.scores ? computeWeightedScore(a.arena.scores, a.layer) : 50;
const scoreB = b.arena?.scores ? computeWeightedScore(b.arena.scores, b.layer) : 50;
const eloA = a.arena?.elo || 1200;
const eloB = b.arena?.elo || 1200;
const kA = getKFactor(a.arena?.benchmarkRuns || 0);
const kB = getKFactor(b.arena?.benchmarkRuns || 0);
const [newA, newB] = updateElo(eloA, eloB, scoreA, scoreB, kA, kB);
// 更新 ELO
if (a.arena) a.arena.elo = newA;
if (b.arena) b.arena.elo = newB;
// 记录到 SQLite
const winner = scoreA > scoreB ? a.skillName
: scoreB > scoreA ? b.skillName
: 'tie';
benchmarkDB.insertComparison({
skillA: a.skillName,
skillB: b.skillName,
scene: a.scene,
layer: a.layer,
winner,
scoreA,
scoreB,
});
totalComparisons++;
}
}
}
return totalComparisons;
}
// ═══════════════════════════════════════════
// 主流程
// ═══════════════════════════════════════════
/**
* 对单个 Skill 执行 benchmark
*/
function benchmarkSkill(skill) {
// 确保有 arena 字段
if (!skill.arena) {
skill.arena = {
elo: 1200,
eloMargin: 50,
rank: null,
benchmarkRuns: 0,
lastBenchmark: null,
scores: {},
securityScan: { status: 'pending' },
highlighted: false,
trending: false,
};
}
const eloBefore = skill.arena.elo || 1200;
// Step 1: 评分
const scores = scoreSkill(skill);
const weightedScore = computeWeightedScore(scores, skill.layer);
// Step 2: 更新 Skill 元数据
skill.arena.scores = {
successRate: scores.successRate,
accuracy: scores.accuracy,
speed: scores.speed,
cost: scores.cost,
robustness: scores.robustness,
completeness: scores.completeness,
};
skill.arena.benchmarkRuns = (skill.arena.benchmarkRuns || 0) + 1;
skill.arena.lastBenchmark = new Date().toISOString();
// Step 3: 记录到 SQLite
benchmarkDB.insertBenchmarkRun({
skillName: skill.skillName,
version: skill.version || null,
successRate: scores.successRate,
accuracy: scores.accuracy,
speedP50: scores.speed,
speedP95: scores.speed * 1.5,
costPerRun: scores.cost,
robustness: scores.robustness,
completeness: scores.completeness,
weightedScore,
eloBefore,
eloAfter: eloBefore, // ELO 在 tournament 阶段更新
testCasesTotal: scores.testCasesTotal,
testCasesPassed: scores.testCasesPassed,
errorLog: null,
});
return { scores, weightedScore };
}
/**
* 对所有 Skill 执行 benchmark + ELO tournament
*/
function benchmarkAll() {
// 初始化数据库
benchmarkDB.initDB();
const skills = loadSkills();
const results = [];
console.log(`[BenchmarkRunner] Starting benchmark for ${skills.length} skills...`);
// Step 1: 评分所有 Skill
for (const skill of skills) {
const { scores, weightedScore } = benchmarkSkill(skill);
results.push({
skillName: skill.skillName,
layer: skill.layer,
scene: skill.scene,
weightedScore,
scores,
});
}
// Step 2: ELO Tournament
const totalComparisons = runEloTournament(skills);
// Step 3: 更新 benchmark_runs 中的 elo_after
for (const skill of skills) {
// 更新最后一条记录的 elo_after
const db = benchmarkDB.initDB();
db.prepare(`
UPDATE benchmark_runs
SET elo_after = ?
WHERE skill_name = ?
ORDER BY id DESC
LIMIT 1
`).run(skill.arena?.elo || 1200, skill.skillName);
}
// Step 4: 计算排名
computeRanks(skills);
// Step 5: 保存
saveSkills(skills);
console.log(`[BenchmarkRunner] Completed: ${skills.length} skills scored, ${totalComparisons} ELO comparisons.`);
return {
totalSkills: skills.length,
totalComparisons,
results: results.sort((a, b) => b.weightedScore - a.weightedScore),
};
}
/**
* 计算排名
*/
function computeRanks(skills) {
// 按场景 + Layer 分组排名
const groups = {};
for (const s of skills) {
const key = `${s.scene || 'unknown'}_L${s.layer || 0}`;
if (!groups[key]) groups[key] = [];
groups[key].push(s);
}
for (const [, group] of Object.entries(groups)) {
group.sort((a, b) => (b.arena?.elo || 1200) - (a.arena?.elo || 1200));
group.forEach((s, i) => {
if (s.arena) s.arena.rank = i + 1;
});
}
// 全局排名
const allSorted = [...skills].sort((a, b) => (b.arena?.elo || 1200) - (a.arena?.elo || 1200));
allSorted.forEach((s, i) => {
if (s.arena) s.arena.globalRank = i + 1;
});
// 标记 highlighted (每个场景 top 3)
for (const [, group] of Object.entries(groups)) {
group.forEach((s, i) => {
if (s.arena) s.arena.highlighted = (i < 3);
});
}
}
module.exports = {
scoreSkill,
computeWeightedScore,
benchmarkSkill,
benchmarkAll,
runEloTournament,
getKFactor,
};