Введение
Здравствуйте! Я - автор портала текстовых игр в жанре "квест" https://questio.ru.
Основная идея на страте проекта состояла в том, чтобы дать LLM максимально полное описание игровой ситуации и попросить придумать реакцию или дополнительные данные.
Любая игра начинается с подготовки - создания и настройки игрового мира. Поэтому и на этом этапе казалось, что будет достаточно подробно описать требования к результату.
Буквально так, как это рассказывают на всех курсах: главное - правильно составить запрос (промпт) и ИИ все сделает!
Однако к этапу релиза LLM кардинально сменила свою роль - почти всегда теперь это "перефразируй текст" и не более. И не 1 большой запрос, а много-много маленьких.
Как так вышло (с примерами) я и расскажу ниже.
Исходные ограничения
Сразу стоит оговориться, что задача (возможно) вполне решаема именно так, как была поставлена изначально, но лишь в случае использования "ризонеров" (LLM, заточенных на рассуждения) достаточно больших размеров. Но использование таких LLM ограничено 2 факторами:
стоимость - цена токена ризонера как правило выше, чем instruct-модели,
время - оно также больше (на объем рассуждений), но имеет весьма большое значение.
Изначальные цели и ограничения:
все должно работать на локальной LLM (идеально, если поместится в 8 Гб)
время ответа не должно быть слишком долгим, особенно для много-этапных операций
текст, предоставляемый пользователю должен быть грамотным, на русском языке
Таким образом, изначально разработка была начата на основе Llama3.1 8B (в то время - единственная LLM подходящего размера,
одновременно хорошо поддерживающая русский язык и относительно хорошо понимающая инструкции).
Но на момент релиза от требования локального запуска пришлось отказаться (надеюсь, что временно) и сейчас "под капотом" трудится gpt-4.1-mini.
Создание игрового мира. Первые шаги
Для того, чтобы понять зачем и какая нужна подготовка игры, расскажу немного о сути того, что из себя представляет каждая игра.
Я вдохновлялся играми, выходившими на ZX Spectrum, такими как "Звездное наследие" или игра про мышь-детектива (название не помню, но помню, что в ней было много смешных моментов). Чисто текстовые игры для меня тогда были сложноваты, поэтому запомнились именно "с элементами графики".
По механике они в принципе одинаковы:
существует некий набор локаций
локации связаны между собой различными переходами
в локациях могут быть игровые персонажи или предметы
в локациях могут быть различные ограничения и триггеры (например, выберешь "поспать", а во сне на тебя кто-то нападает)
в каждой локации есть изображение
в каждой локации в зависимости от npc и предметов доступен ряд действий. Игрок выбирает действие - получает реакцию.
Т.е. весь игровой процесс, если упростить, представляет собой перемещение по локациям и взаимодействие с npc и предметами, имея целью куда-то добраться и выполнить Финальное Действие.
При этом приходится решать различные загадки и головоломки, которые на первых порах можно упростить до "найди как и с помощью чего преодолеть очередное препятствие".
Соответственно, осовремененный вариант такого текстового квеста мне видится как совмещение генеративных возможностей LLM с классической механикой.
В частности это означает свободу действий, но в рамках ограничений мира. Ну и автоматизацию создания игры, конечно.
При этом должны соблюдаться важные условия:
игра должна быть конечной. Т.е. должна быть некая цель и эта цель должна быть достижимой. Я пробовал играть в "бесконечные истории". Это интересно... поначалу. А потом становится скучно - все действия производятся только для того, чтобы их производить. Бесконечное движение по течению...
должны работать некоторые ограничения (враги - нападать, препятствия - не пускать, друзья - помогать)
к каждому врагу - свой подход (найти универсальный "меч-кладенец" и пройти с ним сквозь ряды - слишком просто и не интересно)
для каждого врага должно быть несколько вариантов "решения"
необходимые предметы должны быть достижимы (т.е. до них должна быть возможность добраться, не должно быть взаимных блокировок вроде "чтобы победить стража нужно взять что-то, что лежит в комнате, в которую он не пускает")
Таким образом, минимальным стартовым набором в этом случае становится карта (набор связанных локаций), по которой игроку предстоит перемещаться.
И на этой карте заранее расставлены npc и предметы. А также должна быть сформулирована конечная цель и общее описание мира.
И начал я именно с описания. Опыты, производимые вручную, дали вполне подходящий результат. Получившийся промпт был взят на вооружение для автоматизированной работы.
Пример промпта для создания описания игры
Ты - опытный сценарист компьютерных игр. Мы пишем основу сюжета для компьютерной игры жанра "текстовый квест".
Тема игры и предварительное описание:
{{desc}}
Задача: на основе предварительного описания игры придумай: 1) главного героя (имя и описание), 2) общее описание игрового мира, 3) описание правил и ограничений этого мира, 4) основные жанры, 5) предысторию (что произошло перед событиями игры, что к ним привело) событий для игры, 6) конечную цель, которая будет считаться "успешным прохождением". Напиши развернутое подробное описание в стиле приключенческого романа и оформи его в соответствии с примером (НЕ описывай локации и инвентарь). Учти особенности и ограничения, указанные в предварительно описании. Конечная цель должна быть описана кратко и конкретно, без дополнительных пояснений. Правила также должны быть четкими и лаконичными. Остальное - подробное и развернутое.
Следуй формату примера. Пиши только на русском языке, грамотно, соблюдай правила языка. Убедись, что правила игрового мира не противоречат друг другу и достаточно подробны.
Используй разметку markdown, убедись, что каждый элемент списка начинается с новой строки. Будь лаконичен.
Пример описания игры по теме "Космические приключения мусорщика Шеппарда":
## 🧑🚀 **Главный герой**
**Шепард** – мусорщик, весёлый разгильдяй с мечтой стать настоящим космическим пилотом.
- Тип: неудачник, но с большими амбициями
- Характер: не слишком умён, но **целеустремлён и решителен**
- Особенность: постоянно попадает в курьёзные ситуации
## 🌌 **Игровой мир**
Действие происходит в **Млечном Пути в конце XXII века**, где стали возможны:
- **Межзвёздные полёты**
- **Мгновенные перемещения** благодаря **ретрансляторам массы** (созданным древней расой **протеан**)
### 🛰️ **Галактический порядок**
- **Галактический совет** базируется на космической станции **«Цитадель»**
- **Расы вселенной**:
- Азари, саларианцы, турианцы, кроганы
- Синтетические расы: **геты** и **Жнецы**
## 📜 **Правила мира**
### ❌ **В этом мире НЕТ**:
- Магии и мистики
- Фури и разумных животных
- Магических артефактов
### ✅ **В этом мире ЕСТЬ**:
- Инопланетные цивилизации
- Фантастические технологии и ИИ
- Космические корабли и древние артефакты
### ⚔️ **Текущая обстановка**
Надвигается **глобальная война с Жнецами**, по всей галактике происходят мелкие стычки.
## 🎭 **Основные жанры**
- Космическая фантастика
- Боевик
- Инопланетные цивилизации
## ⏳ **Предыстория**
Через **6 месяцев** после открытия **Космической академии** начинается вторжение **Жнецов** – враждебной расы полуоргаников-полумашин, стремящихся уничтожить все разумные цивилизации.
**Шепард поступает в академию**, чтобы стать пилотом истребителя. Ему предстоит:
1. Окончить обучение
2. Выполнить боевые миссии
## 🎯 **Конечная цель**
**Договориться с инопланетными расами** о совместной борьбе против Жнецов и спасти галактику!
На первых порах все игры создавались исключительно в полуручном режиме. И последовательно по этапам. Обычным делом было "повторить этап". Поэтому проблемы генерирования описания правились не сразу.
Среди них оказались:
Корявое форматирование и оформление (если из примера в исходном промпте убрать псевдографику, итоговый текст получается трудночитаемым - все слито в одно целое; если же наоборот добавить псевдографику, LLM начинает ее влеплять где только можно)
Нестрогое следование формату примера (пробовалось: txt/json, инструкции вроде "строго следуй примеру", вариации с детализацией, несколько примеров) - нет-нет, да и пропустит блок-другой, добавит новый, сместит акценты, часть оформит нормально, а часть сольет в единую кашу, напишет список подряд вместо "на каждой строке" и т.д.
Сложность найти баланс детализации. С одной стороны нужно достаточно детально описать основные аспекты, с другой - каждый из них сделать максимально коротким. LLM же норовила "написать роман" или пропустить что-то.
В момент релиза, когда была предоставлена возможность создания игр самими пользователями, все эти проблемы проявились во весь рост.
В качестве решения подошло поэтапное генерирование, которое по времени не сильно уступает варианту "за 1 заход". Все описание было разделено на логические блоки, к каждому блоку сформирован набор требований, составлен индивидуальный промпт.
Такой подход позволил 1) четко разделить блоки, чтобы их можно было использовать отдельно без дополнительных телодвижений, 2) оформить общий результат так, как нужно мне.
Пример промпта генерации блока описания
Ты - опытный автор текстовых квестов разных жанров. На основе текста 'базовое описание' придумай краткое описание (2 предложения) главного героя (имя, характер и какие-нибудь особенности, прояви креативность).
Например, для базового описания 'Космические приключения мусорщика Шеппарда' описание главного героя может быть таким:
Шеппард – мусорщик, весёлый разгильдяй с мечтой стать настоящим космическим пилотом. Высокий, атлетичный - мечта всех девушек.
Не слишком умен, но целеустремлен и решителен. Неудачник, но с большими амбициями. Постоянно попадает в курьезные ситуации.
'Базовое описание': {base}
Создание игрового мира. Карта
Самый большой и неожиданный стопор проявился в работе над созданием карты. Первый этап - сами локации: просто перечень и связи.
Первоначальный подход: даем описание мира, требования к локациям и их связям и вуаля! Но сложности проявились еще на этапе ручных опытов.
Карту целиком так и не удалось сгенерировать за 1 запрос. В частности, почти всегда были повторы в названиях локаций, а также несостыковки в плане связей - были случаи, когда из начальной локации добраться до конечной вообще не было возможности. Ну и конечно, частые провалы в "бесконечную генерацию".
Поэтому я решил (не сразу, конечно, а путем постепенного отказа от ИИ, как генератора на различных этапах), что сложная аналитика - это не про LLM (из тех, что я могу использовать, как минимум). И генерацию карты сделал полностью программно.
На выходе этого этапа получаю граф, где вершины - локации, а ребра - связи между ними.
Второй этап - именование локаций и генерирование их описаний. Здесь снова я полагал, что будет достаточно одного запроса вида (описание карты подставляется как markdown-таблица со столбцами id, id связанных локаций).
Запрос на создание карты
Ты - сценарист компьютерных игр жанра "текстовый квест".
Твоя задача описать игровые локации. Для каждой локации укажи название и описание внешнего вида (что видит игрок, когда заходит в нее). Каждая локация должна иметь уникальное название.
Локации должны быть релевантны описанию игрового мира. Локации, размещенные рядом, должны логически подходить друг другу.
Описание игрового мира:
{desc}
Описание карты:
{map}
Ответ дай в формате JSON в соответствии с примером:
[{"id": <id локации>, "name": <название локации>, "view": <вид локации>}]
Но не вышло. Единственным способом сгенерировать названия локаций так, чтобы они не повторялись, оказалось последовательная генерация с проверкой результата по массиву ранее созданных названий.
На основе анализа реального использования могу утверждать, что в среднем на 1 список локаций уходит 3-4 итерации.
Запрос для одной итерации
Ты - сценарист компьютерных игр (текстовых квестов).
Описание игрового мира и сюжета:
{common}
Ключевые слова, характеризующие игровой мир: {tags}
Общее описание карты игры:
{map}
Задача: {task}
Название локации - это реалистичное (относительно правил игрового мира) указание места в пространстве (местность, помещение, место) на грамотном литературном русском языке.
Учитывай связи между локациями. Связанные локации должны быть связаны и через названия (например, рубка управления > коридор > каюта - это логичная связь, а чердак дома > подвал - ошибочная, так быть не должно) и соответствовать описанию игрового мира.
Названия соседних локаций не должны быть сильно похожи (иметь одинаковые прилагательные).
Название локации не должно содержать указание на время суток или имя главного героя, оно должно быть коротким (2-3 слова) и соответствовать шаблону "<прилагательное> <существительное> [уточнение, если оно важно]".
Примеры названий:
Рубка капитана
Большая столовая
Тайная тропа
Винный погреб
Межзвездный телепорт
Старая библиотека
Каменный колодец
Прояви креативность, но не отступай от правил игрового мира.
Ответ дай без дополнительных пояснений.
После того, как все названия созданы, одним запросом генерируется список описаний вида этих локаций.
Это единственный этап, который не претерпел значительных правок с момента своего возникновения и работает "как надо".
Аналогичным образом дела обстоят и с NPC (а также и с предметами).
Стартовый "1 запрос" трансформирован в "классический код с элементами ИИ" - последовательная генерация + последовательная же детализация (в отличие от локаций).
Кроме создания описаний локаций и npc на карте должно быть обозначено: перечень закрытых в начале игры локаций, расстановка npc, расстановка предметов.
И опять "одним запросом" не сработало. Основной проблемой стали взаимные блокировки, которые не удавалось устранить даже запросами самопроверки.
Поэтому в итоге весь процесс постепенно тоже ушел в "классическую плоскость". LLM же используется как генератор названий/описаний - на итеративной основе (если LLM повторяет название предмета, несмотря на соответствующую инструкцию "не использовать названия из списка: ...", генерация повторяется со случайным значением температуры).
В качестве позитивного момента хочу отметить, что LLM выкрутится всегда, какой бы абсурдной не казалась ситуация человеку.
В частности, при детализации предметов используются задачи вроде "в локации <Старый колодец> у npc <заяц-садовник> есть предмет, с помощью которого можно обезвредить npc <черный дракон> в локации <Старая башня>. Получив этот предмет, <черный дракон> более не будет мешать главному герою. Придумай название этого предмета, описание его внешнего вида, реакцию <черный дракон> при получении этого предмета".
И оно придумает!
Однако, даже с такими запросами не стоит злоупотреблять универсальностью. Как показала практика, ответы LLM получаются в таких случаях хоть и адекватными, но весьма однообразными.
Особенно, если поручить LLM придумывание ситуаций.
Например, если есть враг - черный дракон, то как при 1 запросе "придумай 5 разных способов преодоления", так и при 5 последовательных, получим примерно одно и тоже - "что-то отдать" и 5 похожих вариантов этого "что-то".
А как же "поговорить"? А неожиданно "победить в бою"?
Скучно же.
Чтобы разнообразить варианты пришлось опять вмешаться руками и формировать запрос к LLM на основе случайных чисел.
Общий алгоритм получается такой:
составляем общий перечень возможных действий (вообще все, что можно)
перемешиваем
берем первые 2-3-4
просим LLM детализировать
Такой же подход приходится применять для вложенных условий ("чтобы <заяц-садовник> отдал главному герою <..>, должно быть выполнено условие: ...").
Геймплей
Собственно весь геймплей состоит всего из 2 шагов:
анализ запроса игрока ("что же он хочет сделать, если формализовать до игровой терминологии?")
реакция на запрос
И вот обо втором шаге и пойдет речь.
Изначально опять же хотелось все решить 1 запросом.
Примерный вариант изначального запроса
Ты - опытный сценарист компьютерных игр в жанре текстовый квест.
Базовое описание игрового мира:
{base}
Фрагмент карты, где находится игрок:
{map}
Инвентарь игрока:
{inventory}
Запрос игрока:
{q}
Задача: придумай ответ на запрос игрока от лица игры, а также опиши модификации игрового мира, которые должны произойти.
Результат оформи в JSON в соответствии с примером:
{...}
Проблемы, всплывшие при использовании такого подхода:
игнорирование возможности (например, в локации лежит бревно (npc), которое можно преодолеть определенными способами (способы перечислены при описании локации) - LLM легко может проигнорировать эти условия и написать "вы успешно ...", а игровая обстановка при этом не изменится)
сочиняет произвольные модификации (перечень того, что может произойти в игровом мире, ограничен и перечислен, но...), которые не могут быть применены
путает атрибутику (например, если игрок хочет осмотреть предмет, действие имеет вид {type: view, subj: <предмет>}, но в ответе LLM может появиться поле subject вместо subj, look вместо view, неполное название предмета, и т.д.)
пишет белиберду в сообщении для пользователя.
Далеко не при каждом запросе эти проблемы проявлялись, но исключить их появление было нельзя.
Поэтому пришлось после шага 1 (формализация) производить анализ события кодом, формировать "дубовый ответ", а потом просить LLM переформулировать его.
Только в этом случае ответы стали в 99% случаев соответствовать ожидаемым.
Малые модели vs большие модели
Отдельно хотелось бы сказать об эффекте перехода с маленькой локальной модели на большую "взрослую". Эффект несомненно есть. Особенно ярко он проявляется в следовании требованиям к формату ответа, точности и адекватности следования инструкциям. Первая большая модель, которую я задействовал - gpt-4o. Но к сожалению "просто переключить на модель побольше" моих проблем не решило. Было реже, менее явно, но все то же самое. Особенно на этапе подготовки игры. Это обстоятельство вызвало немалое удивление и разочарование. Поэтому для себя я решил, что:
даже для большой модели придется хорошо поработать, чтобы качественно ее внедрить
использование моделей необходимо максимально приближать к их сути - предсказание текста, а не ждать от них "разумных рассуждений".
и обратно - хороший результат можно получить и на менее крупных LLM, если постараться (поэтому сначала я успешно перешел на gpt-4.1, а потом и на gpt-4.1-mini)
В качестве итога
Если отбросить романтический флер вокруг ИИ ("сейчас быстренько прикрутим и все у нас само заработает"), а также со скептицизмом воспринимать восторженные слова многочисленных коучей, внедрять ИИ по отдельным этапам процесса, а не сразу как полную замену, то LLM вполне себе могучий инструмент.
И тоже самое справедливо для новых проектов: чем больше будет классического кода - тем лучше, тем полнее и точнее контроль, предсказуемее результаты.
Спасибо за внимание!