Retrieval-Augmented Generation система по дискретной математике (теория графов) с вопросно-ответным чатом по учебнику и интерактивным генератором тестов.
Система ищет ответы в учебнике через TF-IDF и морфологический анализ (pymorphy3), а при наличии локальной LLM (Qwen 2.5 7B) улучшает формулировки вопросов и ответов. Без LLM система полностью работоспособна на шаблонах и TF-IDF — graceful degradation.
- 📖 Чат по учебнику: задайте вопрос по теории графов — система найдёт релевантный фрагмент и вернёт ответ с указанием источников
- 🧠 Генерация тестов (5 типов): автоматически создаёт тест из 5 вопросов разных типов по 57 терминам
- ✅ Проверка ответов: TF-IDF валидация для открытых вопросов, точное совпадение для закрытых
- 📊 Прогресс обучения: отслеживание, какие термины изучены, а какие требуют повторения
- 🔁 Умный выбор терминов: вопросы чаще задаются по терминам, с которыми пользователь ещё не работал или ошибается
- 🌐 Сессии: одна вкладка = одна сессия, история чата сохраняется in-memory (1 час бездействия → автоочистка)
- 🤖 LLM-улучшение (опционально): подключите Qwen 2.5 7B для более связных формулировок и генерации дистракторов
- 🐳 Docker-ready: развёртывание одной командой, готовый docker-compose для бэкенда + LLM
┌─────────────────────────────────────────────────────────────┐
│ Браузер (SPA) │
│ frontend/index.html (Vanilla JS + CSS) │
└──────────────────────────┬──────────────────────────────────┘
│ HTTP (REST JSON)
▼
┌─────────────────────────────────────────────────────────────┐
│ Flask Backend (serve.py) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Chat │ │ Trainer │ │ Session │ │ LLM │ │
│ │ API │ │ API │ │ Manager │ │ Service │ │
│ │ /api/ │ │ /api/ │ │ (memory) │ │ (опц.) │ │
│ │ chat │ │ trainer/│ │ │ │ │ │
│ └────┬─────┘ └────┬─────┘ └──────────┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────┐ ┌────────────┐ │
│ │ Retriever (TF-IDF) │ │ llama.cpp │ │
│ │ ┌────────┐ ┌───────┐ ┌────────┐ │ │ + Qwen │ │
│ │ │ Чанки │ │Термин │ │ TF-IDF │ │ │ 2.5 7B │ │
│ │ │(струк- │ │индекс │ │ матрица│ │ │ (отдельный │ │
│ │ │ турные)│ │(опре- │ │15000 │ │ │ контейнер)│ │
│ │ │ │ │деления│ │n-грамм │ │ └────────────┘ │
│ │ └────────┘ └───────┘ └────────┘ │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ data/chapter.md │
│ data/terms.txt (57) │
└───────────────────────┘
- Сегментация: учебник разбивается на структурированные чанки с заголовками разделов (
##и###) - Лемматизация: все слова приводятся к нормальной форме через pymorphy3 (однократно при старте ≈10 сек)
- TF-IDF: строится матрица 15000 n-грамм (1-2), sublinear TF, cosine similarity
- Бустинг: заголовки разделов ×1.5, совпадение терминов ×1.3
- Индекс определений: отдельный индекс с точным поиском по маркерам («называется», «— это», «определяется» и др.)
- Fallback: при низкой уверенности (<0.15) запрос расширяется лемматически связанными терминами
SmartTermSelector выбирает термины с учётом прогресса пользователя:
| Приоритет | Бакет | Условие |
|---|---|---|
| 🔴 0 | never_asked |
Термин ни разу не спрашивали |
| 🟠 1 | always_wrong |
Спрашивали ≥1, но 0 правильных |
| 🟡 2 | in_progress |
Спрашивали ≥1, правильных ≥1 |
| 🟢 3 | learned |
≥1 правильный ответ |
- Подключается через HTTP к llama.cpp серверу (OpenAI-compatible API)
- Улучшает формулировки вопросов и ответов (без изменения смысла)
- Генерирует дистракторы для тестов
- Безопасность: валидатор проверяет сохранность ключевых терминов; при изменении смысла — возвращается исходный текст
- При недоступности LLM система продолжает работать на шаблонах и TF-IDF
| Тип | Описание | Валидация |
|---|---|---|
definition |
«Что такое {term}?» — открытый ответ | TF-IDF косинусное сходство (0.6) + overlap (0.4) |
true_false |
«Верно ли, что …?» — с морфологической заменой термина | Точное совпадение с truth_value |
multiple_choice |
«Какой термин соответствует определению?» — 4 варианта | Точное совпадение (регистронезависимое) |
fill_blank |
Заполнить пропуск (вариации: весь термин / 1-е слово / последнее слово) | Точное совпадение |
matching |
«Какой термин соответствует определению?» — с маскировкой термина | Точное совпадение |
Для типа definition используется композитная метрика:
final_score = 0.6 × similarity(TF-IDF) + 0.4 × overlap(ключевые слова)
Порог прохождения: ≥ 0.12
# Установка зависимостей
pip install -r backend/requirements.txt
# Запуск
python serve.py
# Открыть в браузере
# http://localhost:5000# Сборка и запуск
docker build -t rag-backend:latest .
docker run -d --name rag-backend -p 5000:5000 \
-v $(pwd)/data:/app/data:ro \
-e PYTHONUNBUFFERED=1 \
-e LLM_ENABLED=false \
--memory=2g \
rag-backend:latest
# Или через docker-compose:
LLM_ENABLED=false docker-compose up backend# 1. Скачайте модель Qwen 2.5 7B Q4_K_M (~4.5 ГБ)
mkdir -p models
wget -O models/qwen2.5-7b-q4_k_m.gguf \
https://huggingface.co/Qwen/Qwen2.5-7B-GGUF/resolve/main/qwen2.5-7b-q4_k_m.gguf
# 2. Запустите оба сервиса
LLM_ENABLED=true docker-compose up --buildLLM-сервер стартует дольше (загрузка модели в память, ~6 ГБ RAM). После появления http://localhost:5000 в браузере — система готова.
Сессия идентифицируется заголовком X-Session-Id. Фронтенд генерирует UUID при загрузке страницы и передаёт его в каждом запросе.
Инициализировать или получить существующую сессию.
curl -X POST http://localhost:5000/api/session/init \
-H 'X-Session-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890'{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"created_at": 1717000000.0,
"has_history": false,
"active_test": false
}Получить историю чата текущей сессии.
curl -X GET http://localhost:5000/api/session/history \
-H 'X-Session-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890'{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"chat_history": [
{ "role": "user", "content": "Что такое эйлеров цикл?", "timestamp": 1717000100.0 },
{ "role": "assistant", "content": "Эйлеров цикл — это цикл...", "timestamp": 1717000100.5 }
],
"active_test": false
}Задать вопрос по теории графов. Система ищет релевантный фрагмент в учебнике и возвращает ответ.
Request:
{
"message": "Что такое эйлеров цикл?",
"question": "Что такое эйлеров цикл?" // альтернативное поле
}Response:
{
"answer": "Эйлеров цикл — это цикл в графе, проходящий по каждому ребру ровно один раз.",
"confidence": "high",
"improved": false,
"term": "Цикл",
"sources": [
{
"text": "Эйлеров цикл — это цикл в графе, проходящий по каждому ребру ровно один раз...",
"score": 0.523
}
]
}| Поле | Тип | Описание |
|---|---|---|
answer |
string | Ответ на вопрос (из учебника, возможно улучшен LLM) |
confidence |
string | high (≥0.3), medium (≥0.15), low (<0.15) |
improved |
bool | Был ли ответ улучшен LLM |
term |
string? | Обнаруженный термин в вопросе |
sources |
array | Список релевантных фрагментов с score |
Сгенерировать новый тест из 5 вопросов. Типы вопросов перемешиваются случайно. Термины выбираются с учётом прогресса пользователя.
curl -X POST http://localhost:5000/api/trainer/generate \
-H 'X-Session-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890'{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"questions": [
{
"id": 0,
"type": "definition",
"question_text": "Что такое гамильтонов цикл?",
"options": null,
"llm_improved": false
},
{
"id": 1,
"type": "true_false",
"question_text": "Верно ли, что ...?",
"options": ["Верно", "Неверно"],
"llm_improved": false
},
{
"id": 2,
"type": "multiple_choice",
"question_text": "Какой термин соответствует определению: ...?",
"options": ["Двудольный граф", "Полный граф", "Регулярный граф", "Связный граф"],
"llm_improved": false
},
{
"id": 3,
"type": "fill_blank",
"question_text": "Мостом называется ______, удаление которого увеличивает число компонент связности.",
"options": null,
"llm_improved": false
},
{
"id": 4,
"type": "matching",
"question_text": "Какой термин соответствует следующему определению?\n\n«...»",
"options": null,
"llm_improved": false
}
],
"total": 5,
"generated_at": 1717000200.0
}Важно: correct_answer не возвращается — ответы проверяются через /api/trainer/check.
Проверить ответ на один вопрос теста.
Request:
{
"question_id": 0,
"answer": "Эйлеров цикл — это..."
}Response (при успехе):
{
"correct": true,
"is_correct": true,
"feedback": "✅ Верно!",
"question_id": 0,
"test_completed": false,
"progress": {
"answered": 1,
"total": 5,
"correct_so_far": 1
},
"next_question_id": 1
}Response (при ошибке, с деталями для открытых вопросов):
{
"correct": false,
"is_correct": false,
"feedback": "❌ Неверно",
"question_id": 0,
"test_completed": false,
"progress": {
"answered": 1,
"total": 5,
"correct_so_far": 0
},
"next_question_id": 1,
"correct_answer": "...",
"score_details": {
"is_open_question": true,
"similarity_score": 0.15,
"overlap_score": 0.08,
"final_score": 0.12,
"similarity_pct": 15,
"overlap_pct": 8,
"final_pct": 12,
"passing_threshold": 0.12,
"threshold_pct": 12
}
}Response (при завершении теста):
{
"correct": true,
"is_correct": true,
"feedback": "✅ Верно!",
"question_id": 4,
"test_completed": true,
"progress": { "answered": 5, "total": 5, "correct_so_far": 4 },
"next_question_id": null,
"final_score": {
"score": 4,
"total": 5,
"time_spent": 120.5
},
"results": [
{
"question_id": 0,
"type": "definition",
"question_text": "Что такое...?",
"user_answer": "...",
"correct_answer": "...",
"is_correct": true
}
]
}Завершить тест досрочно (неотвеченные вопросы засчитываются как неверные).
Request: {} (пустое тело)
Response: Аналогично финальным полям /api/trainer/check.
Сбросить активный тест сессии.
{ "reset": true, "message": "Тест сброшен. Вызовите /api/trainer/generate для создания нового теста." }Получить статистику прогресса изучения терминов для текущей сессии.
curl -X GET http://localhost:5000/api/trainer/progress \
-H 'X-Session-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890'{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"summary": {
"total_terms": 57,
"never_asked": 40,
"always_wrong": 3,
"in_progress": 5,
"learned": 9
},
"coverage": {
"covered": 17,
"total": 57,
"percent": 29
},
"learned": {
"count": 9,
"total": 57,
"percent": 15
},
"terms_progress": [
{ "term": "Граф", "status": "learned", "questions_asked": 2, "correct_answers": 1 },
{ "term": "Петля", "status": "never_asked", "questions_asked": 0, "correct_answers": 0 }
]
}| Метод | Путь | Описание |
|---|---|---|
GET |
/ |
Статика — фронтенд (SPA) |
GET |
/health |
Проверка здоровья сервера |
GET |
/api/terms |
Список всех 57 терминов |
GET |
/terms |
(legacy) Список терминов |
POST |
/ask |
(legacy) Чат без сессий |
{
"status": "ok",
"chunks": 42,
"sessions": 3,
"llm_available": false
}| Переменная | По умолчанию | Описание |
|---|---|---|
LLM_ENABLED |
false |
Включить LLM-улучшение (true/false) |
LLM_BASE_URL |
http://llm-server:8080 |
Адрес LLM-сервера (llama.cpp) |
LLM_TIMEOUT |
30 |
Таймаут запроса к LLM (сек) |
RAG/
├── serve.py # ★ Главная точка входа (Flask, используется в Docker)
├── Dockerfile # Docker-образ бэкенда (gunicorn)
├── Dockerfile.llm # Docker-образ LLM-сервера (llama.cpp + Qwen 2.5)
├── docker-compose.yml # Оркестрация бэкенда и LLM
├── requirements.txt # (корневой, не используется — смотри backend/)
├── .dockerignore
├── .gitignore
├── README.md
│
├── backend/
│ ├── serve.py # (копия корневого, symlink)
│ ├── app.py # Dev-версия (устаревшие эндпоинты, без сессий)
│ ├── retriever.py # ★ TF-IDF поиск + сегментация чанков + индекс терминов
│ ├── trainer.py # ★ Генерация тестов + валидация ответов
│ ├── session.py # ★ Управление сессиями (in-memory, TTL 1 час)
│ ├── llm.py # ★ HTTP-клиент к Qwen 2.5 (graceful degradation)
│ └── requirements.txt # Python-зависимости
│
├── frontend/
│ └── index.html # Single-Page Application (3469 строк, Vanilla JS + CSS)
│
├── data/
│ ├── chapter.md # ★ Учебник по теории графов (1228 строк)
│ └── terms.txt # ★ 57 терминов (по одному на строке)
│
└── scripts/ # (резерв)
Зависимости (backend/requirements.txt):
flask==3.0.3
flask-cors==4.0.1
scikit-learn==1.5.1
numpy==1.26.4
pymorphy3==2.0.2
requests==2.31.0
# Установка
pip install -r backend/requirements.txt
# Запуск с авто-перезагрузкой
FLASK_DEBUG=1 python serve.py- Добавьте термин в
data/terms.txt(одна строка — один термин, напримерМой термин) - Добавьте или найдите определение с маркером («называется», «— это», «Def =») в
data/chapter.md - Перезапустите сервер — retriever перестроит индекс терминов автоматически
- Проверьте:
GET /api/termsдолжен содержать новый термин
Для терминов, у которых нет явного определения с маркером в тексте (например, «Вершина», «Ребро», «Перестановка»), определения задаются вручную в retriever.MANUAL_DEFS. При добавлении нового такого термина — дополните этот словарь.
В проекте нет автоматических тестов. Рекомендуется:
# Проверка через curl
curl -X POST http://localhost:5000/api/chat \
-H 'Content-Type: application/json' \
-d '{"message": "Что такое граф?"}'
curl -X POST http://localhost:5000/api/trainer/generate
# Health check
curl http://localhost:5000/health- Лемматизация с кэшированием: все слова разбираются через pymorphy3 один раз при старте, затем используются для всех 57 терминов. Время старта — ~10 секунд.
- Морфологический поиск терминов: термины находятся в тексте в любой грамматической форме (Граф, Графом, Графа, Графе и т.д.)
- Перевёрнутые определения: поддерживается поиск определений вида «вершина называется изолированной» для термина «Изолированная вершина»
- Составные термины с вставками: находит «Объединение (дизъюнктное) графов» как вхождение термина «Объединение графов»
- Типы вопросов с вариациями:
fill_blankимеет 3 вариации: замена всего термина, первого слова или последнего словаtrue_falseможет заменять термин с согласованием по падежу для создания ложных утверждений
- Progress Map: каждый термин отслеживается отдельно — сколько раз спрашивали, сколько правильных ответов, какие типы вопросов использовались
Проект распространяется под лицензией MIT. Подробнее — в файле LICENSE.