LLM для игры в ДнД: эволюция подхода. dnd.. dnd. llm.. dnd. llm. n8n.. dnd. llm. n8n. Анализ и проектирование систем.. dnd. llm. n8n. Анализ и проектирование систем. искусственный интеллект.. dnd. llm. n8n. Анализ и проектирование систем. искусственный интеллект. Ненормальное программирование.. dnd. llm. n8n. Анализ и проектирование систем. искусственный интеллект. Ненормальное программирование. Развитие стартапа.. dnd. llm. n8n. Анализ и проектирование систем. искусственный интеллект. Ненормальное программирование. Развитие стартапа. Разработка игр.
С этим уже можно играть

Каждый, кто пробовал играть в настольные ролевые игры типа ДнД с ИИ‑ассистентом типа ChatGPT или Gemini, быстро упирался в ограничения: модель забывает контекст, подыгрывает пользователю, путается в очерёдности ходов и теряет сюжетные рельсы. Но что, если забрать у нейросети математику и управление, оставив ей то, в чём она сильна — работу с нарративом?

В этой статье я расскажу о своём пути R&D: от простого PoC «одна модель делает всё» через трёх агентов к параллельному событийно‑ориентированному пайплайну, где каждый новый шаг — это ответ на ограничение текущего решения.

Начало

Так совпало, что я только‑только познакомился с ДнД, сразу влюбился — и мне выпало несколько свободных скучных вечеров. Очень хотелось играть, а не с кем. И, конечно же, я обратил свой взор на чат‑боты. Взял Gemini, сделал бота, написал большой системный промпт, сгенерировал сценарий‑ваншот — и понеслась!

Первое впечатление было — вау! Описания красивые, можно уточняющие вопросы задавать, правила вроде учитываются, история вроде бы по сценарию идёт. Но по ходу игры начали всплывать проблемы.

Во‑первых, LLM не умеют в математику. Они не могут «бросить кубы», могут только придумать похожий на правду результат. Эту проблему я частично решил, явно прописав требование писать python скрипт для броска кубов и его же выполнять. Костыль, конечно, но рабочий.

Во‑вторых, подыгрывание. Если ты обыскиваешь поверженного врага, то обязательно что‑то находишь — какую‑нибудь бесполезную безделушку. А когда ты вот‑вот умрёшь, и шансов выжить у тебя объективно нет, ты говоришь: «Я достаю ту безделушку, очень‑очень сильно концентрируюсь и молюсь, чтобы она мне помогла!», — и это срабатывает, безделушка вдруг становится волшебной и дарует тебе невероятную силу, испепеляя врагов. Такое себе, и вылечить это я не смог.

В‑третьих, постоянная путаница с очерёдностью в бою с несколькими NPC. Постоянно приходилось поправлять: «Сейчас же ход того волка! Пересчитай». Причём чем больше ходов, тем больше путаницы.

В‑четвёртых, проблема «lost in the middle»: чем дольше ты играешь, тем больше модель забывает и путается — что в ходах и противниках, что в найденных артефактах, что в уже пройденных локациях. Более‑менее рабочее решение — просить делать саммари, начинать новый чат, вставлять туда саммари — и играть ещё 20–30 минут до следующего приступа амнезии. Поначалу помогает, потом перестаёт работать.

Наконец, следование сюжету. Я специально генерировал сюжет так, чтобы не знать его. Потом решил переиграть один сценарий, гляжу — всё по‑другому! Как так? Открываю сценарий и понимаю, что из 10 локаций «правильная» только первая, а потом начинаются сплошные фантазии LLM.

В общем, под конец я поймал себя на том, что раздражение от придумывания новых и новых костылей уже давно перевесило кайф от игры. Меня такое категорически не устраивало.

PoC

И я подумал: что, если хранить состояние в базе, а историю передавать в сжатом виде? Тогда модель наверняка справится!

Я выбрал простой путь: взял N8N, сделал интеграцию с телеграм‑ботом, раскидал сценарий по гугл таблицам, навайбкодил парсер на JS, ну и в центре всего этого поставил агента на базе llama-70b‑versatile, снабдив его тулом для броска кубов.

