Как мы боремся с галлюцинации AI Master: гибридный Guard на Embedding + LLM Extractor на примере AI-RPG «Стирая Грань»

от автора

Каждый, кто пробовал создавать текстовые RPG или симуляторы на базе LLM (будь то GPT-4, DeepSeek или локальная 70B), сталкивался с проблемой «Yes-And» проклятия. По своей природе современные языковые модели — это идеальные импровизаторы. Они обучены поддакивать пользователю и развивать его мысль.

В контексте игры это превращается в легальные читы. Игрок пишет: «Я достаю из кармана дымовую шашку и кидаю в охрану» или «Вообще-то я полковник ФСБ, пропустите». Что делает классический AI GM? Он послушно кивает: «Охрана кашляет в дыму, вы проходите», даже если по Game State игрок — бродяга в одних трусах, у которого в инвентаре только ржавый гвоздь.

Меня зовут Алексей, я профессиональный 1С-разработчик, но в свободное время создаю архитектуру инди-системы на Flutter и Python. Сейчас я развиваю проект нарративной AI-driven RPG «Стирая Грань» (анг. — Beyond the Verge).

Для тех, кому интересно посмотреть на стек в действии или поучаствовать в тестировании механик, мы развернули официальный сайт Beyond the Verge. А все самые свежие патчи я выкладываю в профильном Telegram-канале Beyond the Verge. Присоединяйтесь, там много технической внутрянки.

В этой статье я подробно расскажу, почему классический Prompt Engineering здесь бессилен, как наши первые регулярные выражения превратились в бесконечную игру «бей крота» (whack-a-mole) и как мы в итоге построили надежную трехслойную систему защиты: Embedding Classifier → Микро-LLM Extractor → State Validator.

1. Эволюция проблемы: Почему регулярки умерли на проде

Проклятие "Yes-And"

Проклятие «Yes-And»

На ранних этапах защита выглядела наивно — жесткий prompt-инжиниринг вида: «Не придумывай предметы за игрока, проверяй инвентарь». Это отсекало около 20% банальных атак. В остальном модель успешно игнорировала системный промпт, увлеченная генерацией красивой истории.

Тогда появился player_claim_guardверсии 1. Это был классический Regex-вышибала. Он искал конструкции типа «у меня есть Х», «достаю Y», «беру Z».

Почему это превратилось в ад:

  1. Богатство русского языка. Падежи, склонения, синонимы. Игрок пишет: «предъявляю документы». Глагола «предъявляю» не было в regex-паттернах — атака прошла успешно.

  2. Контекстуальный обход. «Из кармана плаща выпадает дымовая шашка, я поднимаю её». Формально игрок не «достал» её, она «выпала». Regex молчит.

  3. Разнообразие типов обмана. Помимо предметов (Possession), игроки начали симулировать связи («Я знаю начальника этой тюрьмы»), знания («Я помню пароль от этого сейфа») или права («У меня есть устное разрешение губернатора»).

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

2. Таксономия AI Yes-And провалов

Векторы Ректона

Векторы Ректона

Чтобы победить врага, его нужно классифицировать. Мы выделили 12 базовых категорий, с помощью которых игрок пытается переписать правила мира (Реткон):

#

Категория

Суть атаки

Как проверяем в Game State

1

Possession

Материализация предметов / денег

Наличие в inventory[] или resources[]

2

Identity

Присвоение фейковой роли / биологии

Проверка character.background / class

3

Relationships

Симуляция лояльности NPC

Проверка списка фракций и companions[]

4

Knowledge

Знание кодов, тайн, паролей

Наличие записей в notes[] или квестах

5

Authorization

Наличие разрешений, пропусков

Наличие физического документа в инвентаре

6

Abilities

Внезапное появление навыков («я умею взламывать»)

Проверка character.skills[]

7

Past events

Переписывание истории («мы же вчера договорились»)

Сканирование лога выполненных событий (chronicles)

8

Location access

Наличие ключей/доступа к закрытым зонам

Keyword-фильтр по инвентарю на ключи/карты

9

Transport

Появление транспорта из воздуха

Проверка состояния глобальной логистики

10

Communication

Вызов подкрепления по рации/телефону

Наличие средств связи в инвентаре

11

Time pressure

Искусственный таймер («взорвется через минуту»)

Prompt-level guard (сверка с хрониками)

12

Meta-game

Слом 4-й стены («это просто сон, я просыпаюсь»)

Prompt-level guard

В первой итерации архитектуры v2 мы взяли топ-5 категорий, которые покрывают 90% реальных эксплойтов на проде, а затем расширили систему до поддержки всех 12.

3. Архитектура Claim Guard v2: Три слоя защиты

Архитектура

Архитектура

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

Слой 1: Векторный ClaimClassifier (ONNX на бэкенде)

Когда игрок отправляет действие, оно попадает в claim_classifier. Наша задача — моментально понять, есть ли в тексте вообще хоть какой-то намек на претензию .

  • Модель: Локальная intfloat/multilingual-e5-base в ONNX-обертке. 0 токенов, отличная поддержка русского и английского языков.

  • Механика: При старте сервера мы один раз вычисляем эмбеддинги для 12 «эталонных» описаний (Anchors). Например, для identity эталон звучит так: «я являюсь работаю служу офицер агент врач детектив член гильдии дворянин».

  • Расчет: Вычисляем Cosine Similarity вектора действия игрока против 12 эталонов.

  • Перформанс: Занимает ~5ms (с кэшированием — <1ms).

  • Результат: Если ни одна категория не пробила порог (threshold = 0.85), мы со спокойной душой пускаем ход по быстрому пути (Hot Path) без затрат на LLM.

