ДнД-бот и LLM-судья для денег: приёмка на 5497 ходах прода, три бага дошли бы до кошелька

Флаг ECONOMY_GATE_ENABLED на проде выключен: LLM-арбитр для денег ДнД-бота прошёл офлайн-приёмку на 5497 ходах прод-корпуса - 2,51% триггеров, 74% покрытия, ноль галлюцинаций и три бага, которые дошли бы до кошелька игрока.
Ребят, продолжаю историю с единой кассой. Из прошлой статьи про архитектурную ошибку в деньгах вы знаете, что касса теперь пишет кошелёк игрока один раз за ход вместо восьми конкурирующих каналов, которые раньше писали его каждый сам. Но касса решает только то, что ей подали детекторы. А что делать с ходом, где ни один детектор не подал заявку, хотя по прозе деньги явно должны были уйти или прийти? Для таких случаев в системе появился резервный судья - LLM-арбитр. Прежде чем выпускать его на живых игроков, я прогнал офлайн-приёмку на 5497 ходах прод-корпуса. Разбираю, что она показала - и что чуть не дошло бы до кошелька.
Зачем ДнД-боту LLM-судья, если касса уже есть
LLM-арбитр - это резервный канал: он не разбирает каждый ход, а включается только когда штатные детекторы промолчали, хотя проза хода прямо говорит про деньги. Работает арбитр вне пишущей транзакции кассы и подаёт такую же заявку, что и обычный канал - просто с опозданием и под более жёсткими ограничениями.
Детекторы в пайплайне заточены под конкретные обороты прозы: “купил”, “заплатил”, “отдал”. Но живая проза шире правил. Игрок может написать “высыпаешь тридцать золотых”, а мастер-бот - отыграть сделку, ни разу не назвав сумму цифрой. Детектор молчит, касса ничего не проводит, а по смыслу хода деньги должны были поменять владельца. Раньше это тихо терялось. Теперь под такие ходы заведено три триггера: silent_debit - списание без явного детектора, silent_credit - начисление без детектора, act_without_amount - акт передачи есть, а суммы нет.
Как проверить фичу до включения, а не после
Прямая проверка на деньгах живых персонажей не вариант, поэтому приёмка идёт офлайн-репреем: скрипт economy_gate_replay.py читает прод-корпус ДМ-ходов за 14 дней в режиме read-only и прогоняет по нему логику гейта, не трогая боевую базу ни одной записью. Это не то же самое, что “зелёные” юнит-тесты - тесты проверяют, что код делает то, что задумано, а репрей проверяет само задуманное поведение на реальных, а не придуманных ходах.
Выборка - 5497 ДМ-ходов. Ходы с воскрешением персонажа из выборки выкинули по трём маркерам (системная строка, спасбросок от смерти, характерный лексикон) - воскрешение это отдельный контур денег, не про торговлю и лут. На проде фича до сих пор спрятана за флагом ECONOMY_GATE_ENABLED, и он выключен: включение - отдельное решение после этого репрея и финального ревью ветки, а не автоматический шаг сразу после кода.
Как часто вообще нужен арбитр: 2,51% ходов
На 5497 ходах прод-корпуса детерминированный триггер арбитра сработал 138 раз - это 2,51% ходов при пороге приёмки в 10%. Тест пройден с большим запасом. Из этих 138: 74 хода - silent_debit, 39 - act_without_amount, 25 - silent_credit.
Двузначный запас важен не только для отчётности. Чем реже вызывается арбитр, тем меньше лишних платных обращений к модели на ход, который касса и так закрыла своими каналами - LLM-судья задуман как редкий резервный контур, а не как обязательная проверка каждого хода. Раньше я уже разбирал, что ход ИИ-мастера сам по себе не бесплатный, поэтому вызывать тяжёлый резервный канал на 97,5% ходов, где и так всё разложено детекторами, было бы просто лишними деньгами продукта.
Списания теряются заметно чаще начислений - 74 против 25 на silent-триггерах. Это логично: игрок скорее промолчит, что у него что-то забрали, чем не заметит, что ему что-то дали.
Насколько арбитру можно верить: 74% покрытия и 16% ложных сработок
На выборке 100 размеченных денежных ходов арбитр - тот же промпт и та же модель, что и в проде - нашёл 20 из 27 реальных денежных событий: покрытие 74%. Из 73 его предложений 12 оказались ложными - false_proposal_rate 16%. Галлюцинаций - ноль, дословность цитаты из прозы (evidence) - 100%.
Разметку выборки делал отдельный агент, а лид вручную сверил 10 ходов - чтобы не проверять разметку тем же контуром, который потом эту разметку и использует. Похожая история с калибровкой ложных срабатываний уже вставала при настройке валидатора ответов бота: там тоже пришлось подбирать порог, чтобы не глушить обычные, ничем не подозрительные ходы. Ноль галлюцинаций при этом - не подарок, а следствие жёсткого правила: заявка арбитра проходит, только если её evidence дословно (после нормализации пробелов) встречается в прозе хода. Не встречается - событие отбрасывается, что бы модель ни насочиняла.
Что из расхождений реально дошло бы до кошелька
19 расхождений между разметкой и ответами арбитра разобрали отдельно - прогнали каждое через реальный прод-код целиком: триггер → вето → клэмп → settle. Результат неожиданно оптимистичный: 10 из 19 на проде вообще не воспроизводятся - триггер бы попросту не сработал. Остальные разложились на три класса промахов: ретроспектива (5 случаев), обещание или названная цена приняты за фактическую передачу (5), сумма без номинала (7).
До кошелька игрока реально дошли бы ровно три: ложный дебет на 30 золотых (“высыпаешь тридцать золотых” - а трактирщик деньги не принял, сделка не состоялась) и два кредита по 10 золотых с выдуманным номиналом (“горсть монет”, “десяток монет” - арбитр додумал число там, где его не было). Отсюда два конкретных must-fix для промпта арбитра: если адресат не принял передачу - статус refused, а не settled; если число в прозе не привязано к номиналу - amount_known=false, и деньги не проводятся вовсе.
Отдельно в корпусе нашёлся живой пример двойного счёта: одни и те же 3 медяка всплыли на двух смежных ходах одной кампании - “три медяка, страж” и следом “три медяка потрачены не зря”. Именно для таких повторов касса дедупит заявки по event_id, а не полагается на то, что арбитр (или любой другой канал) сам заметит, что уже это считал.
Что нашло финальное ревью перед включением
Метрики репрея - не последний рубеж. Финальное ревью пакета в 492 КБ диффа добавило два класса находок, которых сами метрики не ловят. Critical: агрегация кредитных заявок по ключу “сцена + сумма + текст хода” могла случайно снять второй одинаковый лут или квест как дубль - то есть законное повторное вознаграждение выглядело так же, как повтор одной и той же заявки. Important: кредит арбитра мог обойти анти-абьюз бэкстоп, а у дебета не было потолка - теоретическая дыра под инъекцию суммы через текст самого игрока. Оба фикса ушли в код до включения флага: потолок дебета теперь привязан к доверию NPC, а сумма обязана буквально встречаться в предложении вместе с evidence, а не просто где-то в ходе.
Побочный, но честный итог: при включённом флаге в тестовом прогоне легитимно упали два старых теста. Касса оказалась строже, чем канал, который она заменила - дубль теперь отсекается по event_id, а не по признаку конкретного канала, который раньше был мягче. Тесты не подгоняли под зелёный статус, а зафиксировали расхождение как отдельный concern - потому что “тесты стали красными” и “касса теперь работает правильнее” здесь одно и то же.
Что дальше
Пакет - 17 задач плана, исполненных субагентами (сложную логику кассы и арбитра вела одна модель, врезки в общий пайплайн - другая, с ревью после каждой задачи), больше 30 коммитов в ветке и итоговое ревью diff’а в 492 КБ. Флаг ECONOMY_GATE_ENABLED на проде по-прежнему выключен: включение - отдельное решение после того, как репрей и финальное ревью прошли, а не что-то, что происходит само по факту готового кода.
Три бага, которые реально добрались бы до кошелька игрока, - не ноль, но и не сто двадцать, как было в прошлый раз) Мокрый Тортл явно считает деньги игроков аккуратнее, чем я в своё время считал собственные.
Частые вопросы
Что делает LLM-арбитр в экономике ИИ-мастера DnD?
Это резервный канал, который проверяет ход, только когда штатные детекторы не подали заявку на списание или начисление денег, хотя по прозе сделка явно произошла - так система ловит редкие пропуски, не трогая обычные ходы.
Почему фичу проверяли до включения, а не после запуска на игроках?
Потому что офлайн-репрей прод-корпуса не рискует деньгами живых персонажей: скрипт читает сохранённые ходы в режиме read-only и прогоняет по ним логику гейта, не проводя ни одной реальной транзакции.
Что случится, если арбитр ошибётся и спишет деньги, которых не было?
У кассы есть страховки поверх арбитра: заявка проходит, только если цитата из прозы совпадает дословно, есть потолок списания и повторная сверка - поэтому даже ложное предложение арбитра не обязательно попадает в кошелёк.
Как готовился материал: черновик собирает мой ИИ-конвейер по темам из практики таверны, факты сверяются с первоисточниками и правилами, финальную версию я читаю и правлю руками перед публикацией. Обложку тоже рисует нейросеть. Про кухню — на странице о проекте.