На YouTube очень часто мне попадаются видео, где сравнивают разные LLM для вайбкодеров: сделай 3D-аквариум с рыбками, напиши игру, слепи лендинг. Но по ним совсем непонятно, как эти модели поведут себя на существующем проекте с большой кодовой базой и можно ли их применять для ежедневных задач веб-разработчика.
Уже давно пользуюсь Claude Code, и он, в принципе, закрывает все потребности. Но при активной разработке лимиты кончаются довольно быстро... Кроме того, есть риск блокировок, поэтому я всё чаще посматривал в сторону локальных LLM. Было интересно, когда же настанет момент, что их реально можно будет использовать в повседневной разработке.
К сожалению, все модели, которые получалось запустить на своих 16 ГБ VRAM, не внушали оптимизма. Да, их можно использовать для суммаризации текста или написания короткого фрагмента кода, но до агентского кодинга им было очень далеко. И вот мне выдалась возможность поэкспериментировать с DGX Spark, который имеет 128 ГБ унифицированной памяти. Результатами этого эксперимента и хочу поделиться.
В качестве агента для локальных моделей выбрал Hermes — его имя постоянно мелькало в обсуждениях, и многие его хвалили, так что решил попробовать именно его. Модели крутились через vLLM на DGX Spark: весь процесс — чтение кода, поиск нужных мест, внесение правок, запуск тестов и статического анализа — выполнялся самим агентом, без участия человека (но иногда нужно было написать «продолжай», когда работа прерывалась по каким-то причинам).
Тестировал всё на реальных задачах из своего проекта на Symfony. Результатами первых прогонов поделился с коллегами, и им стало интересно, могут ли локальные модели конкурировать с детищем Сбера — тем более что GigaChat недавно обзавёлся агентным режимом. Так в сравнение добавился GigaCode (использовал его внутри GigaIDE) — облачный агент, который на момент написания статьи бесплатен.
Чтобы было от чего оттолкнуться, добавил в качестве контроля Claude Opus 5 (он не участвует в зачёте и служит ориентиром того, как выглядит работа топового облачного агента). Заодно Opus использовался как независимый ревьюер: каждый коммит прогонялся через функциональные тесты, PHPStan и CS Fixer, и по итогам писался вердикт — эти вердикты легли в основу оценок в статье.
Инфраструктурная деталь, которая тоже стала частью эксперимента: проект поднимается локально через docker compose внутри WSL. Hermes и Claude Code установлены прямо в WSL и работают с Docker без проблем, а вот GigaCode живёт на хосте Windows — из-за этого он не мог напрямую обращаться к контейнерам, пришлось писать и вручную скармливать отдельный навык (exec-in-docker), чтобы он вообще мог себя проверить. Без этого навыка GigaCode в первых раундах просто гадал формат команд (падал на попытке экранировать кавычки) и сдавался без самопроверки.
Участники:
Модель |
Полное имя |
Архитектура |
Далее в тексте |
|---|---|---|---|
Qwen 3.6 27B |
|
Dense |
Qwen 27B |
Qwen 3.6 35B |
|
MoE |
Qwen 35B |
Ornith 1.0 35B |
|
MoE |
Ornith |
Laguna S 2.1 118B A8.5B |
|
MoE |
Laguna |
GigaCode |
(облако, бесплатен на момент написания) |
— |
GigaCode |
Claude Opus 5 |
(контроль, вне зачёта) |
— |
Opus |
Qwen 27B сошёл с дистанции после первого же раунда: почти 2 часа на одну задачу, зависание на месте, нерабочий результат — дальше в раундах он не встречается.
Некоторым моделям в раундах 3 и 4 давался «второй шанс» — тот же коммит с усиленным промптом: явное требование написать тесты, прогнать docker compose exec php-fpm php vendor/bin/phpunit, PHPStan и CS Fixer и поправить замечания. В статье это отражено подсекцией внутри соответствующего раунда.
Как считаем баллы
Чтобы не сравнивать модели «на глаз», по итогам каждого раунда каждому рабочему решению начисляются баллы по пяти критериям.
Критерий |
Баллы |
Что оценивается |
|---|---|---|
Работоспособность (Р) |
0–5 |
5 — работает полностью и по всем сценариям ТЗ; 4 — работает, но с незначительными оговорками/рисками; 3 — работает частично (одна из точек входа/сценариев сломана); 2 — работает условно (баг проявляется не всегда); 1 — не работает, но чинится тривиально (один символ/одна строка/один импорт); 0 — не работает, требует существенной доработки |
Статический анализ (Ст) |
0–2 |
2 — чисто по PHPStan и CS Fixer; 1 — есть замечания; 0 — очень много или не проверял |
Тесты (Т) |
0–2 |
2 — есть тесты, покрывающие в т.ч. негативные сценарии; 1 — тесты есть, но слабые/флейковые; 0 — нет тестов |
Соответствие ТЗ и архитектурная чистота (Арх) |
0–2 |
2 — решение точное, минимальный diff, без лишних правок в общих конфигах, в стиле проекта; 1 — есть недочёты (дублирование, лишние правки, отступления от ТЗ); 0 — существенные архитектурные проблемы |
Скорость (Ск) |
0–2 |
2 — быстро относительно других участников раунда; 1 — среднее время; 0 — заметно дольше всех при сопоставимом результате |
Максимум — 13 баллов за раунд.
Немного о проекте и его терминологии
Задачи выполнялись на кодовой базе проекта overseer.observer, сервиса по мониторингу сайтов: проверка успешности HTTP-ответов, сроков действия SSL-сертификатов и доменов, а также многое другое.
Ниже термины, которые встречаются в задачах агентам:
Монитор - сущность, содержащая настройки для периодической проверки определенного URL
Локация - сущность с информацией о remote-сервере, на котором работает приложение, отправляющее http-запросы на указанный в настройках монитора URL
Инцидент - когда на http-запрос получен неуспешный ответ (4xx, 5xx или сетевые проблемы), то создаётся инцидент, который записывается в БД, и о котором уведомляется пользователь, который создал монитор.
Раунд 1: эндпоинт со списком IP для ботов
Задача: добавить эндпоинт /bot-ip-list.txt, отдающий список IP опубликованных локаций (по одному на строку, Content-Type: text/plain), с кэшированием в Redis на 5 минут через LocationRepository.
На вид — тривиальная CRUD-задача на час. Но в проекте уже был настроен пул redis_cache с default_lifetime: 300 и готовый именованный автовайринг CacheInterface $redisCache — то есть правильное решение укладывалось в 2-3 файла без единой правки конфигов. Те модели, которые этого не поняли и начали редактировать services.yaml или security.yaml, потеряли очки.
Как справились участники
Qwen 27B — не работает. Полтора часа на задачу (в какой-то момент завис, пришлось командовать «продолжай»), а в итоге лишняя явная регистрация контроллера в services.yaml перекрыла автоматическую и убила тег controller.service_arguments — контроллер стал приватным и не вызывается вовсе.
Qwen 35B — не работает. Импортировал несуществующий Symfony\Component\Cache\CacheInterface вместо Symfony\Contracts\Cache\CacheInterface — приложение падает на старте с Cannot resolve argument.
Ornith — не работает, хотя решение было почти правильным (кэш через колбэк, чистая выборка одной колонки). Подвела одна деталь — DQL не понимает двойные кавычки:
->andWhere('l.ip != ""') // DQL: Expected Literal, got '"' // нужно было: ->andWhere("l.ip != ''")
Laguna — работает. Только в этом решении реализована инвалидация кэша при изменении локации (через отдельный LocationCacheInvalidator).
Но модель долго не могла понять, что автовайринг $redisCache уже настроен, и в итоге сделала ручную инъекцию через services.yaml. Ещё минус: инвалидация кэша неполная — слушатель ловит изменение и удаление локации, но не создание, поэтому надо ждать, пока новая локация появится в списке до 5 минут.
GigaCode — формально работает (200, TTL подтверждён redis-cli TTL), но с побочным эффектом: попутно раскомментировала строку в config/packages/cache.yaml, переключив бэкенд cache.app на Redis для всего приложения — а через него на проде живёт кэш результатов Doctrine. Никто не просил трогать этот конфиг — это незаметная инфраструктурная правка внутри фичи. Плюс сам TTL нигде явно не задан — 5 минут работают только потому, что таков дефолт пула; поменяют дефолт — кэш станет вечным, и ни один тест этого не заметит.
Opus — работает, diff выглядит компактно, и только в этой реализации написан функциональный тест.
Баллы за раунд
Модель |
Р |
Ст |
Т |
Арх |
Ск |
Итого |
|---|---|---|---|---|---|---|
Opus (контроль) |
5 |
2 |
2 |
2 |
2 |
13 |
Laguna |
4 |
2 |
0 |
1 |
1 |
8 |
GigaCode |
4 |
2 |
0 |
0 |
1 |
7 |
Ornith |
1 |
1 |
0 |
1 |
2 |
5 |
Qwen 35B |
1 |
0 |
0 |
0 |
2 |
3 |
Qwen 27B |
1 |
0 |
0 |
0 |
0 |
1 |
Пояснения к погранично низким баллам:
Ornith получила 1 за работоспособность (фикс — замена двойных кавычек на одинарные в DQL, буквально один символ), но статанализ и скорость (5 мин 35 сек — быстрее всех локальных моделей) вытащили её выше Qwen 35B.
Qwen 35B тоже «однострочный» фикс (неверный импорт
CacheInterface), но 7 ошибок PHPStan и общая небрежность (лишняя обёртка массивом, Telegram в названии контроллера не по смыслу) держат его ниже Ornith.Qwen 27B технически тоже «фикс одним блоком» (убрать лишнюю запись из
services.yaml), но полтора часа на нерабочий результат.
Итог раунда
Рабочий результат из конкурсантов дали только Laguna и GigaCode. Но Laguna переусложнила решение лишними абстракциями, а GigaCode тихо поменял топологию кэша на проде. Остальные три модели (Qwen 27B, Qwen 35B, Ornith) сломались на вещах, которые чинятся одной строкой каждая, но ни одна не запустила собственный код перед тем, как его сдать. Opus выполнил задачу без сюрпризов.
Раунд 2: правка ключа кэша с ловушкой в контроллере
Задача: в IncidentTypeStatsCommand ключ Redis строится по времени начала интервала — нужно сделать так, чтобы в имени ключа было время конца интервала.
Задание сформулировано как правка одной строки, но buildKey() — публичный статический метод с четырьмя потребителями: сама команда (пишет), контроллер IncidentsTypeStatsController (читает), и два тестовых класса. Тронешь только команду — и контроллер продолжит запрашивать по старой схеме: каждая точка графика тихо съедет на 5 минут назад, без единой ошибки в логах. Отдельная ловушка — функциональный тест сеет данные тем же buildKey(), которым пользуется контроллер, поэтому согласованный сдвиг взаимно компенсируется и тест остаётся зелёным, даже если вся конструкция сломана.
Как справились участники
Ornith и GigaCode получили идентичный diff — тронули только команду, контроллер не заметили. Формально, это тот самый прод-баг: тест команды IncidentTypeStatsCommandTest падает (2 failures). Но более широкий функциональный тест остаётся зелёным именно из-за компенсирующего сдвига, описанного выше. GigaCode сделала это за 40 секунд, Ornith — за 1 мин 20 сек; ни одна не запускала тесты перед сдачей.
Qwen 35B — модель прошла весь периметр (команда + контроллер + оба теста), но споткнулась на стиле: в одной строке использовала \array_map( с обратным слэшем, двумя строками ниже — array_map без него. CS Fixer это ловит, CI красный. Плюс захардкодила '+5 minutes' в контроллере вместо использования готовой константы INTERVAL_MINUTES.
Laguna — единственное конкурсное решение, зелёное по всем проверкам (оба теста, PHPStan, CS Fixer). Использует константу INTERVAL_MINUTES вместо хардкода. Но тестовую часть переписала так, что тест теперь повторяет ту же арифметику, что и продакшн-код (from(...)->modify('+5 minutes') вместо явного литерала времени) — по сути, тест перестаёт быть независимой проверкой. На это ушло 9 минут против 4 минут у Qwen 35B при сопоставимом по содержанию diff.
Opus (контроль) пошёл другим путём — не менял сигнатуру buildKey(), а сдвинул время внутри самого метода. За счёт этого контроллер и функциональный тест вообще не пришлось трогать — у всех вызывающих на входе уже было начало интервала. Только этот агент добавил тест, который проверяет не согласованность метода с самим собой, а конкретную строку ключа:
self::assertSame( IncidentTypeStatsCommand::KEY_PREFIX.'2026-08-01T12:05', $key );
Самый короткий diff в раунде (3 строки в src/) и самое быстрое решение (1 мин 58 сек).
Баллы за раунд
Модель |
Р |
Ст |
Т |
Арх |
Ск |
Итого |
|---|---|---|---|---|---|---|
Opus (контроль) |
5 |
2 |
2 |
2 |
2 |
13 |
Laguna |
5 |
2 |
1 |
1 |
0 |
9 |
Qwen 35B |
4 |
1 |
1 |
1 |
1 |
8 |
Ornith |
2 |
2 |
0 |
0 |
2 |
6 |
GigaCode |
2 |
2 |
0 |
0 |
2 |
6 |
Итог раунда
Этот раунд для меня ещё раз показал простую вещь: прежде чем менять публичный метод, надо найти всех его потребителей. Этого принципа придерживались Qwen 35B, Laguna и — по-своему, за счёт более элегантного решения — Opus. Ornith и GigaCode этот шаг пропустили полностью и получили тихий прод-баг вместо ошибки — то есть решение хуже, чем если бы оно просто не запустилось. Laguna забрала первое место среди конкурсантов за счёт полностью зелёного CI, но заплатила за это 9 минутами против 4 минут у Qwen 35B, которую подвёл один лишний обратный слэш.
Раунд 3: команда генерации инцидентов (+ «второй шанс» с усиленным промптом)
Задача: Создай Symfony-команду, которая берёт из БД любой монитор и генерирует по нему от 0 до 3 инцидентов на каждый тип (верхняя граница переопределяется флагом). Если мониторов нет — создать монитор с минимальным набором данных, если нет пользователей — создать пользователя.
Задача кажется простой, но в ней спрятаны две ветки с побочными эффектами (создание пользователя с паролем, создание монитора) и один параметр с неочевидной семантикой (--max-per-type=0).
После первого прогона трём моделям, которые не смогли реализовать рабочий вариант, (Ornith, GigaCode, Qwen 35B) была дана вторая попытка: усиленный промпт с явным требованием написать функциональный тест на три сценария (пустая БД / есть пользователь без монитора / есть и то, и другое), прогнать phpunit, CS Fixer и PHPStan и поправить всё, что они найдут. Была гипотеза, что эти LLM могут написать рабочее решение, если указать им явно перепроверять свой результат.
Первая попытка
Qwen 35B — команда в целом работает и заполняет данные реалистичнее остальных (текст ошибки через match по типу инцидента, resolved/resolvedAt), но 13 ошибок PHPStan (корень — getOneOrNullResult() без явной типизации там, где остальные обошлись findOneBy([])) и красный CS Fixer. Плюс два отклонения от ТЗ: ищет монитор конкретного пользователя вместо «любого», и флаг --max-per-type=0 на самом деле означает «случайно от 0 до 3», а не «ноль» — задать буквальный ноль невозможно.
Ornith — самое короткое и быстрое решение (110 строк, 4 мин 56 сек), но с критическим багом: метод создания пользователя вызывает setPlainPassword(), а хэширование пароля нигде не происходит — падает по NOT NULL constraint прямо на той ветке, ради которой писался метод:
$user->setPlainPassword('password'); $this->em->persist($user); // password остаётся NULL → падение в БД
Второй баг связан с выводом количества созданных инцидентов в консоль. Переменная $createdIncidents объявлена внутри foreach, поэтому в консоли выводится только количество инцидентов последнего типа, а не всех.
foreach ($types as $type) { $incidentCount = random_int(0, $maxIncidents); $createdIncidents = []; for ($i = 0; $i < $incidentCount; $i++) { //... $createdIncidents[] = $incident; } } //... $output->writeln(sprintf('Всего создано %d инцидентов', count($createdIncidents)));
Laguna — модель выдала решение с первой попытки с полностью зелёным CI (PHPStan 0, CS Fixer 0), корректно обрабатывает пустую БД и --max-per-type=0. Прогнала PHPStan/CS Fixer и сделала ad-hoc проверку сама, без запроса. Но самое большое время выполнения в раунде: 22 минуты 30 секунд, 6 зависимостей в конструкторе (два явно лишних), flush() внутри цикла вместо одного в конце.
GigaCode — не запускается вообще: IncidentType::values() вместо IncidentType::cases() роняет команду TypeError'ом на первой же итерации. PHPStan предупреждал об этом заранее — значит, ни статику, ни саму команду перед сдачей не запускали.
Opus (контроль) — работает, самое быстрое решение (4 мин 16 сек) и самое короткое при полном функционале: переиспользует существующий UserManager::setEncodedPassword() вместо ручного хэширования, один flush() на весь прогон, вывод таблицей с распределением по типам. Тесты не писал.
Второй шанс (усиленный промпт)
Qwen 35B (усиленный промпт) — потратила более двух часов и зациклилась, пытаясь исправить замечания PHPStan, результат не сдан.
Ornith (усиленный промпт) — критический баг с паролем и мёртвый счётчик исправлены, добавлены тесты — модель реально нашла тестовую инфраструктуру проекта (BaseKernelTestCase, CreatorTrait). Но PHPStan и CS Fixer остались красными (проверялся, похоже, только src/, а не tests/), а сам тест — флейковый (flaky test): ассерт на Http тип инцидента падает в трети прогонов, т.к. могло быть не создано инцидентов для этого типа:
$incidents = $this->getEntityManager() ->getRepository(Incident::class) ->findBy([]); //... /** @var array<string, int> $incidentByType */ $incidentByType = []; foreach ($incidents as $incident) { $incidentByType[$incident->getType()->name] = ($incidentByType[$incident->getType()->name] ?? 0) + 1; } // ассерт зависит от random_int(0, 2) — типа Http может не оказаться вовсе self::assertArrayHasKey('Http', $incidentByType);
Плюс расхождение с ТЗ выросло: было «любой монитор», стало findAll() — выборка сразу всех мониторов! Вторая попытка потребовала в 2 раза больше времени.
GigaCode (усиленный промпт) — со второй попытки добрался до полностью зелёного состояния: TypeError исправлен, PHPStan 0 ошибок, CS Fixer 0 нарушений, 3 стабильных теста (ассерты на count > 0, а не на конкретный тип — методологически надёжнее, чем у Ornith). Остались следы прежней реализации: рудимент старого бага (enum → string → enum конверсия там, где раньше был баг с типом), для выборки мониторов, как и Ornith, использовал findAll() вместо findOneBy([]), и то же расхождение с ТЗ — генерация по всем мониторам вместо одного. 26 минут — самое долгое решение за оба прогона раунда.
Баллы за раунд
Модель |
Р |
Ст |
Т |
Арх |
Ск |
Итого |
|---|---|---|---|---|---|---|
Opus (контроль) |
5 |
2 |
0 |
2 |
2 |
11 |
GigaCode (усиленный промпт) |
4 |
2 |
2 |
1 |
0 |
9 |
Laguna |
5 |
2 |
0 |
1 |
0 |
8 |
Qwen 35B |
4 |
0 |
0 |
0 |
1 |
5 |
Ornith |
2 |
1 |
0 |
0 |
2 |
5 |
Ornith (усиленный промпт) |
3 |
0 |
1 |
0 |
1 |
5 |
GigaCode |
1 |
1 |
0 |
1 |
0 |
3 |
Qwen 35B (усиленный промпт) |
0 |
0 |
0 |
0 |
0 |
0 |
Итог раунда
Первая попытка ещё раз показала прежний паттерн: между скоростью и корректностью наблюдается обратная зависимость — Ornith и GigaCode, потратив меньше времени (5 и 8 минут соответственно), сдали нерабочий код, а Laguna добилась зелёного CI, но потратила 22 минуты.
Явное указание при втором шансе «пиши тесты» сработало у всех моделей — они действительно нашли тестовую инфраструктуру проекта, а не угадывали. А вот явное «проверяй PHPStan и CS Fixer» сработало только у GigaCode — Ornith отчиталась о прохождении статического анализа, забыв прогнать его по папке tests/. При этом модели, получив вторую попытку, заодно расширили область работы за пределы ТЗ (генерация по всем мониторам вместо одного). Модель Qwen 35B на усиленном промпте зациклилась и не сдала ничего — вторая попытка вышла хуже первой.
Раунд 4: триггер внеочередной проверки при смене локаций монитора
Задача: Если у монитора меняется список локаций — при одиночном и массовом редактировании — нужно триггерить внеочередную проверку, не дожидаясь расписания. Аналогично тому, как это уже происходит при создании монитора.
Формулировка опять звучит просто, но у задачи две точки входа с разной механикой детекта (save() и editBatch()), и порядок «диспатч vs flush vs валидация» решает, будет ли внеочередная проверка запущена на данных, которые в итоге не сохранятся.
Диспатч (dispatch) — отправка события в шину событий, на которое завязана внеочередная проверка ответа URL в настройках монитора
Как и в раунде 3, трём моделям, которые опять провалились из-за отсутствия самопроверки (Qwen 35B, Ornith, GigaCode), дали вторую попытку с усиленным промптом: явное требование написать тесты на весь добавляемый функционал, прогнать phpunit, CS Fixer и PHPStan и исправить замечания.
Первая попытка
Qwen 35B — самый большой diff в раунде и ровно половина задачи не работает: массовое редактирование не триггерит проверку вовсе. Причина — на первый взгляд рабочий код, который молча ничего не делает из-за особенности PHP:
$callback = function (Monitor $monitor) use ($dto, &$changedMonitorIds): void { $this->applyBatchEditDto($monitor, $dto, fn () => $changedMonitorIds[] = $monitor->getId()); };
Стрелочная функция fn () захватывает $changedMonitorIds по значению, несмотря на use (&...) во внешнем замыкании — записывает в собственную копию массива, которая после завершения callback больше недоступна. Внешний массив всегда пуст, диспатча нет. PHPStan это не ловит — только реальный запуск.
Плюс отдельная проблема: новое публичное поле BatchResult::$successIds случайно утекло в сериализованный ответ API — внутренняя техническая деталь стала частью публичного контракта.
Ornith — самый компактный рабочий diff раунда (11 строк): не делает отдельную проверку смены локаций, а вешается на уже существующий в коде детект. Обе точки входа триггерят проверку, чисто по PHPStan и CS Fixer. Но диспатч (триггер внеочередной проверки) стоит до flush() и до валидации — если в батче провалится проверка тарифного лимита на количество локаций, изменения откатятся, а высокоприоритетная проверка уже уйдёт в очередь с данными, которых в БД не будет.
Laguna — реализовала решение с полностью корректной логикой на обеих точках входа и на пути ошибки (провал валидации не даёт диспатча), и только она написала тесты с использованием тестового класса шины TestMessageBus, чтобы проверить реальное содержимое сообщения в шине. Выполнено за 55 минут.
GigaCode — худшее решение раунда: массовая правка физически не выполняется, потому что блок диспатча оказался написан после return:
return $this->changeBatchHandler(/* ... */); if ($monitorsWithLocationsChanged !== []) { // сюда никогда не попадём foreach ($monitorsWithLocationsChanged as $monitor) { /* dispatch */ } }
PHPStan ловит это одной строкой (Unreachable statement) — то есть решение не прогонялось даже статикой перед сдачей. Плюс флаг состояния хранится в приватном поле сервиса-синглтона, а значит переживает между вызовами и рискует утечь на чужой монитор.
Opus (контроль) — работает на обеих точках входа, только в этой реализации был добавлен троттлинг (чтобы нельзя было чаще одного раза в 60 секунд триггерить внеочередную проверку) и учтено, что внеочередная проверка не должна происходить на приостановленном мониторе. Выполнил за 13 мин 25 сек.
Второй шанс (усиленный промпт)
Qwen 35B (усиленный промпт) — снова зациклилась на 2 часа, пытаясь починить тесты, результат не сдан.
GigaCode (усиленный промпт) — качественный скачок: вместо прямого вызова диспетчера завела доменное событие, зеркальное существующему сценарию создания монитора —
class MonitorAfterLocationsChangedEvent extends AbstractMonitorEvent {} // MonitorManager: два dispatch(new MonitorAfterLocationsChangedEvent($monitor)) // в уже существующих точках детекта — и всё, остальное берёт на себя подписчик
Лучший архитектурный ответ на формулировку «сделать как при создании» из всех попыток раунда. PHPStan — 0 ошибок по всему проекту, CS Fixer чисто, 8 тестов (лучшее покрытие раунда, с негативными сценариями на обе точки входа). Но остался дефект — тот же, что в первой попытке Ornith: диспатч до flush() и до валидации. Время выполнения увеличилось до 55 минут (26 минут на первую).
Ornith (усиленный промпт) — вторая попытка вышла хуже первой, причём заметно. Работа не доведена до конца (кончился контекст), но и то, что зафиксировано, не работает: собственный класс события не хранит монитор и падает фатальной ошибкой при вызове $event->getMonitor(), а в save() условие смены локаций построено на sort(), который сортирует массив по ссылке и возвращает bool, — сравнение sort($a) !== sort($b) всегда false:
if ([] !== $newLocationIds && sort($newLocationIds) !== sort($oldLocationIds)) { // true !== true — сюда никогда не попадём }
154 ошибки PHPStan (120 — в собственных тестах), 3 дублирующих друг друга файла тестов, из которых ни один не проходит (7 errors + 8 failures). Первая попытка Ornith была лучшей по экономике решением раунда; расширенный промпт вывел модель за пределы бюджета контекста и превратил рабочий минимум в нерабочий максимум. Контекст забился из-за циклических попыток исправить тесты/PHPStan - та же проблема, что и у Qwen 35B.
Баллы за раунд
Модель |
Р |
Ст |
Т |
Арх |
Ск |
Итого |
|---|---|---|---|---|---|---|
Opus (контроль) |
5 |
1 |
2 |
1 |
2 |
11 |
Laguna |
5 |
2 |
2 |
1 |
0 |
10 |
GigaCode (усиленный промпт) |
4 |
2 |
2 |
2 |
0 |
10 |
Ornith |
2 |
2 |
0 |
2 |
2 |
8 |
Qwen 35B |
0 |
1 |
0 |
0 |
0 |
1 |
GigaCode |
0 |
0 |
0 |
0 |
0 |
0 |
Ornith (усиленный промпт) |
0 |
0 |
0 |
0 |
0 |
0 |
Qwen 35B (усиленный промпт) |
0 |
0 |
0 |
0 |
0 |
0 |
Итог раунда
Laguna и Ornith в первой попытке нашли одну и ту же существующую точку детекта в коде — но Laguna довела дело до конца по всем осям (включая путь ошибки и тесты), но затратила 55 минут, а Ornith сделала это за 8 минут, но с одним дефектом (порядок диспатча). GigaCode и Qwen в первой попытке не нашли рабочего решения вовсе — по разным причинам: GigaCode фактически не выполнила половину задачи (код после return), Qwen — из-за тонкости семантики PHP-замыканий, которую в данном случае не поймал PHPStan.
Второй шанс с усиленным промптом дал интересный результат. GigaCode прошёл путь от «не проходит обязательный PHPStan» до лучшего архитектурного решения и лучшего покрытия тестами в раунде. Ornith, наоборот, деградировала: расширенное задание («правка + тесты + прогон анализаторов») превысило её практический бюджет контекста (хотя размер контекста всем моделям выставлялся одинаковый — 262 144 токена).
Заключение
Общий зачёт (по лучшей из попыток в каждом раунде)
Место |
Участник |
Раунд 1 |
Раунд 2 |
Раунд 3 |
Раунд 4 |
Сумма |
|---|---|---|---|---|---|---|
— |
Opus 5 (контроль, вне зачёта) |
13 |
13 |
11 |
11 |
48 |
1 |
Laguna S 2.1 |
8 |
9 |
8 |
10 |
35 |
2 |
GigaCode |
7 |
6 |
9 |
10 |
32 |
3 |
Ornith 35B |
5 |
6 |
5 |
8 |
24 |
4 |
Qwen 3.6 35B |
3 |
8 |
5 |
1 |
17 |
Qwen 27B выбыл после раунда 1 — по чистой неэффективности железа (очень медленно работают Dense-модели на DGX Spark): полтора часа на задачу, которая у остальных занимала минуты.
Победитель — Laguna S 2.1 118B A8.5B
Разрыв с GigaCode на бумаге небольшой — 35 против 32 — но за этими цифрами разная история. Laguna получала свой результат с первой попытки в каждом раунде: не самая быстрая, но стабильно рабочая, с корректной обработкой ошибочных сценариев. Также эта модель без подсказки писала осмысленные тесты уже в базовом прогоне.
GigaCode набрал сопоставимую сумму, но два из четырёх лучших результата (раунды 3 и 4) получены только после усиленного промпта с явным требованием писать тесты и прогонять статический анализ.
Серебро — GigaCode, с оговоркой
Как самостоятельный агент без дополнительных указаний GigaCode оказался наименее самостоятельным из четырёх: не понимал, что проект живёт в Docker и как это проверить (раунд 3), не находил навык (skill) для проверки без ручного вмешательства, несколько раз останавливался и требовал команды «продолжай». Но именно он лучше всех реагировал на явное усиление промпта — в раунде 4 самостоятельно пришёл к тому же архитектурному решению (доменное событие вместо прямого вызова диспетчера), что уже использовалось в проекте для похожего сценария, и обошёл по числу тестов всех участников раунда, включая Opus. В общем, GigaCode еще есть куда расти, но не забываем, что он бесплатный (по крайней мере, пока).
Бронза — Ornith 35B: быстро, но нестабильно
Ornith была стабильно самой быстрой и самой дешёвой по токенам локальной моделью — и стабильно спотыкалась на одной существенной детали за раунд (незахэшированный пароль, диспатч до flush(), DQL с двойными кавычками). Интересно повела себя на усиленном промпте: в раунде 3 доработка помогла (из «не работает» в «работает, но красный CI»), а в раунде 4 расширенное задание вывело модель за пределы практического бюджета контекста, и рабочий 11-строчный diff превратился в 935 строк нерабочих тестов. Модель хорошо работает в границах маленькой конкретной правки и заметно хуже, когда задание разрастается. Не может эффективно исправлять свой же код.
Аутсайдер — Qwen 3.6 35B
Единственная модель без единого раунда с уверенно рабочим результатом с первой попытки (кроме раунда 2, где ошиблась в одном символе). Больше остальных страдала от невнимательности к деталям ТЗ (поиск монитора не у того пользователя, сломанная семантика флага --max-per-type=0, случайно утёкший в API приватный флаг), а на усиленном промпте в обоих раундах, где он предлагался, зацикливалась на 2 часа без результата.
Что в итоге про self-hosted LLM для повседневной веб-разработки
Итак, главный вопрос: можно ли в 2026 году закрывать реальные задачи без платных облачных агентов? Ответ — уже почти, но с оговорками.
Laguna (на DGX Spark) и GigaCode выдавали рабочий код на реальных Symfony-задачах, но ни одна модель не приблизилась к Opus по сумме баллов, а по времени разрыв местами доходил до 10-13 раз (4 минуты у Opus против 55 у Laguna в раунде 4).
Практический вывод: для задач с чёткими границами (одна точка входа, понятный контракт) self-hosted модели через vLLM и Hermes уже сейчас дают приемлемый результат без завязки на облако и лимиты. Для задач, требующих найти все точки входа по смыслу и предусмотреть путь ошибки — разрыв с топовым облачным агентом пока ощутимый, и его лучше закрывать явными и подробными инструкциями в промпте.
В целом, Laguna мне понравилась, буду и дальше использовать ее в работе. Как минимум, нужно пробовать сделать связку с Opus, в которой Laguna будет писать код, гонять тесты и исправлять замечания PHPStan, а Opus будет ставить задачи и проверять их выполнение.
Остаётся ждать, когда появляться более умные модели с открытым исходным кодом, которые мог запустить обычный разработчик без супер дорогого железа.
Комментарии (6)

eax
11.08.2026 10:39Раз уж Вы располагаете аж 128 Гб памяти, то было бы очень интересно увидеть в тестах ещё и https://github.com/antirez/ds4

Druzd
11.08.2026 10:39Пробовал. Запускается Q4_KV4 спокойно, скорость генерации 30-40 ток/сек (упирается в низкую скорость шины 300 Гб/с). Вообще использовать модели в квантизации Q4 такое себе, по одному и тому же промту можно получить 1000 вариаций ответа. Только поиграться.

Druzd
11.08.2026 10:39по qwen не хватает данных с какими гиперпараметрами запускал (особенно интересует mtp), какой коэффициент пенальти от зацикливания в промтах, использовал ли отдельные system promt, user promt или все в одно окно кидал.

PHP-Programmist Автор
11.08.2026 10:39Qwen запускал с дефолтными параметрами, без mtp и пенальти за зацикливание. Отдельный system prompt не писал, использовался дефолтный для Hermes
Void-Cowboy
интересно, а как вы вообще изначально выбирали локальные модели?
PHP-Programmist Автор
По обзорам, которые мне попадались, и по советам Gemini, ChatGPT, DeepSeek. Смотрел на то, сколько памяти нужно для развертывания, какие показатели на бенчмарках.
Laguna, по сути, стала для меня открытием, которую посоветовал один из ИИ - позволяет выжать максимум из DGX Spark, как мне кажется