Слой 2: Микро-LLM Extractor (Structured JSON)

Если классификатор кричит: «Внимание, тут Possession (0.88) и Identity (0.87)!», включается второй слой — claim_extractor. Классификатор не знает, какой именно предмет достал игрок, он лишь зафиксировал факт. Нам нужно извлечь сущность.

Мы отправляем в OpenAI-совместимый бэкенд микроскопический промпт (~100 токенов ввода) с требованием вернуть строгий JSON-массив:

Extract all player claims about possession, identity, relationships, knowledge,or authorization from this text. Return JSON array. Each entry:{category: "possession"|"identity"..., entity: "what is claimed", player_text: "exact words"}Player text: "я офицер ФСБ, предъявляю удостоверение и иду в архив."

На выходе получаем чистый валидный массив сущностей:

[  {"category": "identity", "entity": "офицер ФСБ", "player_text": "я офицер ФСБ"},  {"category": "possession", "entity": "удостоверение", "player_text": "предъявляю удостоверение"}]

Latency этого шага — ~200ms (модель генерирует всего 50-100 токенов).

Слой 3: ClaimStateValidator (In-memory жесткая логика)

Теперь у нас есть конкретные сущности. Мы передаем их в claim_state_validator, который работает по классическим хардкодным правилам, без всяких нейросетей:

  1. Категория Identity: Берем из JSON entity="офицер ФСБ". Идем в state.character.background. Видим там "бывший солдат". Идем в character.class"воин". Совпадений по overlap слов нет. Статус: UNVERIFIED.

  2. Категория Possession: Берем entity="удостоверение". Запускаем Fuzzy Match (с учетом стемминга и падежей) по массиву inventory[]. Находим предмет {name: "Удостоверение офицера"}. Статус: VERIFIED.

4. Двухэтапное подавление: Pre-Turn Hint + Post-Turn Block

Что делать с сущностями, которые получили статус UNVERIFIED? Мы бьем по AI GM с двух сторон:

Атака спереди (Pre-Turn Guard Injection)

Перед тем как основной AI-мастер начнет генерировать ответ на ход игрока, мы незаметно подмешиваем в его System Prompt динамический контекст:

[GUARD INJECTION]: Игрок утверждает, что он 'офицер ФСБ'. Эта идентичность НЕ подтверждена в Game State. Игрок врет или симулирует. Отыграй подозрение или провал обмана. НЕ выдавай игроку материальные блага.

В 95% случаев после такого явного пинка DeepSeek собирается и выдает шикарный нарратив: бармен кривится, заявляет, что «корочки у тебя липовые», и вызывает настоящую охрану.

Защита сзади (Post-Turn Коррекция)

Но что, если AI GM проигнорировал хинт? Такое бывает на слабых или слишком «творческих» моделях. Модель в тексте пишет: «Ну ладно, верю тебе, держи ключ от архива» и дергает ручку бэкенда inventory_found: ["ключ от архива"].

Здесь включается Post-Turn Guard в campaign_runtime. Он перехватывает массив изменений, который сгенерировал AI, и сверяет его со списком UNVERIFIED клеймов из нашей структуры данных.

if is_claim_unverified(possession_audit, item_name):    logger.warning(f"possession_guard_blocked: {item_name}")    continue # Удаляем предмет из выдачи, спасая базу данных от загрязнения

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

5. Обработка краевых кейсов и отказоустойчивость

Любая распределенная система ломается. Как мы подстраховались?

  • Метафоры: Игрок пишет «Я как рыба в воде в этом здании». Классификатор может выдать identity с низким скором. LLM-экстрактор отфильтрует это в пустой массив [], и ход пройдет без задержек.

  • Битый JSON от экстрактора: Если Микро-LLM сошла с ума и выдала невалидный синтаксис, система делает ровно 1 повторный запрос (retry). Если и он падает — мы элегантно откатываемся (Graceful Degrade) на старый добрый v1 Regex Fallback. Игра не ломается,latency не растет, но мы остаемся под защитой базовых регулярок.

  • Эффект масштаба: На «чистых» ходах без клеймов (~70% игрового времени) оверхед по времени составляет всего ~5ms на векторный поиск. На ходах с клеймами — ~210ms, что абсолютно незаметно, так как генерация основного ответа AI GM все равно занимает 2–4 секунды.

Вывод

Попытка контролировать поведение LLM в играх исключительно промптами — это бесконечная война, в которой разработчик всегда проиграет искушенному игроку. Единственный жизнеспособный путь — выносить валидацию логики за пределы LLM.

Разделение конвейера на дешевые векторные эмбеддинги, специализированную микро-LLM для структурирования и жесткий memory стейт-валидатор позволило нам полностью закрыть дыры в безопасности игрового мира, сохранив при этом гибкость и красоту AI-нарратива.

А как вы боретесь с Yes-And поведением моделей в своих проектах? Пишите в комментариях, обсудим.

ссылка на оригинал статьи https://habr.com/ru/articles/1042402/