zelk12 commited on
Commit
0ee6704
·
verified ·
1 Parent(s): 67cebd9

Upload 3 files

Browse files

At the moment, the main problem is the API, which interrupts the thinking and triggers a response, since in this case it is not possible to accurately understand that the model could not exit the loop.

---
---

На данный момент, основная проблема, это API, который прерывает мышление, и запускает ответ, так как в таком случае не удаётся, точно понять, что модель, не смогла бы выйти из цикла.

Files changed (3) hide show
  1. RLB/benchlocal.pack.json +18 -0
  2. RLB/index.mjs +433 -0
  3. RLB/scenarios.json +0 -0
RLB/benchlocal.pack.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": 1,
3
+ "protocolVersion": 1,
4
+ "id": "repetition-loops-suite-90",
5
+ "name": "Изоморфный бенчмарк зацикливания x90 (Repetition Loops)",
6
+ "version": "6.2.0",
7
+ "description": "Симметричный многоуровневый бенчмарк дегенерации декодера: ровно 90 изоморфных задач (30 RU, 30 EN, 30 MIX) со строгой бинарной оценкой и защитой от списывания примеров.",
8
+ "entry": "./index.mjs",
9
+ "samplingDefaults": {
10
+ "temperature": 0.0
11
+ },
12
+ "capabilities": {
13
+ "tools": false,
14
+ "multiTurn": false,
15
+ "streamingProgress": false,
16
+ "verification": false
17
+ }
18
+ }
RLB/index.mjs ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const manifestPath = path.join(__dirname, "benchlocal.pack.json");
7
+ const scenariosPath = path.join(__dirname, "scenarios.json");
8
+
9
+ export const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
10
+
11
+ function loadScenarios() {
12
+ return JSON.parse(fs.readFileSync(scenariosPath, "utf8"));
13
+ }
14
+
15
+ export function listScenarios() {
16
+ return loadScenarios().map(s => ({
17
+ id: s.id,
18
+ name: s.name,
19
+ category: s.category || "Общие",
20
+ description: s.name
21
+ }));
22
+ }
23
+
24
+ export function getScenarios() {
25
+ return listScenarios();
26
+ }
27
+
28
+ /**
29
+ * Нормализация букв-омоглифов (защита от различий раскладки А/A, В/B, С/C, Е/E, Н/H, Р/P, Х/X)
30
+ */
31
+ function normalizeScriptChar(char) {
32
+ const c = String(char || "").trim().toUpperCase();
33
+ const map = { "A": "А", "B": "В", "C": "С", "E": "Е", "H": "Н", "P": "Р", "X": "Х" };
34
+ return map[c] || c;
35
+ }
36
+
37
+ /**
38
+ * Детектор макро-зацикливаний (период блока от 25 до 250 слов)
39
+ */
40
+ function detectMacroLoop(text) {
41
+ const words = (text.toLowerCase().match(/[\p{L}\p{N}'-]+/gu) || []);
42
+ const n = words.length;
43
+ if (n < 50) return { isLoop: false };
44
+
45
+ const maxL = Math.min(250, Math.floor(n / 2));
46
+ for (let L = 25; L <= maxL; L += 5) {
47
+ const step = Math.max(1, Math.floor(L / 4));
48
+ for (let i = 0; i <= n - 2 * L; i += step) {
49
+ let matches = 0;
50
+ for (let j = 0; j < L; j++) {
51
+ if (words[i + j] === words[i + L + j]) {
52
+ matches++;
53
+ }
54
+ }
55
+ const similarity = matches / L;
56
+ if (similarity >= 0.85) {
57
+ const sample = words.slice(i, i + Math.min(12, L)).join(" ");
58
+ return {
59
+ isLoop: true,
60
+ reason: `Макро-петля: повторяющийся блок ~${L} слов (${Math.round(similarity * 100)}% совпадения): '${sample}...'`
61
+ };
62
+ }
63
+ }
64
+ }
65
+
66
+ const paragraphs = text
67
+ .split(/\n+/)
68
+ .map(p => p.trim().toLowerCase())
69
+ .filter(p => p.split(/\s+/).length >= 15);
70
+
71
+ for (let i = 0; i < paragraphs.length - 1; i++) {
72
+ if (paragraphs[i] === paragraphs[i + 1]) {
73
+ return {
74
+ isLoop: true,
75
+ reason: `Повтор целого абзаца подряд: '${paragraphs[i].slice(0, 50)}...'`
76
+ };
77
+ }
78
+ }
79
+
80
+ return { isLoop: false };
81
+ }
82
+
83
+ /**
84
+ * Детектор микро-зацикливаний (слова, биграммы, короткие фразы)
85
+ */
86
+ function detectMicroLoop(text, finishReason) {
87
+ const words = (text.toLowerCase().match(/[\p{L}\p{N}'-]+/gu) || []);
88
+ if (words.length < 5) return { isLoop: false };
89
+
90
+ for (let i = 0; i <= words.length - 5; i++) {
91
+ const w = words[i];
92
+ if (words[i + 1] === w && words[i + 2] === w && words[i + 3] === w && words[i + 4] === w) {
93
+ return { isLoop: true, reason: `Повтор слова 5+ раз подряд: '${w}'` };
94
+ }
95
+ }
96
+
97
+ for (let i = 0; i <= words.length - 8; i++) {
98
+ if (
99
+ words[i] === words[i + 2] && words[i + 1] === words[i + 3] &&
100
+ words[i] === words[i + 4] && words[i + 1] === words[i + 5] &&
101
+ words[i] === words[i + 6] && words[i + 1] === words[i + 7]
102
+ ) {
103
+ return { isLoop: true, reason: `Тандемный повтор пары слов: '${words[i]} ${words[i + 1]}'` };
104
+ }
105
+ }
106
+
107
+ const maxK = Math.min(20, Math.floor(words.length / 3));
108
+ for (let k = 3; k <= maxK; k++) {
109
+ for (let i = 0; i <= words.length - 3 * k; i++) {
110
+ let isMatch = true;
111
+ for (let j = 0; j < k; j++) {
112
+ if (words[i + j] !== words[i + k + j] || words[i + j] !== words[i + 2 * k + j]) {
113
+ isMatch = false;
114
+ break;
115
+ }
116
+ }
117
+ if (isMatch) {
118
+ const sample = words.slice(i, i + Math.min(k, 6)).join(" ");
119
+ return { isLoop: true, reason: `Тандемный повтор фразы из ${k} слов: '${sample}...'` };
120
+ }
121
+ }
122
+ }
123
+
124
+ if (finishReason === "length" && words.length >= 20) {
125
+ const tail = words.slice(-20);
126
+ const tailBigrams = new Set();
127
+ for (let i = 0; i < tail.length - 1; i++) {
128
+ tailBigrams.add(`${tail[i]}_${tail[i + 1]}`);
129
+ }
130
+ if (tailBigrams.size <= 4) {
131
+ return { isLoop: true, reason: "Аварийное зацикливание хвоста при исчерпании лимита токенов" };
132
+ }
133
+ }
134
+
135
+ return { isLoop: false };
136
+ }
137
+
138
+ /**
139
+ * Валидатор Категории 1: Распознавание смыслового зацикливания (Строгая бинарная оценка)
140
+ */
141
+ function validateCategory1(scenario, rawText) {
142
+ const text = (rawText || "").trim();
143
+ if (!text) {
144
+ return { pass: false, reason: "Пустой ответ от модели" };
145
+ }
146
+
147
+ const pattern = /(?:мой\s+ответ|my\s+answer)\s*:\s*\(\s*([12])\s*\)\s*\(\s*([А-ЯA-Zа-яa-z])\s*\)/i;
148
+ const match = text.match(pattern);
149
+
150
+ if (!match) {
151
+ return {
152
+ pass: false,
153
+ reason: `Не соблюден формат ответа. Ожидалось: 'Мой ответ: (1/2) (Буква)', получено: '${text.slice(0, 60)}...'`
154
+ };
155
+ }
156
+
157
+ const actualBinary = match[1];
158
+ const actualLetterNorm = normalizeScriptChar(match[2]);
159
+
160
+ const expectedBinary = String(scenario.expectedBinary);
161
+ const expectedLetterNorm = normalizeScriptChar(scenario.expectedLetter);
162
+
163
+ // СТРОГАЯ БИНАРНАЯ ПРОВЕРКА: Либо верны оба пункта (100), либо провал (0)
164
+ if (actualBinary !== expectedBinary || actualLetterNorm !== expectedLetterNorm) {
165
+ const verdictName = actualBinary === "1" ? "Да" : "Нет";
166
+ const expVerdictName = expectedBinary === "1" ? "Да" : "Нет";
167
+ return {
168
+ pass: false,
169
+ reason: `Задание не выполнено. Получено: (${actualBinary} - ${verdictName}) (${match[2].toUpperCase()}), ожидалось: (${expectedBinary} - ${expVerdictName}) (${scenario.expectedLetter})`
170
+ };
171
+ }
172
+
173
+ return { pass: true, reason: `Задание выполнено полностью: зацикливание и количество (${expectedBinary}) (${scenario.expectedLetter}) определены верно` };
174
+ }
175
+
176
+ /**
177
+ * Валидатор Категории 2: Массовая генерация 100 элементов
178
+ */
179
+ function validateCategory2(scenario, rawText, finishReason) {
180
+ const text = (rawText || "").trim();
181
+ if (!text) {
182
+ return { pass: false, reason: "Пустой ответ от модели" };
183
+ }
184
+
185
+ const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean);
186
+ const items = [];
187
+
188
+ for (const line of lines) {
189
+ const m = line.match(/^(?:\d+[\.\)]|\-\s*|\*\s*)(.+)$/);
190
+ if (m) {
191
+ items.push(m[1].trim().toLowerCase());
192
+ }
193
+ }
194
+
195
+ const finalItems = items.length >= 40 ? items : lines.map(l => l.toLowerCase());
196
+
197
+ // 1. Повтор одного пункта 4+ раза подряд
198
+ for (let i = 0; i <= finalItems.length - 4; i++) {
199
+ if (
200
+ finalItems[i] === finalItems[i + 1] &&
201
+ finalItems[i] === finalItems[i + 2] &&
202
+ finalItems[i] === finalItems[i + 3]
203
+ ) {
204
+ return {
205
+ pass: false,
206
+ reason: `[LOOP DETECTED] Непрерывный повтор одного пункта 4+ раза подряд: '${finalItems[i].slice(0, 40)}...'`
207
+ };
208
+ }
209
+ }
210
+
211
+ // 2. Чередование пары пунктов ((A B) (A B) (A B))
212
+ for (let i = 0; i <= finalItems.length - 6; i++) {
213
+ if (
214
+ finalItems[i] === finalItems[i + 2] && finalItems[i] === finalItems[i + 4] &&
215
+ finalItems[i + 1] === finalItems[i + 3] && finalItems[i + 1] === finalItems[i + 5]
216
+ ) {
217
+ return {
218
+ pass: false,
219
+ reason: `[LOOP DETECTED] Циклическое чередование пунктов: '${finalItems[i].slice(0, 25)}' / '${finalItems[i + 1].slice(0, 25)}'`
220
+ };
221
+ }
222
+ }
223
+
224
+ // 3. Коллапс разнообразия (> 35% дубликатов по всему списку)
225
+ if (finalItems.length >= 40) {
226
+ const unique = new Set(finalItems);
227
+ const dupRatio = (finalItems.length - unique.size) / finalItems.length;
228
+ if (dupRatio > 0.35) {
229
+ return {
230
+ pass: false,
231
+ reason: `[LOOP DETECTED] Коллапс генератора: ${Math.round(dupRatio * 100)}% пунктов списка являются дубликатами`
232
+ };
233
+ }
234
+ }
235
+
236
+ // 4. Обрыв по лимиту длины
237
+ if (finishReason === "length") {
238
+ return {
239
+ pass: false,
240
+ reason: `Генерация оборвалась по лимиту длины (успела выдать ${finalItems.length} пунктов). Декодер ушел в бесконечную петлю.`
241
+ };
242
+ }
243
+
244
+ // 5. Полнота объема (допуск от 75 пунктов)
245
+ if (finalItems.length < 75) {
246
+ return {
247
+ pass: false,
248
+ reason: `Сгенерировано слишком мало пунктов (${finalItems.length} из 100 требуемых).`
249
+ };
250
+ }
251
+
252
+ return { pass: true, reason: `Успешно: сгенерировано ${finalItems.length} уникальных пунктов без зацикливаний` };
253
+ }
254
+
255
+ /**
256
+ * Валидатор Категории 3: Рекурсивные правила и парадоксы
257
+ */
258
+ function validateCategory3(scenario, rawText, finishReason) {
259
+ const text = (rawText || "").trim();
260
+ const words = text.split(/\s+/).filter(Boolean);
261
+
262
+ if (words.length === 0) {
263
+ return { pass: false, reason: "Отказ генерации: модель не вернул�� текста (зависание или сбой декодера)" };
264
+ }
265
+
266
+ if (words.length < 10) {
267
+ return {
268
+ pass: false,
269
+ reason: `Нарушено условие минимального объема: ответ содержит всего ${words.length} слов (требуется >= 10 слов)`
270
+ };
271
+ }
272
+
273
+ const micro = detectMicroLoop(text, finishReason);
274
+ if (micro.isLoop) {
275
+ return { pass: false, reason: `[LOOP DETECTED] ${micro.reason}` };
276
+ }
277
+
278
+ const macro = detectMacroLoop(text);
279
+ if (macro.isLoop) {
280
+ return { pass: false, reason: `[LOOP DETECTED] ${macro.reason}` };
281
+ }
282
+
283
+ if (finishReason === "length") {
284
+ return { pass: false, reason: "Модель исчерпала лимит токенов в попытке распутать рекурсивный дедлок" };
285
+ }
286
+
287
+ return { pass: true, reason: "Успешно: модель справилась с задачей без впадения в петлю генерации" };
288
+ }
289
+
290
+ function validateResponse(scenario, rawText, finishReason) {
291
+ if (scenario.category === "Определение зацикливания") {
292
+ return validateCategory1(scenario, rawText);
293
+ }
294
+ if (scenario.category === "Массовая генерация (100 элементов)") {
295
+ return validateCategory2(scenario, rawText, finishReason);
296
+ }
297
+ if (scenario.category === "Рекурсивные правила и парадоксы") {
298
+ return validateCategory3(scenario, rawText, finishReason);
299
+ }
300
+ return { pass: false, reason: "Неизвестная категория сценария" };
301
+ }
302
+
303
+ function resolveHelpers(context) {
304
+ if (context && typeof context.getRequiredProvider === "function") {
305
+ return context;
306
+ }
307
+ if (context?.helpers && typeof context.helpers.getRequiredProvider === "function") {
308
+ return context.helpers;
309
+ }
310
+ return {
311
+ getRequiredProvider(providerId) {
312
+ const providers = context?.providers || [];
313
+ const found = Array.isArray(providers)
314
+ ? providers.find((p) => p.id === providerId || p.provider === providerId)
315
+ : providers[providerId];
316
+ return found || { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "none" };
317
+ },
318
+ getInferenceEndpoint(modelId) {
319
+ return context?.endpoints?.[modelId] || null;
320
+ }
321
+ };
322
+ }
323
+
324
+ export async function prepare(context) {
325
+ const helpers = resolveHelpers(context);
326
+
327
+ return {
328
+ async runScenario(input) {
329
+ const allScenarios = loadScenarios();
330
+ const scenario = allScenarios.find(s => s.id === input.scenario.id);
331
+ if (!scenario) {
332
+ return {
333
+ scenarioId: input.scenario.id,
334
+ status: "fail",
335
+ score: 0,
336
+ summary: "Сценарий не найден",
337
+ rawLog: "error=Scenario not found"
338
+ };
339
+ }
340
+
341
+ const provider = helpers.getRequiredProvider(input.model.provider, { enabledOnly: true });
342
+ const inference = helpers.getInferenceEndpoint ? helpers.getInferenceEndpoint(input.model.id) : null;
343
+
344
+ const rawBaseUrl = (inference && inference.status === "running" ? inference.baseUrl : provider?.baseUrl) || "http://127.0.0.1:11434/v1";
345
+ const baseUrl = rawBaseUrl.replace(/\/+$/, "");
346
+ const apiKey = provider?.apiKey || "none";
347
+
348
+ const requestBody = {
349
+ model: input.model.model,
350
+ messages: [{ role: "user", content: scenario.prompt }],
351
+ temperature: 0
352
+ };
353
+
354
+ const response = await fetch(`${baseUrl}/chat/completions`, {
355
+ method: "POST",
356
+ headers: {
357
+ "Content-Type": "application/json",
358
+ Authorization: `Bearer ${apiKey}`
359
+ },
360
+ body: JSON.stringify(requestBody)
361
+ });
362
+
363
+ const data = await response.json();
364
+ const text = (data.choices?.[0]?.message?.content || "").trim();
365
+ const finishReason = data.choices?.[0]?.finish_reason || "stop";
366
+ const result = validateResponse(scenario, text, finishReason);
367
+
368
+ const rawLog = [
369
+ `model=${input.model.model}`,
370
+ `scenario=${scenario.id} ${scenario.name}`,
371
+ `category=${scenario.category}`,
372
+ `finish_reason=${finishReason}`,
373
+ `user_turn_1=${scenario.prompt}`,
374
+ `assistant_turn_1=${text}`,
375
+ `final_answer=${text}`,
376
+ `verdict=${result.pass ? "pass" : "fail"}`,
377
+ `score=${result.pass ? 100 : 0}`,
378
+ `summary=${result.reason}`,
379
+ `\n--- RAW API RESPONSE ---`,
380
+ JSON.stringify(data, null, 2)
381
+ ].join("\n");
382
+
383
+ return {
384
+ scenarioId: scenario.id,
385
+ status: result.pass ? "pass" : "fail",
386
+ score: result.pass ? 100 : 0,
387
+ summary: result.reason,
388
+ rawLog: rawLog
389
+ };
390
+ },
391
+
392
+ async dispose() {}
393
+ };
394
+ }
395
+
396
+ export function scoreModelResults(results) {
397
+ const scored = results || [];
398
+ const total = scored.reduce((sum, r) => sum + (r.score || 0), 0);
399
+ const max = scored.length * 100;
400
+
401
+ const scenarios = loadScenarios();
402
+ const categoryMap = new Map();
403
+
404
+ for (const r of scored) {
405
+ const sc = scenarios.find(s => s.id === r.scenarioId);
406
+ const cat = sc?.category || "Общие";
407
+ if (!categoryMap.has(cat)) {
408
+ categoryMap.set(cat, { total: 0, count: 0 });
409
+ }
410
+ const cur = categoryMap.get(cat);
411
+ cur.total += r.score || 0;
412
+ cur.count += 1;
413
+ }
414
+
415
+ const categories = Array.from(categoryMap.entries()).map(([name, data]) => ({
416
+ name,
417
+ score: data.count > 0 ? Math.round(data.total / data.count) : 0
418
+ }));
419
+
420
+ return {
421
+ totalScore: max > 0 ? Math.round((total / max) * 100) : 0,
422
+ categories
423
+ };
424
+ }
425
+
426
+ export default {
427
+ manifest,
428
+ listScenarios,
429
+ getScenarios,
430
+ prepare,
431
+ createRuntime: prepare,
432
+ scoreModelResults
433
+ };
RLB/scenarios.json ADDED
The diff for this file is too large to render. See raw diff