Проблема: Почему классические RAG-системы не подходят для настолок?
Представьте ситуацию: вы купили новую сложную настольную игру вроде "Терры Мистики" или "Глоссария". Правила занимают 50 страниц PDF, а друзья уже ждут начала игры. Классические RAG-системы типа LangChain или LlamaIndex — это монстры, которые требуют десятки зависимостей, много памяти и сложной настройки. Они созданы для enterprise-решений, а не для быстрого ответа на вопрос "Как проходит фаза действий в "Свинтусе"?"
Ключевая проблема: большинство RAG-фреймворков создают избыточную сложность. Они заточены под корпоративные сценарии с тысячами документов, а не под наш скромный кейс с парой PDF-файлов правил.
Решение: Минималистичный стек для конкретной задачи
Вместо того чтобы тащить в проект тяжеленные библиотеки, мы соберём систему из трёх лёгких компонентов:
- PocketFlow — для работы с векторными эмбеддингами и поиска
- BlackSheep — для создания быстрого веб-API
- ObjectBox — для локального хранения векторов и метаданных
Этот стек даёт нам всё необходимое без лишнего веса. Если вам интересны более сложные RAG-архитектуры, посмотрите мою статью про Production-ready AI-агент с нуля, но для нашего кейса достаточно простого подхода.
Пошаговый план сборки
1Подготовка окружения и данных
Сначала создадим виртуальное окружение и установим минимальные зависимости:
python -m venv venv
source venv/bin/activate # или venv\Scripts\activate на Windows
pip install pocketflow blacksheep objectbox sentence-transformers pdfplumberТеперь подготовим PDF с правилами игр. Я рекомендую начать с 2-3 популярных игр, чтобы протестировать систему. Для работы с PDF отлично подходит pdfplumber — он легче PyPDF2 и лучше сохраняет структуру текста.
import pdfplumber
import json
def extract_rules_from_pdf(pdf_path):
"""Извлекаем текст из PDF с правилами игры"""
chunks = []
with pdfplumber.open(pdf_path) as pdf:
for page_num, page in enumerate(pdf.pages, 1):
text = page.extract_text()
if text:
# Разбиваем на абзацы
paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
for para in paragraphs:
chunks.append({
'text': para,
'page': page_num,
'game': pdf_path.stem
})
return chunks2Создание векторной базы знаний
Вместо тяжёлых векторных БД вроде Pinecone или Weaviate используем ObjectBox — лёгкую embedded базу данных. PocketFlow поможет с эмбеддингами:
from pocketflow import PocketFlow
from sentence_transformers import SentenceTransformer
import objectbox
class GameRulesDB:
def __init__(self, db_path='game_rules.obx'):
# Используем лёгкую модель для эмбеддингов
self.embedder = SentenceTransformer('all-MiniLM-L6-v2')
# Инициализируем PocketFlow для поиска
self.flow = PocketFlow(dim=384) # Размерность нашей модели
# Настраиваем ObjectBox
model = objectbox.Model()
entity = model.entity('RuleChunk', 1)
entity.property('id', objectbox.PropertyType.long, 1, True)
entity.property('text', objectbox.PropertyType.string, 2)
entity.property('embedding', objectbox.PropertyType.byteVector, 3)
entity.property('game', objectbox.PropertyType.string, 4)
entity.property('page', objectbox.PropertyType.int, 5)
self.store = objectbox.Store(model=model, directory=db_path)
self.box = self.store.box('RuleChunk')
def add_rules(self, chunks):
"""Добавляем чанки с правилами в базу"""
for chunk in chunks:
embedding = self.embedder.encode(chunk['text'])
# Сохраняем в ObjectBox
rule = RuleChunk()
rule.text = chunk['text']
rule.embedding = embedding.tobytes() # Сохраняем как bytes
rule.game = chunk['game']
rule.page = chunk['page']
self.box.put(rule)
# Добавляем в PocketFlow для быстрого поиска
self.flow.add_item(rule.id, embedding)Почему именно такая архитектура? ObjectBox хранит метаданные (название игры, страницу), а PocketFlow отвечает за быстрый поиск по векторам. Это разделение ответственности делает систему устойчивее.
3Построение поискового движка
Теперь реализуем логику поиска релевантных фрагментов правил:
class RulesSearchEngine:
def __init__(self, db):
self.db = db
def search(self, query, game_filter=None, top_k=5):
"""Ищем релевантные фрагменты правил"""
# Получаем эмбеддинг запроса
query_embedding = self.db.embedder.encode(query)
# Ищем похожие векторы через PocketFlow
similar_ids = self.db.flow.search(query_embedding, top_k=top_k * 2)
# Фильтруем по игре если нужно
results = []
for item_id, score in similar_ids:
rule = self.db.box.get(item_id)
if rule:
if game_filter and rule.game != game_filter:
continue
results.append({
'text': rule.text,
'game': rule.game,
'page': rule.page,
'score': float(score)
})
if len(results) >= top_k:
break
return resultsЕсли вам нужно работать с действительно большими объёмами данных (60 ГБ писем, например), посмотрите мою статью про локальный RAG на слабом железе. Но для правил настольных игр наш подход более чем достаточен.
4Создание веб-API с BlackSheep
BlackSheep — это быстрый async веб-фреймворк без лишних зависимостей. Создадим простой API:
from blacksheep import Application, json
from blacksheep.server.responses import text
app = Application()
db = GameRulesDB()
search_engine = RulesSearchEngine(db)
@app.route('/api/search')
async def search_rules(request):
data = await request.json()
query = data.get('query', '')
game = data.get('game', None)
if not query:
return json({'error': 'Query is required'}, status=400)
results = search_engine.search(query, game_filter=game)
return json({'results': results})
@app.route('/api/games')
async def list_games(request):
# Получаем список всех игр в базе
games = set()
for rule in db.box:
games.add(rule.game)
return json({'games': list(games)})
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host='0.0.0.0', port=8000)5Интеграция с локальной LLM
Теперь добавим интеллект. Вместо облачных API используем локальную модель через Ollama или LM Studio:
import requests
class GameRulesAssistant:
def __init__(self, search_engine, llm_url='http://localhost:11434/api/generate'):
self.search_engine = search_engine
self.llm_url = llm_url
def answer_question(self, question, game=None):
# 1. Ищем релевантные фрагменты
context_chunks = self.search_engine.search(question, game_filter=game, top_k=3)
# 2. Формируем промпт
context_text = '\n\n'.join([f"[Игра: {c['game']}, стр. {c['page']}] {c['text']}"
for c in context_chunks])
prompt = f"""Ты — помощник по настольным играм. Ответь на вопрос, используя предоставленные правила.
Контекст:
{context_text}
Вопрос: {question}
Ответ должен быть кратким и понятным. Если в контексте нет информации, скажи об этом.
Ответ:"""
# 3. Отправляем в локальную LLM
response = requests.post(self.llm_url, json={
'model': 'llama3.2:3b', # Или другая лёгкая модель
'prompt': prompt,
'stream': False
})
if response.status_code == 200:
return response.json()['response']
else:
return "Ошибка при обращении к модели"Сборка pet-проекта: GameRulesBot
Давайте соберём всё вместе в готовое приложение. Структура проекта:
game_rules_bot/
├── data/
│ ├── terra_mystica_rules.pdf
│ └── carcassonne_rules.pdf
├── src/
│ ├── __init__.py
│ ├── database.py # GameRulesDB класс
│ ├── search.py # RulesSearchEngine класс
│ ├── assistant.py # GameRulesAssistant класс
│ └── api.py # BlackSheep приложение
├── scripts/
│ └── populate_db.py # Скрипт заполнения БД
├── requirements.txt
└── README.mdЗапускаем всё одной командой:
# Устанавливаем зависимости
pip install -r requirements.txt
# Заполняем базу данных правилами
python scripts/populate_db.py
# Запускаем API сервер
python src/api.pyНюансы и частые ошибки
| Проблема | Решение | Причина |
|---|---|---|
| Медленный поиск при большом количестве правил | Используйте HNSW индекс в PocketFlow | Линейный поиск работает медленно при >1000 векторов |
| LLM игнорирует контекст | Добавьте инструкцию "Отвечай ТОЛЬКО на основе предоставленного контекста" | Модели склонны галлюцинировать без жёстких ограничений |
| PDF с таблицами плохо парсится | Используйте pdfplumber с extract_tables() | Таблицы требуют специальной обработки |
| Не хватает памяти на слабом железе | Используйте модель all-MiniLM-L6-v2 вместо больших | Большие модели эмбеддингов требуют много RAM |
FAQ: Ответы на частые вопросы
Можно ли использовать этот подход для других типов документов?
Конечно! Эта архитектура подходит для любых structured документов: кулинарных книг, технических мануалов, юридических документов. Главное — адаптировать парсинг под формат ваших данных.
Что делать если правила игры содержат много изображений?
Для мультимодальных кейсов можно добавить обработку изображений через CLIP или аналогичные модели. Подробнее в статье про мультимодальный RAG.
Как масштабировать систему на сотни игр?
1. Добавьте шардирование по играм в ObjectBox
2. Используйте кэширование частых запросов
3. Рассмотрите переход на более производительную векторную БД для production
Можно ли добавить голосовой интерфейс?
Да! Подключите Whisper для транскрипции голоса в текст, а затем используйте наш API. Получится полноценный голосовой помощник для настольных игр.
Заключение: Почему этот подход работает?
Мы построили полностью локальную RAG-систему без тяжёлых зависимостей. Весь проект:
- Занимает менее 300 строк кода
- Работает на любом компьютере с Python 3.8+
- Не требует облачных API или платных сервисов
- Легко расширяется под новые форматы игр
Этот pet-проект отлично демонстрирует принцип "чем проще, тем лучше" в мире AI. Вместо того чтобы использовать переусложненные фреймворки, мы взяли минимальный набор инструментов и решили конкретную проблему.
Важный урок: Не всегда нужны сложные RAG-системы. Часто достаточно минимальной рабочей версии, которая решает 80% проблем с 20% усилий.
Если вы хотите углубиться в тему, рекомендую мои статьи про полностью локальные RAG-системы и работу с длинными PDF. Удачи в создании вашего игрового помощника!