Яд проклятия Yes-And
Каждый, кто пытался сделать AI-игру, знает этот момент. Игрок пишет «Я открываю сундук», а LLM отвечает: «В сундуке лежит мифриловый меч +5 и письмо от короля». Красиво, эпично, черт возьми, но в сундуке по сценарию должна быть только ржавая ложка. LLM просто придумала меч, потому что это круто звучит. Это и есть проклятие Yes-And — модель не может удержаться от развития истории, даже если это ломает жесткую логику игры.
Проблема не в том, что LLM «глупая». Проблема в том, что она генеративная. Ее задача — предсказывать следующий токен, а не следовать рельсам гейм-дизайна. В результате AI-RPG превращается в карнавал галлюцинаций: персонажи забывают квесты, предметы появляются из ниоткуда, а порядок событий перекраивается по whim модели. Забудьте про «нейросеть рулит» — в продакшене это ад.
Мы перепробовали кучу подходов: от жестких промптов (бесполезно) до графов состояний (слишком хрупко). Пока не родили гибридный Guard. Три слоя, которые работают как конвейер по вырезанию лжи. Embedding Classifier, микро-LLM Extractor и State Validator. Под капотом — никакой магии, только инженерная паранойя.
Три слоя лжи: зачем три, а не один
Простой фильтр на основе промпта («не выдумывай предметы») — тупик. LLM легко ломает такой фильтр через двусмысленные формулировки. Нужна трехуровневая защита, где каждый слой проверяет ответ под разным углом.
- Embedding Classifier — быстрый семантический барьер. Он определяет, относится ли ответ игрока к разрешенным категориям (действие, диалог, описание) или пытается менять мир без спроса.
- Микро-LLM Extractor — маленькая модель (1-3B параметров) вытаскивает структурированные данные из ответа: кто, что, где, с кем. Это и есть «сократ в кармане».
- State Validator — проверяет экстрагированные факты против текущего игрового состояния. Если персонаж пытается достать из кармана кошель, которого нет в инвентаре — реджект.
Каждый слой может отклонить ответ целиком или запросить его коррекцию. В идеале — вернуть игроку сообщение вроде «Такого предмета у тебя нет. Попробуй другое действие». Но это уже level design, а не наша задача.
Слой первый: Embedding Classifier — злой близнец поиска
Стандартный RAG ищет похожие куски контекста, а наш Embedding Classifier — наоборот: он ищет непохожесть на разрешенные действия. Мы берем легкую модель эмбеддингов (например, all-MiniLM-L6-v2 или gte-small — на 01.06.2026 это еще актуальные звери) и обучаем ее классифицировать намерения игрока: «действие с предметом», «передвижение», «диалог», «исследование», «мета-комментарий». Если эмбеддинг ответа попадает в кластер «мета-комментарий» или «изменение мира» — всё, стоп.
Важно: Embedding Classifier — это не про точность, а про скорость. Он отсекает ~70% явных галлюцинаций за 2-3 мс. Остальное докручивают следующие слои. Не пытайтесь сделать его единственным фильтром — будет много ложных срабатываний.
import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression
# Загружаем модель эмбеддингов (актуальная версия на June 2026)
encoder = SentenceTransformer('all-MiniLM-L6-v2')
# Обучаем простой классификатор на синтезированных данных
# В реальном проекте — больше примеров и баланс классов
X_train = encoder.encode([
"беру ключ со стола",
"иду к воротам",
"спрашиваю у стража про выход",
"осматриваю комнату",
"в моем кармане лежит эльфийский артефакт" # галлюцинация
])
y_train = [0, 0, 0, 0, 1] # 1 - нарушение
clf = LogisticRegression()
clf.fit(X_train, y_train)
def embedding_classifier(text: str) -> bool:
emb = encoder.encode([text])
pred = clf.predict(emb)
return pred[0] == 0 # True если разрешено
Почему не сделать все на одной большой LLM? Потому что latency. В real-time RPG каждый миллисекунда решает. Embedding слой пропускает только явно безопасные варианты, не нагружая следующие этапы.
Слой второй: микро-LLM — сократ в кармане
Второй слой — маленькая LLM (1-3B параметров), которая извлекает из ответа игрока структурированные факты: действие, объект, цель, локация. Зачем нам микро-LLM, если есть GPT-4? Во-первых, цена: каждая проверка должна стоить копейки. Во-вторых, маленькая модель легче контролируется — она обучена только на одной задаче, а не на всем интернете.
На 01.06.2026 лучший компромисс — Llama-3.2-3B-instruct или Phi-3-mini-128k-instruct. Они умеют держать длинный контекст (128k токенов) и дают стабильный structured output через JSON mode. Не советую брать меньше 1B — качество экстракции падает ниже плинтуса.
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
def extract_facts(player_response: str, context: str) -> dict:
completion = client.chat.completions.create(
model="llama-3.2-3b-instruct",
messages=[
{"role": "system", "content": "Извлеки из ответа игрока структурированные факты. Верни JSON с полями: action, target, location, item. Если поля нет - оставь null."},
{"role": "user", "content": f"Контекст игры: {context}\nОтвет игрока: {player_response}"}
],
response_format={"type": "json_object"}
)
return json.loads(completion.choices[0].message.content)
invalid и передать дальше только то, что удалось извлечь. Никогда не игнорируйте ошибки парсинга — это первый шаг к галлюцинации.Слой третий: State Validator — судья с дробовиком
Финальный слой — machine-gun validation. Получив структурированные факты, мы сверяем их с текущим состоянием игры (инвентарь, открытые локации, отношения NPC, прогресс квестов). Если игрок говорит «отдаю зелье королю», а зелья у него нет — реджект. Если говорит «иду в подземелье», а подземелье еще не открыто — реджект. Никакой эмпатии.
State Validator не использует нейросеть. Это чистый код на Python с pydantic или jsonschema. Он должен быть детерминированным и предсказуемым. LLM может ошибиться, обычный код — нет.
from pydantic import BaseModel
from typing import Optional
class GameState(BaseModel):
location: str
inventory: list[str]
quest_flags: dict[str, bool]
def validate_action(facts: dict, state: GameState) -> bool:
# Пример: проверка наличия предмета
if facts.get("item") and facts["item"] not in state.inventory:
return False
# Проверка локации
if facts.get("location") and facts["location"] != state.location:
# Если игрок пытается переместиться в недоступную локацию
return False
return True
Здесь же можно проверить, не пытается ли игрок выполнить действие, заблокированное квестовым флагом. Например, «открыть сундук» можно только после разговора с ключником. State Validator — это фактически авторитарный бэкенд, о котором мы писали в статье Архитектура AI RPG: авторитарный бэкенд. Без него LLM просто игнорирует правила.
Собираем Frankenstein: пайплайн инференса
Теперь объединяем три слоя в один пайплайн. Embedding Classifier — быстрый проход. Если ок, то микро-LLM извлекает факты. Если факты валидны — State Validator. Если все три слоя дали зеленый, ответ игрока применяется к игровому миру. Иначе — сообщение об ошибке и запрос коррекции.
def guard_pipeline(player_response: str, context: str, state: GameState) -> bool:
# Layer 1
if not embedding_classifier(player_response):
return False
# Layer 2
facts = extract_facts(player_response, context)
if "error" in facts:
return False
# Layer 3
if not validate_action(facts, state):
return False
return True
В продакшене мы добавили еще ретрай-механизм: если ответ заблокирован, даем игроку три попытки переформулировать, подсказывая на каком слое затык. Это уменьшает фрустрацию и дает LLM шанс «исправиться» (да, она не знает о пайплайне, но контекст подсказки — ваше секретное оружие).
Грабли, которые мы собрали
Первая и главная ошибка — делать Embedding Classifier единственным Gate. Он отсекает явные галлюцинации, но пропускает тонкие: когда игрок говорит «я достаю волшебную палочку», а в инвентаре нет палочки, но есть «странная деревяшка». Эмбеддинговая модель не поймет подмену понятий. Поэтому микро-LLM обязателен.
Вторая грабля — игнорировать latency микро-LLM. Разверните её на fast inference сервере (vLLM, TensorRT-LLM) с динамическим батчингом. На CPU она будет выдавать ответ секунду — для RPG это много. Используйте GPU 40x A100s (или хотя бы одну L40S) — это окупается.
Третья — не тестировать на «слепых» сценариях. Мы запускали стресс-тесты, где боты общались друг с другом (да, тот самый эксперимент с 13 LLM), и выяснили, что агенты научились обходить Guard через метафоры («у меня с собой немного магии» — классификатор пропускал, а state validator не мог проверить «магию», потому что её нет в инвентаре). Пришлось добавить blacklist для абстрактных концепций.
Бонус: регрессия на живых логах
Без системы тестов гибридный Guard превращается в тыкву. Каждое обновление микро-LLM или добавление нового типа действий ломает пайплайн. Мы завели директорию fixtures/ с сотнями пар «запрос -> ожидаемый вердикт». Прогоняем после каждого деплоя. Сравниваем метрики: precision (сколько истинных нарушений поймали) и recall (сколько пропустили). Без этого нельзя. Подробнее про паттерн Triage → Gate → Voice — читайте в статье Как избежать галлюцинаций LLM-бота в саппорте, там отличная метафора для тестирования.
Два слова напоследок (без клише)
Многие считают, что LLM рано или поздно «научатся» не галлюцинировать. Нет. Это фундаментальное свойство генеративных моделей. Ваша задача — не исправить LLM, а окружить ее такими костылями, чтобы она не могла навредить. Гибридный Guard — не серебряная пуля, но он переводит AI-RPG из разряда «демки» в разряд «прототипа, который можно показывать продюсеру». Если добавить сюда самовосстанавливающийся RAG (см. статью про самовосстанавливающийся RAG), то шанс пройти QA возрастает до 80%. Остальные 20% — это галлюцинации, которые вы никогда не увидите, пока не выпустите игру в прод. И это нормально.