Прототип веб-сервиса, который принимает запрос на естественном языке, находит подходящие закупки по 44-ФЗ и 223-ФЗ и объясняет условия участия с помощью RAG-ассистента, подтверждая каждое утверждение ссылкой на карточку закупки.
Проект закрывает пять задач:
| № | Задача | Где смотреть |
|---|---|---|
| 1 | Прототип дизайна веб-сервиса | frontend/index.html, dist/tenderai-prototype.html |
| 2 | Прототип RAG-ассистента | backend/app/rag/ |
| 3 | Frontend и backend модули | frontend/, backend/ |
| 4 | Тестирование работоспособности | tests/, docs/testing-report.md |
| 5 | Сайт стартап-проекта | site/index.html |
- Сайт стартап-проекта — https://fanot.github.io/tenderai/
- Интерактивный прототип сервиса — https://fanot.github.io/tenderai/demo.html
Сайт публикуется автоматически из ветки main (см. .github/workflows/deploy-pages.yml).
Откройте dist/tenderai-prototype.html двойным кликом. Файл самодостаточен: внутри
и интерфейс, и данные, и офлайн-версия поискового движка. Backend не нужен.
pip install -r requirements.txt
cd backend
uvicorn app.main:app --reload --port 8000| Адрес | Что это |
|---|---|
| http://localhost:8000/ | Интерфейс поиска и RAG-ассистента |
| http://localhost:8000/startup | Сайт стартап-проекта |
| http://localhost:8000/docs | Swagger UI (документация API) |
pytest # 70 автотестов
python scripts/run_evaluation.py # прогон + отчёт в docs/testing-report.mdpython scripts/generate_dataset.py # синтетический корпус закупок
python scripts/build_standalone.py # dist/tenderai-prototype.html + dist/deploy/python scripts/build_standalone.py создаёт папку dist/deploy/ — её можно
опубликовать на любом бесплатном статическом хостинге. Пошаговые инструкции для
GitHub Pages, Netlify и Cloudflare Pages — в docs/deploy.md.
Запрос пользователя
│
▼
[1] Разбор запроса ─────────► закон, регион, категория, НМЦК, СМП, актуальность
│ (backend/app/rag/query_parser.py)
▼
[2] Гибридный поиск
├── BM25 (лексика, точные термины, реестровые номера) ─ bm25.py
└── Плотные векторы (семантика, словоформы) ─ embeddings.py + vector_store.py
│
▼
[3] Слияние RRF (Reciprocal Rank Fusion) ─ retriever.py
▼
[4] Жёсткие фильтры + ограниченный бизнес-реранк ─ retriever.py
▼
[5] Сборка контекста и генерация ответа ─ assistant.py + llm.py
▼
[6] Guardrail: проверка ссылок [n] ─ assistant.py
▼
Ответ со ссылками на источники
backend/
app/
main.py точка входа FastAPI
api/routes.py HTTP-эндпоинты
api/schemas.py Pydantic-схемы запросов и ответов
core/config.py конфигурация через переменные окружения
core/store.py загрузка данных и сборка индекса
rag/
text.py нормализация, токенизация, стемминг русского текста
bm25.py BM25 (Okapi) + нечёткое сопоставление опечаток
embeddings.py провайдеры эмбеддингов (local / OpenAI / Yandex / GigaChat)
vector_store.py векторное хранилище в памяти
chunking.py разбиение карточки закупки на смысловые чанки
query_parser.py разбор естественно-языкового запроса
retriever.py гибридный поиск, RRF, фильтры, реранк
llm.py провайдеры генерации + офлайн-фолбэк
assistant.py RAG-конвейер и guardrails
data/
tenders.json корпус закупок
schema.md описание формата карточки
eis_adapter.py адаптер к выгрузке ЕИС
frontend/
index.html одностраничное приложение (поиск, ассистент, аналитика)
offline-engine.js офлайн-движок для автономного прототипа
site/
index.html сайт стартап-проекта
tests/ автотесты (API, качество поиска, guardrails, производительность)
scripts/ генерация данных, сборка прототипа, прогон тестирования
docs/ отчёт о тестировании и метрики
dist/ автономный HTML-прототип и папка для публикации
docs/deploy.md инструкции по публикации сайта
Гибридный поиск вместо одного метода. BM25 хорошо находит точные термины, коды ОКПД2 и реестровые номера, но не понимает синонимы. Векторный поиск понимает смысл, но размывает точные совпадения. Результаты объединяются алгоритмом Reciprocal Rank Fusion — он не требует калибровки шкал двух ранжирований.
Ограниченный бизнес-реранк. Свежесть, статус и совпадение атрибутов дают надбавку не
более 20% к оценке релевантности. Без такого ограничения бизнес-признаки перевешивали
релевантность: на размеченном наборе это стоило ~0.16 MRR (проверяется тестом
test_hybrid_beats_lexical_only).
Фильтры применяются после ранжирования, а не до отсечения кандидатов. Иначе узкий запрос («ИТ-закупки в Москве до 20 млн по 44-ФЗ») мог не найти существующие совпадения, потому что они не попали в первые N кандидатов.
Сменные провайдеры ИИ. Эмбеддинги и генерация вынесены за интерфейс: локальный режим (без ключей и сети), OpenAI, YandexGPT, GigaChat. При недоступности внешнего API сервис деградирует к локальному режиму, а не отдаёт ошибку.
Guardrails против галлюцинаций. Ответ формируется только из найденного контекста;
каждая ссылка [n] проверяется на существование источника; при нулевой выдаче ассистент
прямо сообщает, что закупок не найдено.
Все настройки — через переменные окружения (см. .env.example):
| Переменная | По умолчанию | Назначение |
|---|---|---|
EMBEDDINGS_PROVIDER |
local |
local / openai / yandex / gigachat |
LLM_PROVIDER |
local |
local / openai / yandex / gigachat |
OPENAI_API_KEY |
— | ключ OpenAI |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
адрес OpenAI-совместимого API |
YANDEX_API_KEY, YANDEX_FOLDER_ID |
— | доступ к YandexGPT |
GIGACHAT_API_KEY |
— | доступ к GigaChat |
DEFAULT_TOP_K |
10 |
размер выдачи по умолчанию |
CORS_ORIGINS |
* |
разрешённые источники для CORS |
Без ключей сервис полностью работоспособен в локальном режиме.
| Метод | Путь | Назначение |
|---|---|---|
GET |
/api/health |
Состояние сервиса и размер индекса |
GET |
/api/meta |
Справочники для фильтров |
POST |
/api/search |
Поиск закупок: запрос, фильтры, сортировка, пагинация |
GET |
/api/tenders/{id} |
Карточка закупки и похожие закупки |
GET |
/api/similar/{id} |
Похожие закупки |
POST |
/api/assistant/ask |
Вопрос RAG-ассистенту, ответ со ссылками |
GET |
/api/analytics/overview |
Аналитика по корпусу закупок |
Пример:
curl -X POST http://localhost:8000/api/search \
-H 'Content-Type: application/json' \
-d '{"query":"разработка информационной системы в Москве до 20 млн по 44-ФЗ"}'Полный отчёт — docs/testing-report.md, машиночитаемые метрики — docs/metrics.json.
| Показатель | Значение |
|---|---|
| Автотесты | 70 пройдено, 0 провалено |
| P@1 | 68% |
| Recall@5 / Recall@10 | 95% / 97% |
| MRR | 0.803 |
| Латентность поиска, p95 | ~80 мс |
| Корректность цитирования | 8/8 без замечаний |
- Корпус закупок синтетический — детерминированно сгенерирован и повторяет структуру
выгрузки ЕИС. Для продуктива нужен коннектор (
app/data/eis_adapter.py) и синхронизация. - Локальный эмбеддер — хеширующий векторизатор; он работает без ключей, но уступает полноценной модели. Для продуктива подключается внешний провайдер.
- Индекс хранится в памяти процесса. Для миллионов карточек нужен pgvector / Qdrant и инкрементальная индексация.
- Нет аутентификации, мультитенантности и сохранённых подписок на запросы.