Production-oriented web application: AI-ассистент для студентов по управлению проектами, методологиям и инженерным практикам.
Стек: React SPA + FastAPI API + SSE realtime + Redis queue + PostgreSQL state + Qdrant RAG + llama.cpp/llama-server (TurboQuant KV-cache).
frontend(React + TypeScript SPA)- стартовая страница, login, chat, history, sources panel, knowledge base page, admin logs/roles page.
- streaming ответа через SSE.
backend/api(FastAPI transport layer)- REST endpoints (
/api/auth,/api/chat,/api/knowledge,/api/admin) + SSE endpoint (/api/chat/stream/{job_id}). - API не содержит бизнес-логики генерации: только валидация, auth, enqueue.
- REST endpoints (
backend/services(AI orchestration layer)- очередь, state machine job-ов, сбор контекста, summary-compression, retrieval policy, prompt assembly, вызов llama-server.
backend/persistence(data/state layer)- PostgreSQL: users, conversations, messages, summaries, jobs, logs, knowledge metadata.
- Redis: realtime pub/sub + lock per
(user_id, conversation_id). - Qdrant: векторный индекс chunk-ов знаний.
backend/infrastructure- docker-compose orchestration, env-driven config, structured JSON logging.
Ключевой принцип: transport layer отделен от AI-логики. API принимает сообщения и ставит jobs в очередь, worker обрабатывает их автономно.
PostgreSQL таблицы:
usersid,login(unique),password_hash,role(admin|user),created_at.
conversationsid,user_id,title,summary_text,summary_tokens,created_at,updated_at.
messagesid,conversation_id,user_id,role,content,attachments(JSON),token_count,summarized,created_at.
generation_jobsid,user_id,conversation_id,request_message_id,response_message_id,status,error_message,trace_id, timestamps.- индекс
ix_generation_jobs_scope_status (user_id, conversation_id, status).
knowledge_documentsid,title,source_type,source_uri,checksum(unique),metadata_json,created_at.
knowledge_chunksid,document_id,chunk_index,text,metadata_json,qdrant_point_id(unique),created_at.
pipeline_logsid,trace_id,user_id,conversation_id,message_id,payload(JSON),created_at.
model_capabilitiesmodel_hf(unique),multimodal(источник истины для поддержки изображений).
Qdrant:
- collection
knowledge_chunks- vector: embedding документа/chunk
- payload:
document_id,chunk_index,text,metadata
Redis:
- pub/sub channel
job:{job_id}для realtime статусов/streaming дельт. - lock key
lock:{user_id}:{conversation_id}для сериализации выполнения внутри диалога.
/Стартовая страница- что делает ассистент, где factual режим через базу знаний.
/login- JWT авторизация и регистрация обычного пользователя.
- первый админ создается автоматически из
ADMIN_LOGIN/ADMIN_PASS.
/chat- список диалогов, сообщения, composer, file input для изображений (только если
MODEL_MULTIMODAL=Trueи модель отмечена multimodal), streaming ответа. - автоназвание диалога: новый чат создаётся как
Новый диалог; после первого успешного ответа worker переименовывает его по первому сообщению пользователя (без вызова LLM — эвристика: убрать «расскажи про…», взять до 6 слов, максимум 60 символов; только картинка без текста →Изображение). - статусы:
queued / thinking / retrieving / responding / done / error. - thinking UX: показывается
"Рассуждаю, подождите...", chain-of-thought пользователю не показывается. - панель источников + явный режим:
На основе базы знаний/Общий ответ модели.
- список диалогов, сообщения, composer, file input для изображений (только если
/knowledge- загрузка документов в KB + список документов.
/admin- просмотр pipeline-логов (только admin).
- назначение другого пользователя админом (admin-only).
Мультипользовательская изоляция:
- все запросы и выборки в API и worker scoped по
user_id + conversation_id; - контексты разных пользователей и диалогов не пересекаются.
- При новом сообщении API:
- сохраняет
messages.role=user, - считает активные jobs в этом scope
(user_id, conversation_id)со статусамиqueued|thinking|retrieving|responding, - применяет лимит
MAX_QUEUE_SIZE(по умолчанию3), - если лимит превышен:
429+ понятное сообщение, - иначе создает
generation_jobs.status=queued.
- сохраняет
- Worker:
- выбирает старейший queued job;
- берет redis-lock на
(user_id, conversation_id); при неудаче возвращает job вqueued; - публикует статусы через Redis/SSE.
- Восстановление после сбоя:
- при старте worker все
thinking|retrieving|respondingпереводятся обратно вqueued. - очередь и история не теряются, так как источник истины в PostgreSQL.
- при старте worker все
В prompt попадает:
- системная инструкция;
summary_textдиалога;- последние
RAW_MESSAGES_SIZEraw сообщений; - дополнительно
MESSAGES_SUMMARY_SIZE - 1несуммаризированных bridge-сообщений; - RAG fragments (если retrieval включен);
- текущий вопрос.
Параметры:
RAW_MESSAGES_SIZEdefault10, валидируется>=1;MESSAGES_SUMMARY_SIZEdefault5; если<1, summary отключен;SUMMARY_TOKENS_SIZEdefault12000.
Проверка env-ограничений выполняется один раз при старте приложения (pydantic Settings).
Триггер:
- если общее число сообщений >
RAW_MESSAGES_SIZE + MESSAGES_SUMMARY_SIZE - 1.
Алгоритм:
- выбрать старые сообщения за пределами хвоста (
RAW + bridge); - брать только
summarized=False; - сгенерировать incremental summary с учетом текущего
summary_text; - merge старого summary + нового;
- посчитать токены summary;
- если токенов >
SUMMARY_TOKENS_SIZE, выполнить повторное сжатие summary; - сохранить
conversation.summary_text,summary_tokens; - пометить обработанные сообщения
summarized=True.
Устойчивость:
- после рестарта состояние summary и флаги
summarizedостаются в БД; - контекст восстанавливается детерминированно.
По умолчанию summary генерируется тем же llama-server, что и основной чат (LLAMA_HOST / LLAMA_PORT), с моделью SUMMARY_MODEL_HF или MODEL_HF.
Если включить USE_DEDICATED_SUMMARY_MODEL=True, worker отправляет запросы на сжатие контекста на отдельный llama-server (SUMMARY_LLAMA_HOST / SUMMARY_LLAMA_PORT). Рекомендуемая модель — более лёгкая unsloth/Qwen3.5-4B-GGUF:UD-Q4_K_XL, чтобы не отнимать VRAM у основной 9B-модели.
В docker-compose summary вынесен в отдельные profiles (чтобы CPU- и CUDA-контейнеры не боролись за один порт):
| Стек | Короткий profile | По отдельности |
|---|---|---|
| GPU + summary | llama-gpu-full |
llama-cuda + llama-summary-cuda |
| CPU + summary | llama-cpu-full |
llama + llama-summary |
| Только основная GPU | llama-cuda |
— |
# GPU: основная 9B + отдельная 4B для summary (одна команда)
USE_DEDICATED_SUMMARY_MODEL=True
docker compose --profile llama-gpu-full up --buildЧтобы не писать --profile каждый раз, добавьте в .env:
COMPOSE_PROFILES=llama-gpu-fullПосле этого достаточно:
docker compose up --build# CPU + summary
docker compose --profile llama-cpu-full up --buildНе смешивайте llama-summary (CPU) и llama-summary-cuda (GPU) — оба слушают SUMMARY_LLAMA_PORT (по умолчанию 8767).
Без profile summary отдельный контейнер не стартует — оставьте USE_DEDICATED_SUMMARY_MODEL=False.
knowledge_documents: мета документа и checksum для dedupe.knowledge_chunks: chunk тексты, metadata, привязка к qdrant point id.- Qdrant payload дублирует ключевые метаданные для retrieval без join.
- скользящее окно по символам:
chunk_size=800,overlap=120.
- Это простой baseline; легко заменить на sentence/token chunker.
Рекомендуемые поля:
title,domain,source_uri,doc_type,language,version,tags.
Сейчас минимум:
title+ пользовательскийmetadata_json.
- upload документа через
/api/knowledge/upload, - checksum dedupe,
- chunking,
- embedding (
EMBEDDING_MODEL; в Docker по умолчаниюhashing-384, без PyTorch/CUDA), - upsert points в Qdrant,
- запись
knowledge_chunksв PostgreSQL.
- Retrieval policy based on lexical markers (
гост,iso,scrum,kanban,devops,стандарт, etc.). - Если вопрос conversational/навигационный -> без RAG.
- Если factual question -> retrieval
top_k=5и включение retrieval-aware prompt.
- Qdrant cosine similarity top-k.
- В prompt вставляются chunks с
document_id,chunk_id,score. - В UI показываются использованные источники из фактических retrieval_results.
- Ассистент не генерирует выдуманные ссылки: источники берутся только из retrieval payload.
Текущий Docker-friendly embedding backend hashing-384 использует детерминированный
нормализованный hash-vector. Он нужен, чтобы проверить весь RAG pipeline без
скачивания тяжелых CUDA/PyTorch wheel-ов. Для более качественного semantic search
его можно заменить отдельным embedding-сервисом или transformer backend-ом.
В backend/app/services/prompt_templates.py:
MAIN_ASSISTANT_PROMPTRETRIEVAL_AWARE_PROMPTSUMMARIZATION_PROMPTOPTIONAL_MULTIMODAL_PROMPT
Режимы:
- обычный ответ;
- retrieval-aware factual response;
- summarization;
- optional multimodal.
Безопасность:
- chain-of-thought не показывается;
- при нехватке данных модель должна честно сказать о недостаточности KB.
Обязательные env:
MODEL_HFMODEL_CONTEXT_SIZEADMIN_LOGINADMIN_PASSLLAMA_HOSTLLAMA_PORTKV_CACHE_TYPE_KKV_CACHE_TYPE_VKV_CACHE_TYPE_K_CPUKV_CACHE_TYPE_V_CPULLAMA_IMAGELLAMA_CUDA_IMAGEMODEL_MULTIMODALMAX_QUEUE_SIZERAW_MESSAGES_SIZEMESSAGES_SUMMARY_SIZESUMMARY_TOKENS_SIZESUMMARY_MODEL_HFSUMMARY_MODEL_CONTEXT_SIZEUSE_DEDICATED_SUMMARY_MODELSUMMARY_LLAMA_HOSTSUMMARY_LLAMA_PORTDATABASE_URLREDIS_URLQDRANT_URLEMBEDDING_MODELLOG_LEVEL
Дополнительно:
JWT_SECRET
Файлы:
.envисключен из git;.env.exampleсодержит пример значений.
Предпочтительный запуск через llama-server:
llama-server \
-hf unsloth/Qwen3.5-9B-GGUF:UD-Q4_K_XL \
-c 32768 \
--cache-type-k turbo4 \
--cache-type-v turbo3 \
--host 0.0.0.0 \
--port 8765В docker-compose GPU-контейнеры (llama-cuda, llama-summary-cuda) используют образ с TurboQuant и флаги из KV_CACHE_TYPE_K / KV_CACHE_TYPE_V (по умолчанию turbo4 / turbo3).
Варианты:
- внешний локальный llama.cpp (
LLAMA_HOSTуказывает на host); - контейнер
llamaвdocker-compose(profilellama), если локально не установлен. - CPU-образ:
LLAMA_IMAGE→ghcr.io/ggml-org/llama.cpp:server(KV:q8_0по умолчанию). - GPU + TurboQuant:
LLAMA_CUDA_IMAGE→vito974/llama-cpp-turboquant:server-cuda12(см. §8.2). LLAMA_PLATFORM=linux/amd64фиксирует архитектуру контейнера и помогает избежать случайного запуска через эмуляцию.LLAMA_N_GPU_LAYERS,LLAMA_THREADS,LLAMA_PARALLELуправляют скоростью/параллельностью llama-server.KV_CACHE_TYPE_K/KV_CACHE_TYPE_V— тип KV-cache (TurboQuantturbo4на GPU по умолчанию, см. §8.2).LLAMA_EXTRA_ARGS— дополнительные флаги llama-server (не дублируйте--cache-type-*, они уже заданы через env).- Flash Attention намеренно не включается: на разных сборках llama.cpp этот параметр ведет себя нестабильно и требует явного значения
on|off|auto.
Для NVIDIA GPU используйте отдельный compose profile:
docker compose --profile llama-cuda up --buildПри этом llama-cuda получает сетевой alias llama, поэтому LLAMA_HOST=llama не меняется. На хосте должен быть установлен NVIDIA Container Toolkit / включена GPU-поддержка Docker Desktop.
Модель и cache скачивания сохраняются в примонтированной папке /models.
Путь на хосте задается переменной:
LLAMA_MODELS_DIR=${LOCALAPPDATA}/llama.cppНа Windows это обычно:
C:\Users\<user>\AppData\Local\llama.cpp
Для контейнеров выставлены HOME, HF_HOME, HUGGINGFACE_HUB_CACHE, XDG_CACHE_HOME и LLAMA_CACHE внутрь /models, а /root/.cache дополнительно смонтирован в ту же папку. Поэтому пересборка api/worker/frontend не должна заново скачивать модель. Повторное скачивание возможно только если удалить эту папку, выполнить docker compose down -v для томов, сменить MODEL_HF или если сам llama.cpp не нашел совместимый cache.
CPU fallback:
docker compose --profile llama up --buildДля локального запуска по умолчанию используется MODEL_CONTEXT_SIZE=32768. Окно 128000 для 9B-модели на 16 GB VRAM часто не помещается из-за KV-cache и может не загрузиться вообще.
Пример с CPU-контейнерной моделью:
docker compose --profile llama up --buildБез profile llama:
docker compose up --buildТогда API ожидает уже доступный внешний llama-server.
Модели задаются через .env и подхватываются llama-server при старте контейнера. Пересборка api / worker / frontend для смены модели не нужна — достаточно изменить env и перезапустить соответствующие контейнеры llama.
| Переменная | Назначение |
|---|---|
MODEL_HF |
Основная модель чата (контейнеры llama / llama-cuda) |
MODEL_CONTEXT_SIZE |
Размер контекста основной модели (-c) |
SUMMARY_MODEL_HF |
Модель для summary (контейнеры llama-summary / llama-summary-cuda или та же, что и чат, если флаг выключен) |
SUMMARY_MODEL_CONTEXT_SIZE |
Размер контекста summary-модели |
USE_DEDICATED_SUMMARY_MODEL |
True — summary идёт на отдельный llama-server; False — на основной |
Пример: основной чат на 9B, summary на 4B:
MODEL_HF=unsloth/Qwen3.5-9B-GGUF:UD-Q4_K_XL
MODEL_CONTEXT_SIZE=32768
USE_DEDICATED_SUMMARY_MODEL=True
SUMMARY_MODEL_HF=unsloth/Qwen3.5-4B-GGUF:UD-Q4_K_XL
SUMMARY_MODEL_CONTEXT_SIZE=16384
SUMMARY_LLAMA_HOST=llama-summary
SUMMARY_LLAMA_PORT=8767После правки .env:
docker compose --profile llama-gpu-full up -d
docker compose restart llama-cuda llama-summary-cudaПервый запуск с новым MODEL_HF / SUMMARY_MODEL_HF скачает GGUF в LLAMA_MODELS_DIR (общий cache для обоих контейнеров). Смена квантования или репозитория Hugging Face считается новой моделью.
Замена через docker compose напрямую: в docker-compose.yml команда контейнера уже использует ${MODEL_HF} и ${SUMMARY_MODEL_HF} из .env. Можно также переопределить переменные в shell без правки файла:
MODEL_HF=unsloth/Qwen3.5-9B-GGUF:UD-Q4_K_M docker compose --profile llama-cuda up -d llama-cudaДля внешнего (не compose) llama-server укажите -hf ... при ручном запуске и выставьте LLAMA_HOST / LLAMA_PORT (и при dedicated summary — SUMMARY_LLAMA_HOST / SUMMARY_LLAMA_PORT) в .env.
TurboQuant сжимает KV-cache во время инференса (не веса GGUF и не текст чата). Это позволяет держать более длинный контекст (MODEL_CONTEXT_SIZE) на том же GPU: ориентир ~3–4× меньше памяти на KV при turbo4/turbo3.
RAGAPP включает TurboQuant на уровне llama-server — Python-код (api / worker) менять не нужно.
Важно: официальный образ ghcr.io/ggml-org/llama.cpp:server-cuda не поддерживает turbo2/turbo3/turbo4 (только f16, q8_0, q4_0, …). Для TurboQuant по умолчанию используется форк:
LLAMA_CUDA_IMAGE=vito974/llama-cpp-turboquant:server-cuda12(сборка TheTom/llama-cpp-turboquant, образ на Docker Hub).
| Переменная | Назначение |
|---|---|
LLAMA_CUDA_IMAGE |
Docker-образ для llama-cuda / llama-summary-cuda |
KV_CACHE_TYPE_K |
K-cache на GPU (по умолчанию turbo4) |
KV_CACHE_TYPE_V |
V-cache на GPU (по умолчанию turbo3, асимметричный режим из документации форка) |
KV_CACHE_TYPE_K_CPU |
K-cache на CPU-контейнерах (по умолчанию q8_0) |
KV_CACHE_TYPE_V_CPU |
V-cache на CPU-контейнерах (по умолчанию q8_0) |
Контейнеры llama-cuda / llama-summary-cuda:
--cache-type-k ${KV_CACHE_TYPE_K} --cache-type-v ${KV_CACHE_TYPE_V}
Контейнеры llama / llama-summary (официальный CPU-образ):
--cache-type-k ${KV_CACHE_TYPE_K_CPU} --cache-type-v ${KV_CACHE_TYPE_V_CPU}
Рекомендуемые значения GPU (turbo* требуют LLAMA_CUDA_IMAGE с TurboQuant):
KV_CACHE_TYPE_K |
KV_CACHE_TYPE_V |
Комментарий |
|---|---|---|
turbo4 |
turbo3 |
Рекомендуемый default: качество K, сжатие V |
turbo4 |
turbo4 |
Максимальное качество TurboQuant |
q8_0 |
q8_0 |
Без TurboQuant (официальный образ ghcr.io/.../server-cuda) |
f16 |
f16 |
Без сжатия KV, максимум VRAM |
После смены образа или типа KV-cache:
docker compose pull llama-cuda llama-summary-cuda
docker compose --profile llama-gpu-full up -d llama-cuda llama-summary-cudaОшибка Unsupported cache type: turbo4 означает, что запущен официальный llama.cpp без форка — обновите LLAMA_CUDA_IMAGE в .env и пересоздайте контейнеры (docker compose up -d --force-recreate llama-cuda llama-summary-cuda).
Если качество ответов на длинных диалогах ухудшилось — откатите на q8_0/f16 или поставьте официальный ghcr.io/ggml-org/llama.cpp:server-cuda без --cache-type turbo*.
- Скопировать env:
cp .env.example .env-
Проверить значения
DATABASE_URL,REDIS_URL,QDRANT_URL,LLAMA_HOST/PORT. -
(Опционально) Проверить, что порты свободны:
.\scripts\check_ports.ps1Или вручную на Windows:
netstat -ano | findstr ":8766"
netstat -ano | findstr ":8767"
docker compose psЕсли порт занят старым контейнером: docker compose stop llama-summary llama-summary-cuda или смените SUMMARY_LLAMA_PORT в .env.
- Запустить:
docker compose up --build- Открыть:
- frontend:
http://localhost:5173 - api docs:
http://localhost:8000/docs
- При необходимости загрузить стартовую базу знаний:
python scripts/seed_knowledge_base.pyСкрипт логинится под ADMIN_LOGIN / ADMIN_PASS и индексирует Markdown-файлы из
knowledge_seed/ через публичный API /api/knowledge/upload.
Обычные пользователи могут зарегистрироваться на странице /login. Админ может
выдать роль администратора на странице /admin.
Формат JSON (structured logs), включая:
trace_iduser_idconversation_idmessage_idinboundcontext_windowcompressionretrievalllm_callperformancequeue
Как искать проблемы:
- фильтр по
trace_idчерез stdout лог контейнераapi/worker; - корреляция по
conversation_idдля конфликтов контекста; - по
queue.size/queue.positionнаходить перегруз; - по
llm_call.time_to_first_token_msсмотреть задержку до первого токена; - по
llm_call.tokens_per_second_after_firstсмотреть скорость генерации после первого токена; - по
performance.generation_latency_msсмотреть общее время LLM streaming-вызова; - по
performance.total_from_question_received_msсмотреть время от поступления вопроса до завершения генерации; - по
retrieval.latency_ms/performance.retrieval_latency_msсмотреть время поиска по векторной базе; retrieval.used=falseпри factual вопросе — индикатор tuning policy/KB.
Backend тесты (pytest + coverage threshold 40%):
- queue policy,
- context recovery,
- summary policy,
- rag policy,
- multimodal policy,
- llama-server stream parser,
- token estimator,
- API smoke happy path.
Запуск:
cd backend
pytestRAGAPP/
frontend/
src/
pages/
api.ts
App.tsx
backend/
app/
api/
services/
models.py
db.py
config.py
worker.py
tests/
knowledge_seed/
scrum.md
kanban.md
devops.md
project_documentation.md
scripts/
seed_knowledge_base.py
docker-compose.yml
.env.example
.gitignore
README.md
- Горизонтально масштабировать
api(stateless transport). - Масштабировать
workerнесколькими инстансами: redis-lock не даст одновременно обрабатывать один(user,conversation)scope. - PostgreSQL/Redis/Qdrant вынесены в отдельные сервисы и имеют volume-персистентность.
- Контекст и jobs восстанавливаются после рестартов через БД.
- Retrieval policy сейчас эвристическая (keyword-based), без ML-классификатора intent.
- Embeddings по умолчанию
hashing-384: это легкий backend для локального запуска, не полноценная transformer-семантика. - Chunking baseline по символам; токен-ориентированная нарезка может повысить качество.
- Нет отдельного observability stack (Prometheus/Grafana/OTel), только structured logs.
- Нет фоновых миграций Alembic; используется auto create tables при старте.
- Встроенный upload принимает текстовые документы; PDF/DOCX можно добавить отдельным parser-слоем перед
IngestionService. - Контейнер
llamaвключается профилем--profile llama; без него нужен внешнийllama-serverнаLLAMA_HOST:LLAMA_PORT.
Минимальный seed лежит в knowledge_seed/:
scrum.md- роли, события и артефакты Scrum;kanban.md- поток работ и WIP-limits;devops.md- CI/CD и базовые DevOps-практики;project_documentation.md- минимальный набор проектной документации.
Далее можно расширять через upload API пакетно (books, ГОСТ/методички, internal standards).