Как устроен Pravia
Что происходит между вопросом пользователя и ответом, какие числа мы измерили и чего система сознательно не делает.
Что такое Pravia
Pravia — платформа AI-сотрудников, которые отвечают на вопросы по документам самой компании. Сотрудник работает там, где он нужен: на сайте компании во встраиваемом виджете, в Telegram или через API.
Обзор в видео
Тот же материал, что и текст ниже, — в видео.
Обзор Pravia
Загрузка обзора…
Файл обзора в формате MP4.
Скачать MP4Три версии одной записи
Как проходит вопрос
Шесть шагов от загруженного документа до готового ответа. Обычным языком — как это работает, и отдельно техническим блоком — как именно настроено.
Блок для инженеров: реальные имена, числа и файлы. Всё остальное на странице описывает ту же систему обычным языком.
Извлечение текста
Документ или сайт превращается в текст. Принимаются PDF, DOCX, Markdown и TXT, а также адрес сайта и его карта сайта. Сервер берёт только эти форматы — всё остальное отклоняется.
Технические детали
- разрешённые расширения: .pdf .md .markdown .docx .txt
- импорт с сайта: адрес страницы и sitemap.xml
Разбиение на фрагменты
Длинный текст режется на фрагменты средней величины, чтобы поиск возвращал точные куски, а не целые документы. Соседние фрагменты частично перекрываются, чтобы ответ не обрывался на границе.
Технические детали
- RecursiveCharacterTextSplitter
- maxTokens 500, overlap 50
- lib/ai/chunk.ts
Как текст становится числами
Каждый фрагмент превращается в компактное числовое описание его смысла. Считает модель, запущенная прямо на сервере Pravia: текст компании наружу не уходит и во внешний сервис не отправляется.
Технические детали
- onnx-community/embeddinggemma-300m-ONNX
- 768 измерений, mean pooling + L2-нормализация
- ONNX Runtime в процессе сервера
- lib/ai/embed.ts, lib/ai/models.ts
Хранение
Текст и его числовое описание лежат в одной базе вместе с остальными данными Pravia, а специальный индекс ускоряет поиск по смыслу. Идентификатор AI-сотрудника дублируется прямо в строки с фрагментами — это сделано намеренно, ради скорости поиска.
Технические детали
- PostgreSQL 16 + pgvector
- колонка vector(768)
- HNSW idx_chunks_embedding, vector_cosine_ops, m=16, ef_construction=64
- bot_id денормализован в строки фрагментов — осознанно, ради скорости поиска
Поиск
Вопрос пользователя ищется по смыслу среди фрагментов этого AI-сотрудника. Возвращается не больше трёх лучших совпадений и только те, что прошли порог релевантности. Если по смыслу не нашлось ничего, есть запасной поиск по словам.
Технические детали
- оператор <=> — косинусная дистанция
- limit 3, threshold 0.7 — константы VECTOR_SEARCH в lib/ai/query-build.ts
- пул кандидатов растёт с размером корпуса, потолок — 60
- диверсификация: сколько фрагментов может дать один документ
- rerank (bge-reranker-base): есть, но по умолчанию выключен — RERANK_ENABLED не задан
- запасной поиск: совпадение по словам (ILIKE)
Генерация ответа
Найденные фрагменты и вопрос уходят в модель, она пишет ответ. Ответ приходит посимвольно, и клиент показывает его по мере поступления. Настройки подобраны так, чтобы формулировки были максимально предсказуемыми.
Технические детали
- DeepSeek-совместимый chat completions
- temperature: 0, max_tokens: 8000 — потолок длины ответа
- стриминг по сырому SSE, без AI SDK
- <knowledge_context> помечает найденный текст как недоверенные данные и запрещает выполнять инструкции из него — изоляция от подмены запроса
- KNOWLEDGE_CONTEXT_WRAPPER в lib/ai/generate.ts
Качество поиска — измерено, а не обещано
Ниже — числа из автоматического прогона на боевой конфигурации. Мы публикуем их вместе с корпусом и командой, иначе цифра ничего не значит.
- Файл замера
- tests/fixtures/rag-baseline.json · metricsVersion 1.1.0
- Дата замера
- 29 сентября 2026
- Коммит
- 272755f
- Конфигурация
- { limit: 3, threshold: 0.7 }, режим vector — это боевая конфигурация Pravia
- Корпус
- 67 документов / 717 фрагментов / 2 AI-сотрудника
- Оценено кейсов
- 29 из 32: у трёх нет эталонного ответа, поэтому они не считаются ни попаданием, ни промахом
- recall@1
- 0.454
- recall@3
- 0.672
- precision@3
- 0.299
- MRR
- 0.644
Что здесь не считается
Два показателя не вычислимы в боевой конфигурации, и мы не подставляем на их место другие числа.
- recall@5боевая конфигурация возвращает максимум 3 фрагмента, поэтому «recall@5» был бы recall@3 под другим именем
- nDCG@10та же причина: измерять 10 позиций при выдаче из 3 — значит переименовать nDCG@3
Это измерение только поиска
Модель генерации не вызывалась ни разу. Мы мерили, какие фрагменты находятся по запросу, а не какие ответы они дают.
Про этот корпус
Это оценочный корпус, а не демонстрационный набор из пяти документов, который показан в видео.
Чего система не делает
Ниже — осознанные границы, а не недоработка. Мы перечисляем их, чтобы вы могли решить сами, подходит ли вам такой размен.
Нет переписывания вопроса
Вопрос уходит на поиск в том виде, в каком его набрали. Система не переписывает его, не подставляет синонимы, не строит гипотетический ответ и не разбивает сложный вопрос на части. Единственное преобразование — перевод с русского на английский, и в боевой конфигурации он отключён: базовая модель и так понимает оба языка.
Технические детали
- query rewriting, query expansion, HyDE, декомпозиция, исправление опечаток: нет
- перевод ru→en: условный, в боевой конфигурации отключён
Нет урезания найденного под лимит
Отобранные фрагменты не обрезаются под бюджет длины запроса. Число 8000 в настройках — это потолок для длины ответа, а не ограничитель того, сколько текста попадёт в запрос.
Технические детали
- max_tokens 8000 — потолок длины ответа
- усечение найденного контекста под бюджет токенов: нет
Нет проверки ссылок
Мы просим модель отвечать только по найденным фрагментам, но не проверяем, что каждое утверждение ответа действительно из них следует. Это требование в инструкции, а не техническая гарантия.
Технические детали
- проверка, что утверждения ответа следуют из найденного: нет
- основание — текст системной инструкции
Переоценка выдачи выключена по умолчанию
Дополнительный проход модели, которая пересматривает найденные фрагменты, в системе есть, но по умолчанию не запускается — включается одной переменной окружения.
Технические детали
- RERANK_ENABLED: по умолчанию не задан → выключено
- модель переоценки: bge-reranker-base
Оценка в интерфейсе — упрощённая
Рядом с найденным фрагментом показывается число от 0 до 1. Для поиска по смыслу это «насколько далеко», перевёрнутое в удобную шкалу, а не вероятность правильного ответа. Если сработала запасная выдача, число всегда одно и то же.
Технические детали
- оценка в интерфейсе = 1 − косинусная дистанция
- запасная выдача (поиск по словам): оценка жёстко 0.5
Документ перезагружается целиком
Повторная загрузка документа не оставляет половину: прежние фрагменты удаляются и новые записываются одной операцией. Если часть фрагментов обработать не удалось, это честно помечается отдельным статусом, а не выдаётся за полностью успешную загрузку.
Технические детали
- delete-then-insert в одной транзакции
- уникальный индекс (document_id, chunk_index) — защита от дублей
- частичный прогон: embeddingStatus: "partial"
На чём это собрано
Фактический стек, без рекламных формулировок.
- Приложение
- Next.js 16 (App Router) + React 19
- База данных
- PostgreSQL 16 + pgvector
- Фоновые задачи
- BullMQ — опционально: пустой REDIS_URL переводит обработку документов в синхронный режим, система продолжает работать
- Аутентификация
- Better Auth
- Стили
- Tailwind CSS 4
- Векторизация
- @huggingface/transformers (ONNX), локально на сервере
- Генерация
- DeepSeek-совместимый API, свой ключ компании (BYOK) на тарифе Business
Готовы нанять AI-сотрудника?
Начните бесплатно — без кредитной карты. Подключите источники знаний и запустите AI-сотрудника за минуты.