Мой блог
Как заставить ИИ-движок соблюдать непрописанные правила: опыт D&D-бота
Проблема галлюцинаций ИИ-мастера и нетипичные действия игроков
В классических настольных ролевых играх вроде Dungeons & Dragons ключевая прелесть заключается в полной свободе действий. Однако при попытке перенести этот опыт в Telegram-бота на базе языковых моделей разработчик сталкивается с барьером. Во время боя персонаж вместо банальной атаки мечом может заявить что-то абсолютно неожиданное: например, лишить себя мизинца и положить его в жертвенную чашу, чтобы мгновенно разгадать загадку древнего алтаря.
Когда я проводил первый альфа-тест своего D&D-бота три месяца назад, подобные ситуации вскрыли фундаментальную проблему чисто текстовых LLM. Игрок четыре раза подряд предпринимал попытки отрезать себе палец. Нейросеть каждый раз генерировала красивый литературный ответ, но этот текст оставался «галлюцинацией на бумаге»: здоровье персонажа не уменьшалось, статус загадки не менялся, а инвентарь оставался прежним. Игра не имела механической памяти об этом событии.




Заранее прописать жесткие правила (написать скрипты) под каждую возможную выдумку человека невозможно — это превратило бы гибкую ролевую систему в обычную текстовую новеллу со строго рельсовым сюжетом. В результате я спроектировал архитектурный MVP, который умеет корректно обрабатывать подобные импровизации без прямого хардкода таких правил в коде.

Архитектура «веера судей»: разделение ИИ и программного кода
Основная концептуальная идея нового движка заключается в четком разделении обязанностей. Нейросеть не должна напрямую меняет состояние мира или единолично решать, что произошло. Ее задача — вынести суждение. Программный код на основе этого суждения производит математические расчеты, меняет записи в базе данных и проверяет условия. После этого фиксация факта передается отдельному ИИ-рассказчику, который переводит сухие цифры в художественный текст.
Чтобы система работала точно и быстро, я отказался от идеи использовать одну большую модель для всех задач. Вместо этого задействован параллельный «веер» из специализированных узких LLM-судей (в текущей версии их 8):

- Дуэлянт — оценивает боевые действия, физические атаки и применения оружия;
- Резолвер — отвечает за логику головоломок, ценность подношений и активацию древних механизмов;
- Часовой — отслеживает реакцию окружения, агрессию монстров и срабатывание ловушек;
- Импровизатор — выносит вердикты по нетипичным физическим трюкам и нестандартным заявкам.
Такой подход оказался крайне выгоден экономически. Каждая модель получает минимальный, четко сформированный контекст и узкий промпт. Для этой роли отлично подходят недорогие и быстрые модели уровня GPT-4o Mini.
Разбор кейса: от декомпозиции запроса до броска кубика
Перед отправкой запроса судьям в цепочку включается Роутер. Его задача — расчленить сложную фразу игрока на атомарные действия и выявить их взаимосвязи. Запрос с жертвенным пальцем Роутер разбивает на две последовательные операции:

- Персонаж откусывает или отрезает себе мизинец;
- Персонаж помещает полученный палец в чашу.
Роутер определяет, что второе действие напрямую зависит от успешного выполнения первого, после чего запрашивает вердикты у подходящих судей.
Судьи формируют структурированные ответы в формате JSON, отвечая на фиксированный набор вопросов:

- Возможно ли действие в текущей сцене? (Да);
- Требуется ли проверка параметров через бросок d20? (Для действия 1 — да, Атлетика со сложностью 15. Для действия 2 — проверка не требуется);
- Каковы последствия успеха и неудачи? (При успехе action_1 — персонаж теряет HP и получает дебафф; при неудаче — действие провалено);
- Решает ли это действие загадку? (Резолвер подтверждает: палец является достаточной жертвой, «Загадка Порога» разгадана).
Получив эти суждения, в работу вступает Python-код. Поскольку действие зависит от случайного броска, система временно «замораживает» транзакцию и сохраняет логику в БД. Игроку предлагается нажать кнопку броска d20. Код берет модификатор Атлетики из карточки персонажа, генерирует случайное число, суммирует их и сравнивает со сложностью 15.
В случае успеха код автоматически списывает очки здоровья, меняет статус загадки на «решена», открывает связанный проход в соседнюю локацию и вносит записи в БД. Если же бросок провален, первое действие отменяется, а второе каскадно блокируется, так как не был получен необходимый предмет. Лишь после этого сгенерированный итоговый список фактов отправляется ИИ-рассказчику для финальной литературной описи.
Многоуровневая система правил: от жесткого кода до свободного ИИ
Чтобы система не разваливалась при любых непредвиденных вводных, правила в движке распределены по трём жестким уровням:
1. Базовый код движка
На этом уровне живут абсолютно детерминированные вещи: расчет очерёдности ходов в бою, списание очков действий, математика урона, учет текущего здоровья, управление БД и открытие дверей. Код ничего не придумывает, а лишь строго исполняет команды.
2. Механики судей в промптах
Каждый судья запрограммирован понимать контекст своего домена. Например, Дуэлянт легко отличает обычный удар мечом от импровизированной атаки и понимает, что попытка бросить песок в глаза орку — это не нанесение урона песком, а попытка наложить статус «ослепление». На выходе модель генерирует JSON с нужными флагами.
3. Доменная маршрутизация и импровизация
Роутер отвечает за то, чтобы обычный осмотр комнаты не отправлялся к Дуэлянту. Но если действие игрока вообще не поддается стандартной классификации, оно направляется к Импровизатору. Именно он оценивает, способен ли персонаж прыгнуть на высоты сеттинга, выбить себе зуб или запрыгнуть на потолок, определяя сложность проверки и возможную цену ошибки.
Архитектурные риски и неизбежные компромиссы
Создание ролевого ИИ-движка на базе мультиагентной системы неизбежно влечет за собой ряд инженерных сложностей:
- Множественные точки отказа. Если Роутер ошибается с выбором судей на старте, вся цепочка выдаст некорректный результат. Ошибка в интерпретации триггера может привести к тому, что враг не вступит в бой или появится раньше времени.
- Проблемы классификации. В ходе тестирования я заметил, что Дуэлянт может полностью проигнорировать нетипичную заявку вроде «плюнуть в лицо противнику», так как она не подходит ни под категорию оружия, ни под привычную импровизированную атаку. В итоге действие просто сбрасывается.
- Вариативность суждений. Из-за вероятностной природы LLM одно и то же импровизированное действие в разных сессиях может трактоваться по-разному. В одном прогоне модель потребует бросок на успех действия, а в другом — посчитает действие автоматически успешным, но затребует бросок на урон от приземления.
Подобная нестабильность — это неизбежная плата за отказ от строго прописанных рельсов. Главная задача разработчика здесь сводится к точной настройке границ между вероятностным вычислением нейросети и детерминированным исполнением кода.
Источник: habr.com
