Мокрый Тортл Мокрый Тортл Wet Tortle Таверна для героев A tavern for heroes
ТавернаTavern ГеймплейGameplay Как игратьHow to play ПутьJourney ВопросыFAQ Блог Роадмап Играть в TelegramPlay on Telegram

Нейросети для DnD: как 66 слепков живых ходов поймали баг раньше, чем я тронул 19 000 строк pipeline.py

· Владислав Новиков

Нейросети для DnD: как 66 слепков живых ходов поймали баг раньше, чем я тронул 19 000 строк pipeline.py

Golden master (characterization) тесты фиксируют реальное поведение кода как есть, без понимания логики - их записывают до рефакторинга и сверяют результат после правок построчно. Команда ИИ-мастера DnD записала 66 слепков живых ходов с прода, реплей уличил все три намеренно внесённые ошибки в pipeline.py - так тесты стали тросом перед распилом файла на 19 000 строк.

ИИ-мастер DnD - это бот, который ведёт партию сам, без живого мастера за столом. Всё, что бот знает о ходе - деньги, квесты, бой, NPC, предметы - считает один файл, pipeline.py. Файл разросся до 19 000 строк, и трогать его стало страшно: одна правка в денежном канале могла аукнуться в квестах, а починка боя - обнулить награду. Раньше это ловил только игрок, который написал в саппорт “а где мои деньги”. Ребят, вот что мы сделали, чтобы ловить такое до релиза, а не после жалобы.

Что такое golden master тесты и зачем они перед рефакторингом

Golden master (он же characterization) тест не знает, что “правильно” - он фиксирует, что код делает прямо сейчас, и потом сверяет с этим эталоном любые изменения. Термин ввёл Майкл Физерс в книге “Working Effectively with Legacy Code”: такой тест защищает существующее поведение легаси-кода от случайных правок при рефакторинге, когда обычных unit-тестов на это поведение никогда не писали (Wikipedia, Characterization test).

Для ИИ-мастера DnD это буквально трос: прежде чем резать pipeline.py на куски, нужно на чём-то держаться, если резка пойдёт не так.

Почему pipeline.py ИИ-мастера DnD нельзя просто переписать

pipeline.py - 19 104 строки, четверть всего бэкенда бота. Один метод применения хода - 6582 строки, 487 условий if, вложенность в 12 уровней, и всё это - одна пишущая транзакция на 5615 строк. Каналы записи - деньги, квесты, бой, NPC, предметы - не изолированы друг от друга: они общаются через 55 мутабельных атрибутов self, поэтому правка в одном месте может незаметно дёрнуть соседний канал.

Мы уже разбирали, к чему приводит такая связность на практике - например, в истории про архитектурную ошибку, которая пряталась за 120 багами с деньгами: один и тот же корень давал десятки разных симптомов, потому что каналы были перепутаны между собой. С файлом такого размера переписать “набело” за один присест нельзя - слишком много мест, где что-то может тихо сломаться.

Что теряет команда, когда рефакторит легаси без тестов

Монолит уже брал реальную цену - не гипотетическую. На задачи в pipeline.py уходило 0,29 повторного захода (рефикса) вместо базовых 0,1: правка с первого раза не приживалась почти в трети случаев. Второй разработчик-агент простаивал в 15 волнах работы из 17, потому что все правки упирались в один и тот же файл. Полный прогон одного хода гоняли всего 7 тестовых файлов из 509 - остальные проверяли код кусками, не видя картину целиком. Golden-тестов не было вовсе, а более ранняя попытка параллельно переписать пайплайн под флагом (условный “pipeline_v2”) в своё время провалилась - старый и новый код разошлись и правки приходилось вносить дважды.

Похожая ловушка “тесты вроде зелёные, а баг всё равно долетел до игрока” уже разбиралась в статье про живой QA-прогон и скрипт-аудитор ИИ-мастера DnD - зелёный сьют без золотых тестов проверяет только то, что кто-то догадался проверить руками.

Как записали 66 слепков живых ходов ИИ-мастера DnD

