У заметки есть предыстория: результаты турнира локальных моделей на пяти PHP-задачах - там таблица баллов и выводы, какая модель для чего годится.
Зачем свой прогонщик, если есть готовые бенчмарки
Публичные бенчмарки отвечают на вопрос «насколько модель хороша вообще». Мне нужен ответ на другой вопрос: «справится ли она с моей задачей за приемлемое время». Это разные вещи, и расхождение бывает обидным.
Плюс локальный прогонщик решает три бытовые проблемы:
- Свои задачи. Никто не мерит модели на разборе цен с неразрывными пробелами и деревьях категорий с циклами, а я это пишу каждый день.
- Объективность. Автотесты не устают и не жалеют знакомую модель. Глазами я бы такое не оценил честно.
- Повторяемость. Вышла новая модель - добавил строку в конфиг и прогнал заново. Через полгода можно сравнить результаты на тех же задачах.
Важная оговорка: это не замена большим бенчмаркам и не повод делать глобальные выводы. Это личная линейка под свои задачи. Но именно она мне и нужна.
Что понадобится
- Локальный сервер с любой моделью и OpenAI-совместимым API (у меня llama.cpp на порту 8080).
- Интерпретатор языка, на котором пишете. У меня PHP, поэтому тесты я прогоняю через
phpиз командной строки. - Python 3 для прогонщика (удобно дергать API и разбирать ответы).
- Час времени на сам прогон, если моделей много и они думающие.
Структура прогона
Всё живёт в одной папке, у меня это phpbench:
phpbench/
prompts/ # тексты задач (по файлу на задачу)
tests/ # тесты (по файлу на задачу)
ref/ # эталонные решения, по которым проверяются сами тесты
results/ # ответы моделей, извлечённый код и итоговая таблица
run_bench.sh
Логика простая: прогонщик берёт задачу из prompts, отправляет её модели, вытаскивает из ответа код, проверяет синтаксис и прогоняет через тест из tests. Результат складывает в results. Эталоны из ref нужны, чтобы убедиться: тесты корректны и задача вообще решаема.
Шаг 1. Придумать задачи
Три правила, которые я вывел для себя.
- Задача должна быть проверяемой автоматически. Если результат нельзя сравнить с эталоном, вы снова скатываетесь к оценке на глаз. Формулировка должна предполагать конкретную функцию с конкретной сигнатурой.
- Задача должна повторять реальную практику. Не алгоритм сортировки из учебника, а то, что вы правда пишете: разбор цен, агрегаты по дереву разделов, кэш, чистка пользовательского HTML, вычисление выражения без eval.
- В задаче должны быть крайние случаи. Именно на них видно разницу между моделями. Неразрывные пробелы в цене, цикл в дереве категорий, протухший TTL,
javascript:в ссылке, деление на ноль.
Мои пять задач выглядели так:
parsePriceToRub(string $input): ?array
aggregateCategoryTree(array $categories, array $products): array
LruTtlCache(int $capacity, \Closure $clock)
sanitizeHtml(string $html): string
calcExpression(string $expr): float|int
Сигнатуры я указываю прямо в промпте, иначе модель назовёт функцию по-своему и тест не найдёт её.
Шаг 2. Написать тесты и эталоны
Тест - это обычный скрипт, который получает файл с решением и печатает результат вида «пройдено из скольких». У меня тест на PHP принимает путь к решению первым аргументом:
<?php
require $argv[1];
$pass = 0;
$total = 0;
function chk($cond, $msg) {
global $pass, $total;
$total++;
if ($cond) { $pass++; } else { echo " FAIL: $msg\n"; }
}
chk(parsePriceToRub('1 234,56 руб.')['amount'] === 1234.56, 'неразрывный пробел');
chk(parsePriceToRub('1234') === null, 'без валюты должен быть null');
// ... остальные проверки
echo "PASS $pass/$total\n";
Эталонные решения обязательны. Сначала я написал тесты, потом сам решил задачи и прогнал их через тесты. И знаете что? Три эталона сначала не прошли свои же тесты. Где-то в тесте было неверное ожидание, где-то в эталоне баг. Это нормальный этап, и пропускать его нельзя: если эталон не даёт сто процентов, тесты врут, а вы потом делаете неверные выводы о моделях.
Ещё момент: не жалейте проверок. У меня на пять задач получилось 62 проверки. Чем больше крайних случаев, тем меньше шансов, что модель пройдёт тест случайно.
Шаг 3. Собрать прогонщик
Прогонщик делает по каждой модели одно и то же. У меня это bash, который дергает Python для запросов к API.
for model in "${MODELS[@]}"; do
# 1. остановить предыдущую и запустить нужную
llm.sh stop; llm.sh start "$model"
# 2. дождаться готовности сервера
until curl -s http://127.0.0.1:8080/health | grep -q ok; do sleep 4; done
# 3. по каждой задаче: запрос, извлечение кода, тесты
for task in 1 2 3 4 5; do
python3 ask.py "$model" "$task" # сохраняет ответ и код
php "tests/test$task.php" "results/$model/task$task.php"
done
done
Внутри ask.py три вещи: запрос к API, извлечение кода из ответа и запись метрик.
body = {
'messages': [{'role': 'user', 'content': prompt}],
'max_tokens': 8000,
'temperature': 0.2,
}
# ... запрос ...
content = data['choices'][0]['message']['content']
code = re.search(r'```(?:php)?\s*(.*?)```', content, re.S)
if not code:
# блок не закрыт - берём всё после открывающего ```
code = re.search(r'```(?:php)?\s*(.*)$', content, re.S)
Обратите внимание на второй шаблон: если модель не закрыла блок кода, нельзя терять почти готовое решение. Такое случается, когда модель упирается в лимит токенов.
Шаг 4. Считать метрики, а не только баллы
Баллы без времени обманчивы. У меня модель, которая выиграла турнир по качеству, работала в пять раз медленнее остальных и потратила на пять задач 36 минут. Для ежедневной работы это неприемлемо, и никакая таблица баллов этого не покажет, если не записывать время.
Поэтому в итоговую строку я пишу: модель, задачу, скорость генерации, общее время, количество токенов, результат теста. Из этих строк уже собирается сводная таблица.
model task tg_tps wall_s tokens result
Tiel-Coder-35B-A3B-Q8 1 41.0 68.4 354 PASS 15/15
Грабли, на которые я наступил
- Модели возвращают код без открывающего тега. Почти все ответили функцией без
<?php. Если такой файл отдать PHP, он считается обычным HTML, функция не определяется, и все тесты падают. Прогонщик должен сам добавлять тег, если его нет. - Думающие модели съедают лимит токенов на размышления. На сложных задачах блок размышлений раздувается так, что на код места не остаётся: ответ приходит пустым. Я сначала поставил 3000 токенов и получил нули у всех моделей, потом поднял до 8000 и только тогда увидел реальные результаты. Если модель можно запустить без размышлений, стоит прогнать и так.
- Тесты нужно запускать с таймаутом. Одна модель сгенерировала парсер, который зациклился на части выражений. Без таймаута прогон просто зависает, и вы не понимаете, что происходит. Обязательно ловите это и записывайте как провал, а не как зависший процесс.
- Проверяйте тесты на эталонах, прежде чем винить модели. Половина моих первых «нулевых» результатов оказалась не виной моделей, а багами в прогонщике и тестах.
- Держите одинаковые условия для всех. Одинаковый промпт, одинаковая температура, одинаковый лимит токенов, одинаковый контекст. Иначе сравнение превращается в лотерею.
- Помните, что именно вы измерили. Мои задачи одиночные, без инструментов и многошаговых циклов. Модель, которая слабо решает их, может раскрыться в агентском режиме, и наоборот. Это не повод для глобальных выводов, это повод для следующего теста.
Что показывает такая таблица
Практическая польза не в «кто первый», а в понимании профилей. У меня по итогам вышло так:
- одна модель лучшая по качеству (92 процента), но настолько медленная, что годится только для сложных задач;
- две модели дают 81-82 процента при скорости около 40 токенов в секунду, и это лучший баланс для ежедневной работы;
- маленькая модель оказалась лучшей в одной конкретной задаче (чистка HTML) и худшей в другой;
- расцензуренные версии в среднем теряют около трети баллов на алгоритмических задачах.
Ни один публичный бенчмарк мне такого не рассказал. А тут я просто открываю таблицу и вижу, кого звать на какую работу.
Итог
Свой прогонщик - это не про спортивную честность, а про практическую пользу. Задачи из моей работы, проверка автотестами, скорость рядом с баллами. Полдня на сборку и потом годы пользы: вышла новая модель, добавил строку в конфиг, прогнал, сравнил с прошлыми результатами.
Если тема зайдёт, в следующей заметке разберу, как измерять модели в агентском режиме - с инструментами, файлами и многошаговыми задачами. Там уже понадобится другая методика: считать не только правильность ответа, но и число шагов, стоимость и умение не сломать то, что уже работает.
С чего всё началось и что из этого получилось - в отдельной заметке Турнир локальных LLM: 5 сложных PHP-задач, 9 моделей, 62 проверки. Там сводная таблица по всем моделям, разбор кто в чём силён и выводы, кого звать на какую работу. А здесь - инструмент, которым это собрано. Сделаете свой набор задач, и сможете так же проверять любую новую модель на своих условиях.
И да: чужие бенчмарки читать всё равно полезно. Просто своя линейка отвечает на ваш вопрос, а чужая - на чей-то ещё.