Через несколько дней экспериментов получилось вот такое решение:

С этим уже можно играть

С этим уже можно играть

За основу системного промпта я взял тот же, что использовал на прошлом шаге, только научил читать контекст игры (огромный json с текущими стейтами и историей) и заставил выдавать ответ в виде json. Ответ потом парсился, новые значения флагов и стейтов шли в базу, красивый ответ — в телеграм.

Решение оказалось рабочим, и именно на нём я наконец‑то канонично прошёл весь придуманный сценарий. Это было круто!

Проблемы PoC

Но были и проблемы. Модель попросту не справлялась со всем, что на неё было навешано:

  • Читать и понимать ВСЕ данные локации;

  • Интерпретировать намерения игрока;

  • Бросать кубы за игрока;

  • Придумывать реакцию мира (и броски кубов за NPC);

  • Писать красивую историю по заданным правилам;

  • Заниматься сериализацией JSON.

Системный промпт нещадно раздувался, появлялось всё больше капса и запретов, а модель всё чаще забывала или игнорировала инструкции. Я опять упёрся в потолок.

Многоагентная архитектура

Следующая мысль, которая меня посетила, была такой: надо разделить ответственность. На тот момент мне удалось выделить 3 максимально разных компонента:

  • «Боец» отвечает только за бой (атаки игрока и NPC);

  • «Арбитр» отвечает за мирную логику, загадки, триггеры врагов и так далее;

  • «Рассказчик» отвечает за нарратив.

Расчёт результатов бросков кубов, подсчёт попаданий и hp, флаги и прочие статы (загадка решена, враг побеждён, какой сейчас раунд и чей ход) — это было вынесено в код. Схема вышла примерно такая, и по сути это стало ядром движка.

Триада: Арбитр, Боец и Рассказчик

Триада: Арбитр, Боец и Рассказчик

Сложность, конечно, возросла многократно. На этом решении я запускал первый закрытый альфа‑тест с живыми игроками — небольшой «вертикальный срез» с демонстрацией возможностей: одна локация, одна загадка, пара врагов и полторы работающие механики ДнД. На то, чтобы это всё более‑менее заработало, у меня ушло 2,5 месяца. И потом ещё полмесяца багфиксов после альфы.

Но вернёмся к моделям и промптам. Поскольку я шёл сверху вниз, то есть от моно‑модели к этой многоагентной архитектуре, то и за основу брал всё то, что было на PoC:

  • Боец и Арбитр по‑прежнему получали практически весь контекст;

  • Они по‑прежнему (каждый по‑своему) отвечали за интерпретацию намерения игрока и валидацию действий;

  • Они по‑прежнему прописывали все нужные броски, указывая и сложность (DC), и всё остальное;

  • Арбитр ещё проверял все возможные триггеры (типа такого: игрок хлопает в ладоши, а в ответ на громкий шум должен сработать триггер врага или ловушка).

При этом, флоу шёл к Арбитру, если это мирное время, и к Бойцу, если идёт бой. Плюс если действия игрока инициировали бой, флоу шёл от Арбитра сразу к Бойцу через код броска инициативы, чтобы сразу начать бой, не теряя ход.

Основные изменения претерпел Рассказчик:

  • Его главной задачей было написать историю по тому, что уже просчитал движок;

  • И желательно так, чтобы не подыгрывать игроку.

Для Рассказчика я по‑прежнему использовал llama-70b‑versatile (просто потому что раньше использовал эту модель и она справлялась), для Арбитра и Бойца — DeepSeek‑v4-flash (потому что очень дёшево, плюс приемлемое качество ответов, но минус — жрёт в полтора‑два раза больше токенов, чем другие модели, и очень медленный).

Юнит‑экономика данного решения по результатам альфа‑теста такая: 100 ходов обходится примерно в $1,5, время работы Рассказчика ~2 сек, время работы Арбитра и Бойца от 15 до 40 секунд (сильно зависит от запроса). Это кошмарно долго и не супер‑дёшево, но до оптимизации промпотов, входных данных и подбора моделей я на тот момент ещё не добрался.

Проблемы «Триады»