Слепок хода - это полный дамп состояния до и после хода: строки кампании и персонажа плюс все ответы LLM записаны как есть, а вся случайность хода - от броска кубика до исхода боя - тоже зафиксирована, а не сыграна заново вслепую. На проде включили флаг записи на паре тестовых аккаунтов, и рекордер набрал 66 слепков по 20 тегам сценариев: покупка, квест, бой, находка монет, неоднозначные команды игрока и другие.

При реплее записанные LLM-ответы и случайность подставляются обратно (в коде это называется ReplayLLM и ReplayRng), а результат сравнивается с записанным дампом через принтер, который убирает несущественный шум вроде порядка ключей. Все 66 слепков плюс 3 технических проверки на корректность самого формата - 69 тестов - проходят зелёным за 25 секунд. Для сравнения: до этого полный прогон одного хода умели воспроизвести только 7 тестовых файлов из 509 - теперь любой из 66 сценариев переигрывается детерминированно, без обращения к живой LLM.

Рядом со слепками завели манифест модулей хода и тест-страж, который сверяет код с манифестом - если кто-то заведёт модуль в обход правил структуры, страж не даст закоммитить. Добавили и pre-commit хук, запрещающий commit -n в обход проверок: слепки бесполезны, если их можно тихо обойти в спешке.

Кстати, про сами LLM-вызовы хода мы уже писали отдельно - там же разбирали, как срезали цену хода на 56% кэшем промпта и переездом на другую модель, если интересно, откуда в одном ходе вообще берутся в среднем 7,4 обращения к LLM.

Как проверили, что трос правда держит, а не просто зелёный

Зелёный сьют ничего не доказывает сам по себе - нужно было убедиться, что тесты действительно ловят поломки, а не молчат из вежливости. Проверили тремя намеренными правками по одной строке кода.

Первая - денежный канал: множитель перевода золота в медь заменили с ×100 на ×10. Упал один слепок из денежной группы каналов - этого было достаточно, план требовал “упадёт хотя бы один”. Вторая - пометка квеста как оплаченного заменена на противоположную: упали сразу четыре слепка в разных сценариях, от находки монет до обещанной награды. Обе поломки трос поймал с первого раза.

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

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

Что дальше: как ИИ-мастер DnD переедет на новую архитектуру

Дальше - 12 срезов, каждый выносит один канал записи (деньги, квесты, бой и так далее) в отдельный модуль. Решили переносить код без параллельного флага “старая версия / новая версия”: гарантией остаются слепки, существующие тесты и ручная проверка, что перенесённое тело функции не изменилось построчно. Флаг с постепенным включением на пользователей планируется только на срезах, где код не просто переносится, а перестраивается - и то со сроком жизни, а не навсегда.

Причина - тот самый провалившийся ранее pipeline_v2: параллельная ветка живёт неделями, а каждую правку приходится вносить в оба места одновременно, что удваивает работу и удваивает шанс разъехаться. Трос из слепков дешевле в поддержке и не создаёт второй копии кода, за которой нужно следить.

Новую структуру решили делать не одним большим модулем, а папкой на каждый канал - деньги, квесты, бой, NPC, предметы, кондиции - и каждый файл внутри не длиннее 400 строк. Модуль на несколько тысяч строк был бы не сильно лучше нынешнего монолита, поэтому граница зафиксирована жёстко, а не “как получится”.

Пока это только фундамент - сам пайплайн ещё не тронут ни строчкой, но теперь под рукой есть 66 свидетелей того, как он ведёт себя на проде прямо сейчас. Дальше будет интереснее - когда первый канал реально переедет в отдельный модуль.

Частые вопросы

Чем golden master тест отличается от обычного юнит-теста?

Юнит-тест проверяет ожидаемый результат по заранее написанной спецификации. Golden master (характеризационный) тест фиксирует фактическое поведение кода как эталон и сравнивает с ним будущие изменения - он не знает, правильно ли текущее поведение, только ловит расхождение.

Зачем нужны тесты перед рефакторингом легаси-кода без покрытия?

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

Как проверить, что тесты реально ловят баги, а не просто зелёные?

Внести намеренную поломку в одну строку кода и прогнать тесты: если ни один не упал - трос дырявый. Так нашли слепой канал урона по герою и добрали отдельный тег под него.

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