Анатомия сломанного бэкенда: почему ИИ-агенты разрушают мультиплеер и как стабилизировать состояние
Генерация кода большими языковыми моделями создает устойчивую иллюзию всемогущества: за пару диалоговых сессий можно получить трехмерный интерфейс, сложные игровые расчеты и сотни зеленых модульных тестов. Однако при переходе от изолированного клиентского кода к сетевому взаимодействию в реальном времени наивный подход («вайбкодинг») неизбежно заходит в тупик. Накопление мелких правок от нейросети не решает системных проблем, а лишь маскирует архитектурный распад.
Инженерный опыт создания браузерной пошаговой 3D-игры Hexnome наглядно демонстрирует границы возможностей современных ИИ-агентов. Проект, доведенный до продакшена за десять рабочих сессий через Claude Code, потребовал 245 детализированных промптов, 99 коммитов в основную ветку и полной утилизации первого варианта бэкенда объемом в 30 коммитов. История этого перезапуска объясняет, почему автономные модели бессильны перед распределенным состоянием и как разработчику выстроить детерминированный сетевой контур.
Исходная композиция: правила, стек и иллюзия быстрого старта
Hexnome задумывалась как цифровая адаптация настольной puzzle-механики с полем из гексагональных цветков-пластин, общим пулом фишек и личными резервами игроков. Пространство состояний включает 36 уникальных тайлов с биологической символикой (от спирали ДНК до бензольного кольца), систему оплаты размещения, кольцевые связи вокруг якорей и механику жетонов-джокеров.
Чтобы исключить барьеры установки для пользователей, выбор был сделан в пользу веб-стека вместо тяжелых движков Unity или Godot:
- Клиентская часть: SPA на базе Vue 3 и Vite с управлением состоянием через Pinia;
- Графический слой: трехмерная сцена на Three.js с декларативной оберткой TresJS;
- Изолированное ядро правил: независимый пакет на чистом TypeScript, не знающий о DOM и рендерере;
- Серверный слой: NestJS, ORM-библиотека Prisma и база данных MySQL;
- Верификация: модульные тесты на Vitest.
Разделение плоской математики правил и визуального 3D-представления сразу принесло плоды. ИИ-агент безупречно преобразовывал словесные формулировки правил в строгие TypeScript-функции. За первые два вечера проект получил гексагональную сетку, шейдерные фаски, анимацию драг-энд-дропа и одиночный игровой цикл. Ядро возвращало причину любого недопустимого хода, интерфейс подсвечивал тайлы, а тесты проходили со стопроцентным успехом.
Ловушка первой попытки: когда локальные фиксы маскируют катастрофу
Проблемы начались при переходе к сетевому мультиплееру. Первая архитектурная гипотеза казалась логичной и экономной: взять готовый синглплеерный стейт и надстроить над ним тонкую серверную прослойку. Состояние хранилось в браузере, ходы отправлялись в базу, а протокол WebSocket (постоянный двунаправленный канал связи) служил только для уведомления об изменении индекса последней записи в журнале.
Для поддержки нескольких участников поверх клиентской модели был создан адаптер контекста игрока. Чтобы не ломать написанные ранее тесты, идентификатор текущего места сделали необязательным параметром (seat?). Это решение оказалось фатальным:
- Разрушение изоляции данных: рендерер вызывал методы поля без указания активного игрока, из-за чего тайлы одного участника самопроизвольно отрисовывались в чужом резерве.
- Асинхронные гонки (race conditions): параллельные действия игроков приводили к тому, что два разных объекта занимали одну и ту же ячейку, а перезагрузка страницы скрывала рассинхронизацию.
- Утечки памяти в Node.js: разрастание слушателей событий при постоянных переподключениях WebSocket приводило к деградации процесса.
В этой ситуации ИИ-ассистент повел себя как классический исполнитель: на каждый зарегистрированный баг он послушно находил функцию, добавлял проверку контекста, генерировал новый тест и рапортовал об успехе. Но количество ошибок не уменьшалось. Модель исправляла локальные симптомы, не обладая системным видением архитектурной несовместимости. После десятого циклического бага стало очевидно, что кодовая база зашла в тупик. Потребовалось решение инженера: полностью остановить разработку, выбросить ветку из 30 коммитов и начать проектирование бэкенда заново.
Пересборка на Event Sourcing и детерминированный лог команд
Вторая попытка отталкивалась от жесткого архитектурного принципа: сначала полное и неделимое состояние игры на одном устройстве, и лишь затем — подключение сети.
Вместо попыток синхронизировать независимые клиентские копии была принята модель единого источника правды:
- Глобальный инвариант: для партии на трех игроков в памяти всегда существуют строго один общий источник, три резерва и три поля. Личность подключенного пользователя определяет лишь угол обзора виртуальной камеры и права на совершение конкретного действия. Рассинхронизировать состояние невозможно физически, поскольку оно едино.
- Изоляция секретов: генерация колоды была вынесена в изолированный микросервис. Сервер инициализирует генератор псевдослучайных чисел по скрытому seed, выдает клиентам идентификаторы и гарантирует, что порядок будущих тайлов невозможно подсмотреть в коде браузера.
- Журнал событий (Command Log): каждый ход оформляется как атомарная команда с уникальным идентификатором (
cmdId) и ссылкой на предшествующее состояние (prevSeq). Уникальный составной индекс в MySQL на кортеж(gameId, prevSeq)на уровне базы данных блокирует попытки двух игроков одновременно стать продолжением одной позиции. - Роль WebSocket: сокеты освобождены от передачи тяжелого состояния. Они передают только легковесный сигнал о сдвиге курсора журнала команд. Если сообщение потеряно, клиент автоматически дозапрашивает недостающие дельты через фоновый опрос (polling).
Разделение ответственности: где эффективен ИИ, а где нужен человек
Опыт создания Hexnome позволяет четко разграничить зоны продуктивности языковых моделей и области обязательного инженерного контроля при создании комплексных программных систем.
| Слой разработки | Роль ИИ-агента | Зона ответственности инженера |
|---|---|---|
| Пользовательский интерфейс и графика | Генерация шейдеров, верстка компонентов, математика гексагональных сеток | Выбор графического стека и ограничение области неизвестного |
| Логика правил | Написание чистых функций, валидаторов и исчерпывающего набора модульных тестов | Формулирование инвариантов предметной области и ограничений механики |
| Сетевая архитектура | Генерация шаблонного кода контроллеров, DTO и миграций базы данных | Выбор модели консистентности (Event Sourcing) и схемы владения данными |
| Отладка и рефакторинг | Локализация сбоев по стектрейсу, написание регрессионных тестов | Оценка сходимости архитектуры, решение об откате нежизнеспособных веток |
Чек-лист стабильности для проектов с активным ИИ-кодогенератором
Чтобы кодовая база под управлением нейросетевых ассистентов не деградировала в клубок взаимных блокировок, разработчику необходимо соблюдать правила структурной гигиены:
- Формулируйте инварианты, а не экраны: промпт должен описывать не визуальный эффект, а границы владения памятью и неизменяемые условия (например, «состояние едино для всех игроков, клиент меняет только проекцию отображения»).
- Отказывайтесь от необязательных полей в доменных моделях: необязательные параметры вроде
seat?создают лазейки, которые сохраняют тесты зелеными, но взрывают продакшен при параллельных действиях пользователей. - Требуйте однонаправленного потока данных: мутация состояния по месту вызова должна быть запрещена; любые изменения применяются только через детерминированные чистые функции-обработчики.
- Контролируйте метрику сходимости багов: если на устранение одной проблемы модель порождает два новых пограничных случая, архитектурная модель неверна — продолжать итерации бессмысленно, требуется откат к стабильной точке.
- Вводите идемпотентность на уровне протокола: каждая сетевая команда обязана содержать уникальный клиентский ключ (
cmdId) и ссылку на предыдущую версию сущности (prevSeq).
Итог ретроспективы однозначен: современные ИИ-инструменты многократно ускоряют рутинное кодирование, но не отменяют законов распределенных систем. Успех продукта по-прежнему определяется способностью инженера проектировать надежные границы данных и вовремя выбрасывать нежизнеспособный код.

![Node.JS [ru]](/api/digests/it_development/daily/20260814/assets/sources/we-use-js.jpg)