Как устроен Pravia

Что происходит между вопросом пользователя и ответом, какие числа мы измерили и чего система сознательно не делает.

Что такое Pravia

Pravia — платформа AI-сотрудников, которые отвечают на вопросы по документам самой компании. Сотрудник работает там, где он нужен: на сайте компании во встраиваемом виджете, в Telegram или через API.

Как проходит вопрос

Шесть шагов от загруженного документа до готового ответа. Обычным языком — как это работает, и отдельно техническим блоком — как именно настроено.

Блок для инженеров: реальные имена, числа и файлы. Всё остальное на странице описывает ту же систему обычным языком.

  1. Извлечение текста

    Документ или сайт превращается в текст. Принимаются PDF, DOCX, Markdown и TXT, а также адрес сайта и его карта сайта. Сервер берёт только эти форматы — всё остальное отклоняется.

    Технические детали

    • разрешённые расширения: .pdf .md .markdown .docx .txt
    • импорт с сайта: адрес страницы и sitemap.xml
  2. Разбиение на фрагменты

    Длинный текст режется на фрагменты средней величины, чтобы поиск возвращал точные куски, а не целые документы. Соседние фрагменты частично перекрываются, чтобы ответ не обрывался на границе.

    Технические детали

    • RecursiveCharacterTextSplitter
    • maxTokens 500, overlap 50
    • lib/ai/chunk.ts
  3. Как текст становится числами

    Каждый фрагмент превращается в компактное числовое описание его смысла. Считает модель, запущенная прямо на сервере Pravia: текст компании наружу не уходит и во внешний сервис не отправляется.

    Технические детали

    • onnx-community/embeddinggemma-300m-ONNX
    • 768 измерений, mean pooling + L2-нормализация
    • ONNX Runtime в процессе сервера
    • lib/ai/embed.ts, lib/ai/models.ts
  4. Хранение

    Текст и его числовое описание лежат в одной базе вместе с остальными данными Pravia, а специальный индекс ускоряет поиск по смыслу. Идентификатор AI-сотрудника дублируется прямо в строки с фрагментами — это сделано намеренно, ради скорости поиска.

    Технические детали

    • PostgreSQL 16 + pgvector
    • колонка vector(768)
    • HNSW idx_chunks_embedding, vector_cosine_ops, m=16, ef_construction=64
    • bot_id денормализован в строки фрагментов — осознанно, ради скорости поиска
  5. Поиск

    Вопрос пользователя ищется по смыслу среди фрагментов этого AI-сотрудника. Возвращается не больше трёх лучших совпадений и только те, что прошли порог релевантности. Если по смыслу не нашлось ничего, есть запасной поиск по словам.

    Технические детали

    • оператор <=> — косинусная дистанция
    • limit 3, threshold 0.7 — константы VECTOR_SEARCH в lib/ai/query-build.ts
    • пул кандидатов растёт с размером корпуса, потолок — 60
    • диверсификация: сколько фрагментов может дать один документ
    • rerank (bge-reranker-base): есть, но по умолчанию выключен — RERANK_ENABLED не задан
    • запасной поиск: совпадение по словам (ILIKE)
  6. Генерация ответа

    Найденные фрагменты и вопрос уходят в модель, она пишет ответ. Ответ приходит посимвольно, и клиент показывает его по мере поступления. Настройки подобраны так, чтобы формулировки были максимально предсказуемыми.

    Технические детали

    • 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-сотрудника за минуты.