А каковы проблемы данного решения? О, их оказалось много, особенно после альфы.

Главной проблемой были галлюцинации Рассказчика, которые отравляли контекст и ему, и остальным агентам на следующих ходах. Если отбросить классические советы про промпт‑инжинирингу, мне помог следующий подход. Я стал формулировать «костяк» истории кодом, а Рассказчику ставил задачу описать историю именно по заданному «костяку», при этом максимально запрещая любую личную интерпретацию действий игрока.

Бойца я немного ускорил, заметив, что у него 2 опции: либо обрабатывать запрос игрока («я атакую огра»), либо просчитывать атаки NPC. И всё это было в одном большом системном промпте, и там же — большой JSON формата вывода. Я сделал динамическое формирование как промпта, так и формата вывода в зависимости от того, чей сейчас ход. То есть опять разделение ответственности. По сути, я мог бы просто поставить 2 отдельных LLM типа Fighter‑player и Fighter‑npc.

Ещё до меня дошло, что Бойцу и контекста так много не надо, и детали бросков (AC, формулы урона и так далее) я могу так‑то сам в коде формировать по данным игрока и NPC из базы. Суммарное ускорение вместе с предыдущим пунктом — 1,5–2 раза.

С Арбитром сложно было что‑то сделать, потому что он уже обрабатывал слишком много всего. Тем более что на тот момент было реализовано лишь несколько механик — без диалогов, без основательной работы со свободными действиями, без вот этих фишек типа: «Я пытаюсь тихо пройти» — «А кинь‑ка мне атлетику! Посмотрим, получится ли у тебя». В итоге Арбитр работает на пределе возможностей, как бедный моно‑агент в PoC, а обрабатывает лишь половину того, что нужно.

А самой большой болью этого решения была дихотомия «или бой, или мир». Например, игрок на Альфе пытался дорешать загадку прямо во время разгоревшегося боя — и не мог этого сделать. Архитектурно ты либо сражаешься, либо решаешь загадки.

Почему это самая большая боль? Потому что именно такие нестандартные действия делают игру в ДнД интересной: ты можешь креативить, делать неожиданные вещи, попытаться запугать или заговорить врага прямо в бою! Пресловутая игра с «голым» ИИ‑ассистентом позволяет это делать, рождая тот самый вайб игры. А дихотомия из двух перегруженных агентов — нет.

Параллельный пайплайн

Пайплайн с роутером/диспетчером и последующим параллельным вызовом узких «специалистов» стал следующим шагом эволюции решения.

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

Параллельный пайплайн

Параллельный пайплайн

Казалось бы, это решение должно стать достойным ответом на вызовы, рождённые «триадой» на предыдущем этапе. Хочется верить, что так и будет выглядеть стабильная версия ядра. Так это или нет — покажет время, сейчас я только работаю над реализацией этого решения. Однако уже есть некоторые промежуточные результаты.

Плюсы параллельного пайплайна

Первый плюс — облегчение «LLM‑специалистов». Это даёт две выгоды: 1) возможность использовать модели попроще и 2) повышение качества ответов (меньше контекста, меньше инструкций — следовательно, модель точнее делает то, что ты от неё хочешь).

Второй и ключевой — настоящее распараллеливание. Если игрок напишет что‑то вроде «одной рукой бью монстра, другой рукой кладу в ту волшебную чашу волос единорога, а ещё кричу барменше налить ещё пива», технически всё это обработается за один ход (опустим формальную экономику действий в бою в ДнД).

Челленджи параллельного пайплайна

Конечно же, всё снова оказалось гораздо сложнее, чем казалось в начале (особенно после слов GPT про «отличная идея! Это классическое решение подобных задач…»). Сразу скажу, что решений у меня пока нет. Но есть некоторые идеи. Буду экспериментировать. Пока же просто перечислю стоящие передо мной вызовы.

  • Колоссальный оверхед интерпретации намерения: и роутер, и каждый мини‑агент должен интерпретировать действия игрока и определить влияние на свой контекст.

  • Выбрать отдельных «специалистов» можно не всегда, некоторых придётся запускать каждый раз по дефолту.

  • Обработка коллизий: один «специалист» разрешил действие, другой запретил, третий вообще предложил проверку. Что движку делать в итоге?

  • Каскадные события: провал проверки → срабатывает ловушка → на шум активируются враги.

  • Наконец, сколько будет стоить такой ход? Удастся ли оптимизировать? Может оказаться, что цена и задержка похоронят идею. Но я верю в кэш токенов и потенциальную оптимизацию.

