Skip to content

Repository files navigation

RAG по теории графов

CI Python License: MIT

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)  │
              └───────────────────────┘

Как работает поиск (retriever.py)

  1. Сегментация: учебник разбивается на структурированные чанки с заголовками разделов (## и ###)
  2. Лемматизация: все слова приводятся к нормальной форме через pymorphy3 (однократно при старте ≈10 сек)
  3. TF-IDF: строится матрица 15000 n-грамм (1-2), sublinear TF, cosine similarity
  4. Бустинг: заголовки разделов ×1.5, совпадение терминов ×1.3
  5. Индекс определений: отдельный индекс с точным поиском по маркерам («называется», «— это», «определяется» и др.)
  6. Fallback: при низкой уверенности (<0.15) запрос расширяется лемматически связанными терминами

Как работает тестирование (trainer.py)

SmartTermSelector выбирает термины с учётом прогресса пользователя:

Приоритет Бакет Условие
🔴 0 never_asked Термин ни разу не спрашивали
🟠 1 always_wrong Спрашивали ≥1, но 0 правильных
🟡 2 in_progress Спрашивали ≥1, правильных ≥1
🟢 3 learned ≥1 правильный ответ

LLM-улучшение (опциональное)

  • Подключается через 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

Быстрый старт

Запуск без Docker

# Установка зависимостей
pip install -r backend/requirements.txt

# Запуск
python serve.py

# Открыть в браузере
# http://localhost:5000

Запуск с Docker (только бэкенд, без LLM)

# Сборка и запуск
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

Запуск с Docker + LLM

# 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 --build

LLM-сервер стартует дольше (загрузка модели в память, ~6 ГБ RAM). После появления http://localhost:5000 в браузере — система готова.


API-документация

Управление сессиями

Сессия идентифицируется заголовком X-Session-Id. Фронтенд генерирует UUID при загрузке страницы и передаёт его в каждом запросе.

POST /api/session/init

Инициализировать или получить существующую сессию.

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
}

GET /api/session/history

Получить историю чата текущей сессии.

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
}

Чат (Q&A)

POST /api/chat

Задать вопрос по теории графов. Система ищет релевантный фрагмент в учебнике и возвращает ответ.

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

Тренажёр

POST /api/trainer/generate (или GET)

Сгенерировать новый тест из 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.

POST /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
    }
  ]
}

POST /api/trainer/complete

Завершить тест досрочно (неотвеченные вопросы засчитываются как неверные).

Request: {} (пустое тело)

Response: Аналогично финальным полям /api/trainer/check.

POST /api/trainer/reset

Сбросить активный тест сессии.

{ "reset": true, "message": "Тест сброшен. Вызовите /api/trainer/generate для создания нового теста." }

GET /api/trainer/progress

Получить статистику прогресса изучения терминов для текущей сессии.

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) Чат без сессий

GET /health

{
  "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

Добавление нового термина

  1. Добавьте термин в data/terms.txt (одна строка — один термин, например Мой термин)
  2. Добавьте или найдите определение с маркером («называется», «— это», «Def =») в data/chapter.md
  3. Перезапустите сервер — retriever перестроит индекс терминов автоматически
  4. Проверьте: 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.

About

RAG-система по теории графов. Чат + авто-генерация тестов (5 типов). TF-IDF + Qwen 2.5 7B.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages