Files
anti-plagiarism/docs/ARCHITECTURE.md
jze9 a76e46c561
All checks were successful
Deploy / deploy (push) Successful in 4s
Deploy / test (push) Successful in 2m53s
feat(ops): замер качества детекции — первые честные цифры
О качестве проверки мы до сих пор знали только «механизм жив»: находит
подброшенный фрагмент. Продукт при этом продаёт процент заимствований, за
который никто не ручался — неизвестно было ни сколько списываний система
пропускает, ни как часто обвиняет невиновных.

Бенчмарк делает из документов корпуса «студенческие работы» четырёх видов и
гоняет их через настоящий путь L1. Первый замер на проде:

  дословно        100% найдено
  лёгкий рерайт   100% найдено
  сильный рерайт    0% — это работа L3/L4, не L1
  оригинал          0% ложных обвинений

То есть основа работает как задумано. Показательно другое: из 20 взятых
документов в замер попали 8 — у остальных нет полного текста нужной длины.
Узкое место не алгоритм, а глубина корпуса.

Запускать после изменения порогов и параметров winnowing — иначе непонятно,
улучшение сделано или ухудшение.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 20:45:59 +05:00

25 KiB
Raw Blame History

Архитектура — Академический помощник

Каноничное описание системы. Обновляется вместе с кодом; при расхождении верить коду, а не этому файлу. Визуальная схема — DIAGRAM.md. Смежные документы: DR-HA.md (отказоустойчивость), INGESTION.md (наполнение корпуса), ../README.md (быстрый старт и команды).

1. Назначение

Веб-сервис для студентов: ввёл тему → система находит открытые академические источники, делает изложения, форматирует список литературы по ГОСТ и проверяет текст на плагиат. Всё асинхронно: пользователь отправляет задачу, закрывает браузер и получает результат на email (или в реальном времени через WebSocket, если вкладка открыта).

2. Общая схема

                         ┌────────────────────────────┐
                         │  Frontend (React SPA)       │  CT 102 → nginx
                         │  Home/Search/Check/Cabinet/ │
                         │  Bibliography/Settings/Admin │
                         └──────────────┬──────────────┘
                                        │ HTTPS (/api, /ws)
                         ┌──────────────▼──────────────┐
                         │  API Gateway (FastAPI)       │  1.32:8000
                         │  JWT · rate-limit · WebSocket │
                         │  /metrics (Prometheus)        │
                         └───┬───────────────────────┬──┘
                             │ publish (RabbitMQ)    │ read/write
                             │                       │
        ┌────────────────────┼───────────────────┐  │
        ▼                    ▼                   ▼  │
  queue.index          queue.gpu           queue.gost
  ┌───────────┐      ┌────────────┐      ┌───────────┐
  │ worker-   │      │ worker-gpu │      │ worker-   │
  │ indexer   │      │ FAISS/Qdr. │      │ gost      │
  │ L1 Winnow │      │ L3 семант. │      │ ГОСТ 7.1/ │
  │ L2 MinHash│      │ L4 LLM     │      │ 7.0.5     │
  │ PDF/DOCX  │      │ эмбеддинги │      │           │
  └─────┬─────┘      └──────┬─────┘      └─────┬─────┘
        │                   │                  │
        └─────────┬─────────┴───────┬──────────┘
                  ▼                  ▼
            queue.notify       общие данные
          ┌────────────┐   ┌──────────────────────────────┐
          │ worker-    │   │ PostgreSQL 1.38 · Redis 1.35  │
          │ notifier   │   │ RabbitMQ .82 · MinIO 1.21     │
          │ SMTP email │   │ Elasticsearch (local 1.32)    │
          └────────────┘   │ Ollama 1.40 (bge-m3, эмбед.)  │
                           │ OpenRouter/DeepSeek — LLM (опц.)│
                           │ [opt] Qdrant · Prometheus/Graf.│
                           └──────────────────────────────┘

3. Сервисы

Сервис Технологии Ответственность
api FastAPI, SQLAlchemy async (asyncpg), Redis, Celery-producer HTTP/WS API, auth (JWT), rate-limits, диспетч задач в очереди, админ-панель
worker-indexer Celery, PyMuPDF, python-docx, xxhash, datasketch Извлечение текста (PDF/DOCX/TXT), фрагментация, L1 Winnowing, L2 MinHash LSH, парсинг источников, обогащение full-text
worker-gpu Celery, FAISS/Qdrant, httpx→Ollama/OpenRouter Эмбеддинги (EMBED_BACKEND: ollama/sentence_transformers/cloud), L3 семантический поиск, L4 LLM-анализ парафраза (LLM_BACKEND: ollama/openrouter), семантический поиск источников
worker-gost Celery Список литературы по ГОСТ 7.1-2003 / Р 7.0.5-2008
worker-notifier Celery, smtplib Email: письма-результаты и верификация (jze9mail.ru)
frontend React 18, Vite, TS, Tailwind, Zustand, React Query SPA: 11 публичных страниц + админ-панель. Собирается в статику, отдаётся nginx

4. Модель данных (PostgreSQL)

  • users — email, hashed_password (bcrypt), name, is_verified, is_admin, plan (free/student/premium), verification_token.
  • tasks — id (UUID), public_id (внешний), user_id, type (TaskType: search/plagiarism/summarize/gost), status (TaskStatus: queued→processing→done/failed), celery_task_id, input_data (JSON), result (JSON), error, queue_position.
  • documents — корпус источников: source (openalex/arxiv/cyberleninka/ user_submission), ext_id, doi, title, authors (JSON), year, lang, journal/volume/issue/pages, abstract, url, minio_key (full-text в MinIO), faiss_id.
  • fingerprints — doc_id, hash_value (BIGINT, Winnowing), position — для L1.
  • usage_logs — user_id, action — учёт лимитов по тарифу.
  • parse_sources — задания парсеров (админка): тип, query, годы, лимит, статус, last_run_id — ссылка на последний прогон, resume_token — позиция продолжения для массовых источников (номер статьи в дампе, токен страницы бакета).
  • parse_runs — прогоны заливки: стадия, счётчики (target/fetched/processed/ added/duplicates/skipped/failed), cancel_requested, heartbeat_at, журнал событий (JSON). Тайминги раздельные: started_at — постановка в очередь, run_started_at — реальный старт работы воркером (при массовом запуске между ними часы ожидания), finished_at — конец. Из них админка рисует шкалу загрузки, см. §12.
  • staged_works — пользовательские загрузки на модерацию перед добавлением в корпус.
  • admin_sessions — одноразовые коды входа в админку.

5. Асинхронный конвейер (Celery + RabbitMQ)

Брокер — RabbitMQ, backend результатов — Redis. Маршрутизация по префиксу задачи:

Очередь Задачи Воркер
queue.index index.extract_and_check, index.add_document, index.run_parser, index.enrich_full_text, index.ingest_upload worker-indexer
queue.gpu gpu.check_plagiarism, gpu.embed_documents, gpu.search_semantic, gpu.index_stats worker-gpu
queue.gost gost.format_bibliography worker-gost
queue.notify notify.send_task_done, notify.send_verification worker-notifier

Важно: API коммитит задачу в БД до send_task (иначе гонка dispatch-before-commit).

6. Ключевые потоки

Проверка плагиата. upload (api, файл→MinIO, Task) → index.extract_and_check (извлечь текст → фрагментация → L1 Winnowing по fingerprints → L2 MinHash LSH в Redis) → передаёт частичные совпадения в gpu.check_plagiarism (L3 FAISS/Qdrant семантика по фрагментам → для подозрительных L4 LLM-парафраз) → app.scoring.aggregate_results (итоговый %) → результат в Task → notify.send_task_done. Фронтенд (HighlightedDocument.tsx) показывает исходный текст работы с подсветкой совпадающих/процитированных фрагментов поверх обычного списка нарушений (PlagiarismReport.tsx).

Поиск источников. api → gpu.search_semantic: эмбеддинг запроса → векторный поиск (FAISS/Qdrant) + Elasticsearch BM25 → объединение → результат.

Список литературы. api → gost.format_bibliography: документы из БД → app.bibliography.build_bibliography (сортировка кириллица→латиница, нумерация, формат 7.1/7.0.5) → результат.

Наполнение корпуса. админка → index.run_parser — один конвейер для всех источников, со шкалой, журналом и кнопкой остановки. Внутри два пути записи:

  • обычные источники (OpenAlex/arXiv/PMC/КиберЛенинка, фильтр is_oa) — index.add_document на каждый документ: дедуп по ext_id, fingerprints, MinHash, Elasticsearch, эмбеддинг, затем index.enrich_full_text (скачать OA-PDF → MinIO → переиндексация);
  • массовые источники (wikipedia_ru, pmc_bulk) — парсер отдаёт генератор, запись идёт пачками через COPY (app/bulk_writer.py), позиция продолжения хранится в parse_sources.resume_token. Эмбеддинги там не считаются: они медленнее заливки на порядок и стали бы её узким местом, вектора досчитываются отдельно (§12).

Ход заливки в обоих случаях пишется в parse_runs (§12). Второй путь наполнения — ручная загрузка файлов админом: api сохраняет их в MinIO (corpus-upload/) → index.ingest_upload (извлечь текст → add_document, source=manual_upload). Это не проверка на плагиат: файл сразу становится источником для сравнения, минуя отстойник.

7. Детекция плагиата — 4 уровня

  1. L1 Winnowing (worker-indexer/app/algorithms/winnowing.py) — точные/частичные совпадения: k-граммы → xxHash → минимум в скользящем окне → fingerprint; Jaccard.
  2. L2 MinHash LSH (.../minhash.py) — нечёткие совпадения: шинглы → MinHash (128 перм.) → LSH-индекс в общем Redis (префикс antiplag_lsh, upsert, graceful-фолбэк в память).
  3. L3 семантика (worker-gpu) — эмбеддинги bge-m3 (1024d, нормированы), бэкенд переключается EMBED_BACKEND (ollama — дефолт, GGUF через Vulkan на AMD GPU; sentence_transformers — локальная загрузка CUDA/CPU; cloud — облачный инференс той же модели, без локального GPU) → cosine в FAISS IndexIDMap2(IndexFlatIP) или Qdrant (VECTOR_BACKEND); порог 0.75.
  4. L4 LLM-парафраз — оценивает пары «источник↔фрагмент» для подозрительных из L3; порог confidence 0.7. Бэкенд переключается LLM_BACKEND: ollama (локальная модель, нужен GPU-хост) или openrouter (облачный DeepSeek — геоблокировку РФ обходит SOCKS5-прокси singbox-proxy, докер-сервис на базе sing-box/Trojan). Ни один документ с source=user_submission (прошлые проверки пользователей) не участвует в сравнении ни на одном уровне — иначе работа матчилась бы сама с собой.

Итог: scoring.aggregate_results — доля уникальных помеченных позиций (не выше 100%). Кандидаты, похожие семантически, но не подтверждённые LLM как парафраз, не теряются — попадают в отдельный список «похожие по теме источники» (рекомендации, не нарушения).

8. Векторный бэкенд

Абстрагирован за worker-gpu/app/vector_store.get_backend(); VECTOR_BACKEND=faiss (файловый синглтон, дефолт) или qdrant (сервис, снимает SPOF/конкурентную запись). Переключение и миграция — см. README, раздел «Векторный бэкенд».

9. Инфраструктура и топология

Компонент Узел Примечание
api + воркеры + Elasticsearch 1.32 (app-хост) docker-compose.prod.yml
Frontend (nginx, статика + TLS) CT 102 деплоится отдельно
PostgreSQL 16 1.38 (выделенный LXC) UTF8; бэкап→MinIO
Redis 7 1.35 (выделенный LXC) кэш, rate-limits, LSH
RabbitMQ 192.168.20.82 брокер Celery
MinIO (S3) 1.21 документы, full-text, бэкапы
Ollama (bge-m3, эмбеддинги) 192.168.1.40 «embedding-cpu» (VM 210 на хосте pve2 .1.37) Xeon 4314, AVX-512+VNNI, 8 vCPU; модель всегда в RAM (OLLAMA_KEEP_ALIVE=-1), сторожевой таймер раз в 2 мин перезапускает зависшую Ollama. Замер на данных корпуса: 3.6 док/с против 2.2 у прежнего сервера
Ollama на GPU (выведен 31.08.2026) 192.168.20.109 «embedding-gpu» RX580; отказал по amdgpu ring timeout (Vulkan-контекст умирал при живом systemd-юните). Оставлен как есть — откат сводится к возврату OLLAMA_URL в Infisical
OpenRouter (DeepSeek, L4 LLM) облако опционально вместо локальной Ollama (LLM_BACKEND=openrouter); из РФ доступен только через singbox-proxy
Infisical (секреты) 192.168.20.111 «VM111» self-hosted, infisical.jze9.ru; единственный источник правды для .env на проде
Qdrant / Prometheus / Grafana 1.32 опционально, под compose-профилями

CT 108 (192.168.20.163, GTX1070, qwen2.5:7b) — старая LLM-нода, роль полностью заменена OpenRouter, контейнер остановлен, но не удалён.

Деплой — Gitea Actions по push в main: гейт test (ruff+mypy → 113 юнит-тестов), затем deploy (needs: test) через scripts/deploy.sh (умная пересборка изменённых сервисов). PG/Redis/RabbitMQ/MinIO не в compose — общая инфра берётся из .env, который на каждом деплое генерируется заново из Infisical (см. §10).

10. Конфигурация и секреты

Приложение читает .env (пример структуры — .env.example), но на проде этот файл не редактируется руками и не персистентен как источник правды — на каждом деплое scripts/deploy.sh логинится в self-hosted Infisical (https://infisical.jze9.ru, проект academ, окружение prod) через Machine Identity (Universal Auth) и генерирует .env заново (infisical export) до синка кода. Правки секретов/конфига — через Infisical, а не через .env на сервере (следующий деплой затрёт локальные правки). Если Infisical недоступен или вернул подозрительно мало ключей, деплой прерывается раньше, не трогая рабочий .env.

Обязательно менять (значения уже в Infisical, не дефолты из .env.example): SECRET_KEY, POSTGRES_PASSWORD, MINIO_SECRET_KEY. Опции: VECTOR_BACKEND, QDRANT_URL, EMBED_BACKEND, LLM_BACKEND, GRAFANA_ADMIN_PASSWORD.

Локальная разработка (docker-compose.test.yml) продолжает использовать обычный .env, отдельный от прод-потока через Infisical.

11. Качество и тесты

Замер качества детекции — scripts/ops/detection_benchmark.py. Делает из документов корпуса «студенческие работы» (дословная копия, лёгкий и сильный рерайт, плюс заведомо оригинальный текст) и прогоняет через настоящий путь L1. Первый замер, 05.09.2026:

Случай Доля найденных
дословно 100%
лёгкий рерайт (выброшено каждое 10-е слово) 100%
сильный рерайт (каждое 3-е слово) 0% — задача L3/L4, не L1
оригинальный текст 0% ложных обвинений

Запускать после изменения порогов (EXACT_FRAGMENT_THRESHOLD), параметров winnowing и плотности отпечатков: без этих чисел непонятно, улучшение сделано или ухудшение. Ограничение замера: в выборку попадают только документы с реальной глубиной (>800 отпечатков) — по документам с одной аннотацией мерить нечего, и это само по себе показатель состояния корпуса.

  • 113 юнит-тестов (pytest, per-service) на чистую логику L1-L4/скоринг/фрагменты/ ГОСТ/библиография; инфра замокана или не нужна. Запуск: make test.
  • Гейты CI: ruff (весь Python) + mypy (доменная логика) + тесты — блокируют деплой. make lint. Хермет-раннеры в python:3.11-slim.
  • Чистая доменная логика вынесена из Celery-задач в тестируемые модули (scoring.py, fragments.py, bibliography.py). Подробнее — README «Тестирование».

12. Наблюдаемость и эксплуатация

  • Мониторинг: scripts/ops/antiplag_monitor.py (cron 5 мин на 1.32, email при смене статуса). Адреса берутся из прод-.env, а не зашиты в код: прежняя версия месяц проверяла остановленный CT 108 и не видела реального сервера эмбеддингов — постоянный ложный DOWN заглушал настоящие аварии. Проверяются в том числе воркеры Celery: живой брокер ещё не значит работающую систему (05.09 воркеры час простаивали при «зелёном» брокере). Опционально — Prometheus+Grafana (профиль observability, метрики API + Flower).
  • Бэкапы: pg_dump→gzip→MinIO, cron 03:00, ротация 14. Проверка восстановления — scripts/ops/pg_restore_verify.sh. HA/DR — DR-HA.md.
  • Шкала загрузки источников (админка → «Источники»): каждый запуск создаёт строку parse_runs, воркер пишет туда прогресс раз в ~2с. Шкала считается по формуле api/app/core/progress.py: 0→50% — выборка из источника, 50→100% — индексация в базу. Раскрытая строка показывает журнал прогона (по шагам, с таймингами). Кнопки «Запустить всё» / «Остановить всё» — массовый старт и кооперативная отмена (флаг cancel_requested, воркер останавливается сам на ближайшем тике; уже начатый прогон не рвём посреди записи в базу).
  • Панель отладки (админка → «Отладка», GET /api/admin/debug + /health): один срез — доступность инфраструктуры (PostgreSQL/Redis/MinIO/ES/Ollama/ брокер: этот блок показывается даже когда сам срез не собирается, потому что при аварии первый вопрос — что именно отвалилось), живые воркеры Celery и что именно они крутят, глубина очередей RabbitMQ (в т.ч. unacked и число потребителей), покрытие корпуса эмбеддингами, активные и проблемные прогоны, зависшие прогоны (нет heartbeat >10 мин), упавшие проверки за сутки и текущие бэкенды (EMBED_BACKEND/LLM_BACKEND/ VECTOR_BACKEND).
  • Бюджет времени прогона (PARSER_TIME_BUDGET_S, по умолчанию 1500с) — защита от краш-лупа по consumer_timeout RabbitMQ, см. DR-HA.md §6.
  • Покрытие L3 меряется по индексу, а не по БД. Колонка documents.faiss_id для этого непригодна: отметка остаётся после пересоздания индекса (смена модели/размерности) и после сбоев worker-gpu. Панель отладки спрашивает реальное число векторов задачей gpu.index_stats и отдельно предупреждает о ложных отметках; чинит их scripts/ops/faiss_reconcile.py.
  • Добор эмбеддингов — scripts/ops/reembed_missing.py: документ попадает в корпус сразу, а вектор для L3 считает отдельная задача gpu.embed_documents; если worker-gpu или Ollama были недоступны, эти задачи теряются и документ остаётся невидимым для семантического поиска. Скрипт находит faiss_id IS NULL и переотправляет задачи пачками (dry-run по умолчанию). Покрытие видно в панели отладки.

13. Безопасность

JWT-аутентификация, bcrypt-хэши паролей, email-верификация, отдельный вход в админку (одноразовые коды, admin_sessions). Rate-limits по тарифу — в Redis. CORS — явные origins. Пользователь видит только свои задачи (ownership проверяется, в т.ч. на WebSocket).

14. Раскладка репозитория

services/         api, worker-{gpu,indexer,notifier,gost}, frontend
scripts/          parsers/ (OpenAlex/arXiv/PMC/КиберЛенинка), seed_*.py, ops/,
                  deploy.sh, run_tests.sh, run_lint.sh
infra/            nginx/, prometheus/, grafana/, singbox/ (VPN-прокси для OpenRouter)
docs/             ARCHITECTURE.md (этот файл), DR-HA.md
.gitea/workflows/ deploy.yml (гейт test → deploy)
ruff.toml · mypy.ini · Makefile · docker-compose.prod.yml