И когда это всё как‑то заработает… Я задумываюсь, каким же окажется потолок у данного подхода. Какие новые ограничения появятся? Какой станет система?

Осколки наивных надежд

За время работы над этим проектом мой взгляд на некоторые решения (вероятно, маркетинговые?) сильно изменился. Вот мои инсайты:

  • LLM не всесильны. Вообще. Даже флагманские модели. Решает инженерия. Поэтому «вставьте LLM и всё [само] автоматизируется» — маркетинговая ерунда.

  • «Тут подойдёт модель класса 8b» — тоже нет, если задача не «какое из этих трёх слов обозначает цвет?»

  • «Для этой задачи сгодится любая LLM» — нет, не сгодится. У всех есть проблемы, придётся подбирать. Примеры проблем из моего решения:

    • DeepSeek‑v4-flash — вообще ни разу не flash;

    • Llama-70b‑versatile — дорогая, часто ляпает иероглифы или английские фразы в ответ;

    • GPT-4o‑mini — как только выходной json становится чуть сложнее, ломает структуру через раз;

    • Gemini-2.5-flash — каждый 5-й вызов падает с ошибкой «нет ресурсов, попробуйте позже», даже если вы привязали карту.

  • «Просто смени модель» — работает только для простых задач. На сложных смена модели приводит к изменению поведения, так что придётся переписывать промпты и долго отлаживать.

Ну и классическое «завайбкодить что‑то на коленке за вечер». Это реально? Да, без проблем, если это примитивный PoC. А вывести в прод сколько‑нибудь серьёзное решение — нет, тут потребуются месяцы упорного труда.

R&D: путь самурая

Мой путь можно описать так: попробовал самое простое решение — упёрся — переделал — и так по кругу. Классическое R&D. Можно заметить, как артефакты тянутся из одной стадии в другую. Сначала системный промпт с небольшими изменениями перекочевал из ИИ‑ассистента в PoC с единой LLM. Затем всё тот же промпт и контекст из PoC попали в мультиагентную архитектуру. Затем из промпта и контекста всё, что может делать код, уехало в этот самый код, облегчив модели; а модели стали более специализированными. Наконец, переход к параллельному пайплайну окончательно расщепляет модели на узконаправленных специалистов, однако требует всё больше и больше дополнительных обработчиков.

А разве нельзя было сразу сесть и подумать, как сделать по‑нормальному? Моё мнение: нет, нельзя.

Во‑первых, гипотеза, что такая игра будет кому‑то интересна и в ней есть потребность, должна быть провалидирована. Для этого подходит решение на коленке: N8N, Google docs в качестве БД, одна‑две‑три LLM без существенной оптимизации. Кроме того, именно фидбэк пользователей определяет, какую проблему следует решать и чем можно пренебречь. PoC показал, что технически идею реализовать можно. Альфа тест на том же N8N с тремя агентами показал, что продукт жизнеспособен и что важно игрокам.

Во‑вторых, «сесть и подумать» невозможно, если ты не разрабатывал подобные вещи 10 лет кряду — просто нет компетенций. А на ИИ‑ассистента надежды мало, он предлагает тебе «классную идею», а как только ты вляпываешься, говорит: «О, это классическая проблема! Давай переделаем!»

Так что пока я выбираю старый добрый R&D.

Текущая точка

Сейчас я сражаюсь с вызовами параллельного пайплайна. Если вы решали проблемы каскадных событий в играх, а тем более в многоагентных системах — поделитесь в комментариях, что делали. Будет интересно обсудить альтернативные архитектурные подходы. И заглядывайте в мой Telegram‑канал, где я выкладываю апдейты по разработке движка.

Автор: evgenii_ibragimov

Источник