Почему LLM — плохой гейм-мастер (но вы всё равно хотите его использовать)
Вы запускаете игру. LLM генерирует описание замка, полного скелетов. Игрок надевает кольцо невидимости. В следующем раунде LLM уже не помнит про кольцо. Скелеты атакуют. Игрок в ярости. Знакомо?
Проблема не в том, что модель глупая. Проблема в самой архитектуре: мы просим LLM быть одновременно и storyteller, и базой данных. А она — только storyteller. И крайне ненадёжная база данных.
В этой статье я покажу, как построить авторитарный бэкенд для AI RPG. Бэкенд, который хранит истину, а LLM использует только для генерации текста и принятия решений в строго определённых рамках. Никакой амнезии. Никаких галлюцинаций, которые ломают сюжет.
Эту статью стоит читать, если вы уже пробовали делать AI RPG через один гигантский промпт и устали чинить логические дыры.
Проклятие контекстного окна
Допустим, у вас игра в духе D&D с 10 сессиями. В каждой сессии ~10 тысяч токенов истории. Суммарный контекст — 100k токенов. Даже GPT-4 (128k контекста) начнёт "забывать" детали из первой сессии. Это не баг, это физика. Attention-механизм рассеивается. Модель помнит "есть кольцо невидимости", но не помнит, что оно одноразовое и уже сломалось.
Традиционное решение — RAG (извлекаем релевантные факты из векторной базы). Но RAG добавляет задержку, шум в выдаче, и всё равно не гарантирует, что модель использует факты. Модель может проигнорировать даже чётко написанный факт в промпте. Эти эффекты подробно разбираются в статье LLM понимают цель, но игнорируют её.
Ошибка новичка: пытаться решить проблему амнезии, увеличивая контекстное окно. Это лечит симптомы, а не причину. Причина — модель не должна хранить состояние игры.
Авторитарный бэкенд: кто здесь главный
Идея проста: всё игровое состояние (инвентарь, здоровье, координаты, квесты, отношения NPC) хранится в реляционной или графовой базе. Бэкенд решает, что именно LLM должна знать в текущий момент. LLM получает тщательно отфильтрованные факты + текущий запрос игрока. Её задача — только сгенерировать текстовую реакцию, не противоречащую фактам.
Это перекликается с подходом гибридного AI, описанным в статье Гибридный AI: как объединить детерминированный анализ и LLM. Только там бизнес-логика, а здесь — игровая логика.
1 Определяем модели данных
База: PostgreSQL + pgvector (для семантической памяти). Основные сущности:
- Player — id, имя, уровень, опыт.
- Location — id, название, описание, координаты.
- Item — id, название, тип, свойства (JSONB).
- Quest — id, название, статус (active/completed/failed), этапы.
- Memory — id, вектор (embedding), текст, важность (float), тег.
Важность — ключевое поле. Она определяет, какие воспоминания попадут в контекст LLM при нехватке места.
2 Structured Output: жёсткий контракт на действия LLM
Мы не позволяем LLM писать "как ей вздумается". Каждый ответ — строгий JSON с полями: narrative (текст для игрока), actions (массив изменений состояния, которые LLM предлагает). Бэкенд проверяет и применяет эти изменения.
Для этого используем библиотеку Instructor (Python) или Outlines. Они берут Pydantic-схему и заставляют LLM вернуть валидный JSON. Если модель галлюцинирует поле, которого нет в схеме — ответ отбрасывается.
from pydantic import BaseModel
from typing import List, Optional
class GameAction(BaseModel):
action_type: str # "move", "take", "attack", "speak"
target: str
parameters: dict = {}
class LLMResponse(BaseModel):
narrative: str
actions: List[GameAction]
location_change: Optional[str] = None
# Пример вызова с Instructor
import instructor
from openai import OpenAI
client = instructor.patch(OpenAI())
response = client.chat.completions.create(
model="gpt-4o-mini",
response_model=LLMResponse,
messages=[
{"role": "system", "content": "Вы — гейм-мастер. Игрок в подземелье. У него есть: {items}."},
{"role": "user", "content": "Я осматриваю сундук"}
]
)
print(response.actions) # [GameAction(action_type='take', target='gold_coin', parameters={'amount': 50})]
Почему это работает. LLM больше не может "случайно" убить игрока, если у неё нет права на action_type "damage". Бэкенд решает, какие action_type вообще разрешены в данной ситуации.
3 Управление памятью: pgvector и система важности
Каждое событие в игре ("игрок взял меч", "убил гоблина", "поговорил с королём") превращается в embedding (через text-embedding-3-small) и сохраняется в таблице memories. Поле importance вычисляется на основе: (1) релевантность активному квесту, (2) новизна, (3) частота упоминаний.
Перед каждым запросом к LLM бэкенд собирает контекст:
- Текущая локация, инвентарь, активные квесты (из PostgreSQL).
- Последние N сообщений игрока (краткосрочная память).
- Топ-K воспоминаний из pgvector, семантически похожих на запрос и сортированных по важности.
Если размер контекста превышает лимит (например, 6000 токенов), обрезаем наименее важные воспоминания. Это жёсткий алгоритм, без участия LLM.
Боевой пример: FastAPI + pgvector
Соберём минимальный эндпоинт для обработки сообщения игрока:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncpg
import openai
app = FastAPI()
class PlayerMessage(BaseModel):
player_id: int
message: str
@app.post("/act")
async def handle_action(msg: PlayerMessage):
# 1. Получить состояние игрока из БД
state = await get_player_state(msg.player_id)
# 2. Собрать релевантные воспоминания
memories = await query_memories(msg.player_id, msg.message, top_k=5)
# 3. Сформировать системный промпт с фактами
prompt = build_prompt(state, memories, msg.message)
# 4. Вызвать LLM с structured output
response = await call_llm(prompt)
# 5. Валидировать действия (например, проверить, что предмет есть в инвентаре)
validated = validate_actions(response.actions, state)
# 6. Применить изменения к БД
await apply_changes(msg.player_id, validated)
# 7. Сохранить воспоминание о текущем событии
await save_memory(msg.player_id, f"Игрок: {msg.message}, Ответ: {response.narrative}")
return {"narrative": response.narrative}
Функция validate_actions — это ваша guard-система. Если LLM предлагает игроку взять "меч дракона", а игрок мёртв — действие отклоняется, и LLM просят перегенерировать ответ с пометкой "этот предмет недоступен".
Типичная ошибка: доверять LLM самостоятельно изменять состояние. Я видел проекты, где модель могла вызывать SQL-запросы напрямую. Результат: игроки получали по 1000 HP и легендарные артефакты бесплатно. Никогда не давайте LLM прямой доступ к базе.
Как не надо делать: классический антипаттерн
Вот что я видел в коде новичков: один гигантский системный промпт с детальным описанием мира, инвентаря и всех квестов. Игрок пишет "сделай меня королём" — и LLM думает: "Почему бы и нет? Это интересный поворот". Модель забывает, что персонаж — крестьянин. Мир ломается.
Вместо этого — авторитарный бэкенд проверяет все изменения. Если LLM предлагает сделать игрока королём, бэкенд проверяет: есть ли у игрока корона, признал ли его совет? Если нет — ответ: "Вы не можете стать королём. Сначала найдите корону и заручитесь поддержкой знати". LLM подчиняется.
Борьба с амнезией через structured output
Даже с контекстом LLM может "забыть" использовать предмет. Structured output спасает, потому что мы можем принудительно включить в ответ поле use_item и явно указать, какие предметы доступны. Но есть нюанс: если в контексте нет упоминания предмета, модель может его не предложить. Поэтому мы добавляем в промпт секцию "Активный инвентарь: {list}".
Метод Agent Skills (см. статью Agent Skills: как упаковать знания для LLM-агентов) предлагает выносить знания в отдельные модули. В нашем случае — это навык "инвентарь", навык "квесты" и т.д. LLM выбирает, какой навык применить, и бэкенд подгружает соответствующую схему.
Что по производительности
Основная задержка — вызов LLM. Запросы к pgvector (с HNSW-индексом) в 10 раз быстрее, чем типичный LLM-вызов. Валидация JSON — микросекунды. Поэтому узкое место — модель. Используйте локальную модель (например, Llama 3.2 70B через vLLM) для экономии, если не нужна высокая креативность. Для сложных сюжетов — GPT-4o mini или Claude 3.5 Sonnet (на май 2026 — уже есть Claude 4, но цены кусаются).
Неочевидный совет (вместо заключения)
В конце 2025 года вышла статья о том, как 13 LLM самоорганизовались в картель (читайте Как AI-боты самоорганизовались в картель). Мораль: если дать LLM свободу, они могут начать сговариваться против игрока. Авторитарный бэкенд — не только про память, но и про безопасность. В 2026 году, когда мультиагентные RPG станут мейнстримом, это будет главным мерилом качества игры.
Совет напоследок: начните с простого прототипа — FastAPI + SQLite + Instructor. Переход на pgvector — когда станет больно от задержек RAG. И никогда не верьте LLM, когда она обещает "помнить всё".