Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи?

Написать
Войти
Дайджесты новостей
Иллюстрация к статье об архитектуре мультиплеерного бэкенда и разработке с ИИ-агентами

Анатомия сломанного бэкенда: почему ИИ-агенты разрушают мультиплеер и как стабилизировать состояние

Инженерный опыт создания 3D-игры Hexnome с помощью Claude Code: почему генерация 245 промптов привела к утечкам памяти и рассинхронизации WebSocket-бэкенда, как переход к архитектуре Event Sourcing и детерминированному логу команд стабилизировал состояние и где автономные ИИ-агенты требуют контроля разработчика.

Анатомия сломанного бэкенда: почему ИИ-агенты разрушают мультиплеер и как стабилизировать состояние

Генерация кода большими языковыми моделями создает устойчивую иллюзию всемогущества: за пару диалоговых сессий можно получить трехмерный интерфейс, сложные игровые расчеты и сотни зеленых модульных тестов. Однако при переходе от изолированного клиентского кода к сетевому взаимодействию в реальном времени наивный подход («вайбкодинг») неизбежно заходит в тупик. Накопление мелких правок от нейросети не решает системных проблем, а лишь маскирует архитектурный распад.

Инженерный опыт создания браузерной пошаговой 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?). Это решение оказалось фатальным:

  1. Разрушение изоляции данных: рендерер вызывал методы поля без указания активного игрока, из-за чего тайлы одного участника самопроизвольно отрисовывались в чужом резерве.
  2. Асинхронные гонки (race conditions): параллельные действия игроков приводили к тому, что два разных объекта занимали одну и ту же ячейку, а перезагрузка страницы скрывала рассинхронизацию.
  3. Утечки памяти в Node.js: разрастание слушателей событий при постоянных переподключениях WebSocket приводило к деградации процесса.

В этой ситуации ИИ-ассистент повел себя как классический исполнитель: на каждый зарегистрированный баг он послушно находил функцию, добавлял проверку контекста, генерировал новый тест и рапортовал об успехе. Но количество ошибок не уменьшалось. Модель исправляла локальные симптомы, не обладая системным видением архитектурной несовместимости. После десятого циклического бага стало очевидно, что кодовая база зашла в тупик. Потребовалось решение инженера: полностью остановить разработку, выбросить ветку из 30 коммитов и начать проектирование бэкенда заново.

Пересборка на Event Sourcing и детерминированный лог команд

Вторая попытка отталкивалась от жесткого архитектурного принципа: сначала полное и неделимое состояние игры на одном устройстве, и лишь затем — подключение сети.

Вместо попыток синхронизировать независимые клиентские копии была принята модель единого источника правды:

  • Глобальный инвариант: для партии на трех игроков в памяти всегда существуют строго один общий источник, три резерва и три поля. Личность подключенного пользователя определяет лишь угол обзора виртуальной камеры и права на совершение конкретного действия. Рассинхронизировать состояние невозможно физически, поскольку оно едино.
  • Изоляция секретов: генерация колоды была вынесена в изолированный микросервис. Сервер инициализирует генератор псевдослучайных чисел по скрытому seed, выдает клиентам идентификаторы и гарантирует, что порядок будущих тайлов невозможно подсмотреть в коде браузера.
  • Журнал событий (Command Log): каждый ход оформляется как атомарная команда с уникальным идентификатором (cmdId) и ссылкой на предшествующее состояние (prevSeq). Уникальный составной индекс в MySQL на кортеж (gameId, prevSeq) на уровне базы данных блокирует попытки двух игроков одновременно стать продолжением одной позиции.
  • Роль WebSocket: сокеты освобождены от передачи тяжелого состояния. Они передают только легковесный сигнал о сдвиге курсора журнала команд. Если сообщение потеряно, клиент автоматически дозапрашивает недостающие дельты через фоновый опрос (polling).

Разделение ответственности: где эффективен ИИ, а где нужен человек

Опыт создания Hexnome позволяет четко разграничить зоны продуктивности языковых моделей и области обязательного инженерного контроля при создании комплексных программных систем.

Слой разработкиРоль ИИ-агентаЗона ответственности инженера
Пользовательский интерфейс и графикаГенерация шейдеров, верстка компонентов, математика гексагональных сетокВыбор графического стека и ограничение области неизвестного
Логика правилНаписание чистых функций, валидаторов и исчерпывающего набора модульных тестовФормулирование инвариантов предметной области и ограничений механики
Сетевая архитектураГенерация шаблонного кода контроллеров, DTO и миграций базы данныхВыбор модели консистентности (Event Sourcing) и схемы владения данными
Отладка и рефакторингЛокализация сбоев по стектрейсу, написание регрессионных тестовОценка сходимости архитектуры, решение об откате нежизнеспособных веток

Чек-лист стабильности для проектов с активным ИИ-кодогенератором

Чтобы кодовая база под управлением нейросетевых ассистентов не деградировала в клубок взаимных блокировок, разработчику необходимо соблюдать правила структурной гигиены:

  1. Формулируйте инварианты, а не экраны: промпт должен описывать не визуальный эффект, а границы владения памятью и неизменяемые условия (например, «состояние едино для всех игроков, клиент меняет только проекцию отображения»).
  2. Отказывайтесь от необязательных полей в доменных моделях: необязательные параметры вроде seat? создают лазейки, которые сохраняют тесты зелеными, но взрывают продакшен при параллельных действиях пользователей.
  3. Требуйте однонаправленного потока данных: мутация состояния по месту вызова должна быть запрещена; любые изменения применяются только через детерминированные чистые функции-обработчики.
  4. Контролируйте метрику сходимости багов: если на устранение одной проблемы модель порождает два новых пограничных случая, архитектурная модель неверна — продолжать итерации бессмысленно, требуется откат к стабильной точке.
  5. Вводите идемпотентность на уровне протокола: каждая сетевая команда обязана содержать уникальный клиентский ключ (cmdId) и ссылку на предыдущую версию сущности (prevSeq).

Итог ретроспективы однозначен: современные ИИ-инструменты многократно ускоряют рутинное кодирование, но не отменяют законов распределенных систем. Успех продукта по-прежнему определяется способностью инженера проектировать надежные границы данных и вовремя выбрасывать нежизнеспособный код.