Вопрос-ответный сервис по тематическому корпусу «внедрение AI в бизнес-процессы»: каждый ответ строится только на проиндексированных источниках и сопровождается кликабельными цитатами.
Full-cycle RAG demo: ingestion of open-source corpora, local embeddings (multilingual-e5-base), pgvector retrieval, LLM answers with inline citations and an eval harness.
Обычная нейросеть отвечает «по памяти» — и может незаметно выдумать. Здесь она работает как библиотекарь: прежде чем отвечать, находит в собственной библиотеке нужные страницы и отвечает строго по ним, прикладывая номера сносок. Библиотека собирается заранее: открытые документы по теме режутся на фрагменты, каждому фрагменту присваивается числовой «адрес смысла» (эмбеддинг), и всё складывается в базу. Когда вы задаёте вопрос, он превращается в такой же «адрес», база мгновенно находит самые близкие по смыслу фрагменты, и уже по ним модель формулирует ответ со ссылками [1], [2] на первоисточники. Любую фразу ответа можно перепроверить одним кликом.
ingest/ingest.py (по расписанию / вручную)
git clone открытых репозиториев (MIT)
│ markdown → чанки ~600 токенов
▼
embedder/server.py (FastAPI + sentence-transformers,
│ intfloat/multilingual-e5-base, mode=passage)
▼ 768-мерные L2-нормированные векторы
PostgreSQL + pgvector (rag_sources, rag_chunks, HNSW cosine)
▲
│ top-k по cosine similarity
server/server.mjs ── POST /api/agents/rag/answer
│ 1) embedQuery (mode=query) → вектор вопроса
│ 2) retrieveChunks → топ-6 фрагментов
│ 3) prompt «отвечай только по источникам» → OpenRouter LLM
│ 4) парсинг сносок [N] → citations
▼
public/agents/rag/ (чат с цитатами и списком источников)
eval-контур: ingest/eval.py гоняет ingest/eval-questions.yaml через API,
считает recall@5 и latency p50/p95, пишет baseline в rag_eval_runs.
- Node.js без фреймворков (
node:http), единственная зависимость сервера —postgres. - OpenRouter API как генератор ответов: используются free-модели, primary выбирается по живому ранкингу, при ошибке провайдера — один retry на fallback-модель.
- PostgreSQL 16 + pgvector 0.6, HNSW-индекс по cosine (
ingest/schema.sql). - Локальный embedder: FastAPI + sentence-transformers, модель
intfloat/multilingual-e5-base(768d, RU+EN), CPU-only sidecar (embedder/). - Ingestion на Python: клонирование git-репозиториев с открытой лицензией, чанкинг markdown по токенам (target 600 / max 750 / overlap 100), батчевый эмбеддинг (
ingest/ingest.py). - Eval-харнесс: recall@5 и latency по фиксированному набору вопросов (
ingest/eval.py), результаты в БД. - Фронтенд: статический HTML + vanilla JS, без сборки.
cd server
npm install
cp ../.env.example .env # вписать OPENROUTER_API_KEY
npm start
# открыть http://127.0.0.1:8787/agents/rag/Без Postgres и embedder-а сервер поднимется, но /api/agents/rag/* будут отвечать configured: false — это ожидаемо. Полный цикл:
- Поднять Postgres с pgvector и применить
ingest/schema.sql. - Запустить embedder:
cd embedder && pip install -r requirements.txt && uvicorn server:app --host 127.0.0.1 --port 8788(на сервере — черезinstall.sh+ systemd-юнит). - Заполнить корпус:
cd ingest && pip install -r requirements.txt && RAG_DB_URL=... RAG_EMBEDDER_URL=http://127.0.0.1:8788 python ingest.py --sources sources.yaml --workdir ./sources. - Раскомментировать
RAG_DB_URLиRAG_EMBEDDER_URLв.envи перезапустить сервер. - (Опционально) снять baseline:
python eval.py --questions eval-questions.yaml— recall@5, latency p50/p95.
| Переменная | Обязательна | Описание |
|---|---|---|
OPENROUTER_API_KEY |
да (для ответов) | Ключ OpenRouter; без него /answer отвечает 503 |
RAG_DB_URL |
да (для RAG) | Строка подключения к Postgres с pgvector |
RAG_EMBEDDER_URL |
да (для RAG) | URL embedder-sidecar-а, например http://127.0.0.1:8788 |
LLM_POOL_HOST / LLM_POOL_PORT |
нет | Адрес и порт HTTP-сервера (по умолчанию 127.0.0.1:8787) |
OPENROUTER_MODEL |
нет | Жёстко заданная primary-модель (иначе — топ живого ранкинга) |
LLM_POOL_DEFAULT_MODEL |
нет | Модель по умолчанию, если ранкинг недоступен |
OPENROUTER_FALLBACK_MODEL |
нет | Fallback-модель для retry |
LLM_POOL_REASONING_EFFORT |
нет | Уровень reasoning (low/medium/high) |
LLM_POOL_RANKING_URL |
нет | Источник живого ранкинга free-моделей |
RAG_EMBED_MODEL / RAG_EMBED_DIM |
нет | Отображаются в /status (по умолчанию 768) |
RAG_TOP_K |
нет | Сколько фрагментов доставать (по умолчанию 6, максимум 10) |
RAG_MIN_SCORE |
нет | Порог cosine-score для включения фрагмента в промпт (0.5) |
RAG_QUERY_MAX_CHARS / RAG_SNIPPET_MAX_CHARS |
нет | Лимиты длины вопроса и сниппета |
RAG_EMBEDDER_TIMEOUT_MS |
нет | Таймаут запроса к embedder (15000) |
LLM_POOL_RATE_WINDOW_MS / LLM_POOL_RATE_LIMIT |
нет | Окно и лимит rate-limit по IP (5 мин / 20) |
LLM_POOL_CHAT_TIMEOUT_MS / LLM_POOL_RANKING_TIMEOUT_MS |
нет | Таймауты запросов к OpenRouter и ранкингу |
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/llm-pool/health |
Health-check: наличие ключа, текущая primary/fallback-модель |
| GET | /api/llm-pool/quota |
Статус free-tier квоты OpenRouter (скользящее окно ошибок) |
| GET | /api/agents/rag/status |
Статус корпуса: число источников/фрагментов, embedder, последний eval; 503 configured:false без БД |
| POST | /api/agents/rag/answer |
{ "question": "...", "k"?: 1–10 } → ответ с citations, retrievedSources, таймингами retrieval и метаданными модели |
- Rate limit: 20 запросов с одного IP за 5 минут (429 с
Retry-After). - Вопрос обрезается до 1000 символов; тело запроса — до 128 KB.
- Требуются внешние сервисы: PostgreSQL + pgvector (корпус и eval) и embedder-sidecar (Python, ~1 GB RAM под модель). Без них роуты RAG честно отвечают
configured: false. - Генерация идёт через free-модели OpenRouter: возможны паузы и 429; сервер делает один retry на fallback-модель, статус квоты виден в
/api/llm-pool/quota. - Faithfulness в eval-контуре не считается (шумно на free-моделях) — метрика зарезервирована как NULL.
Часть портфолио antonov-ai.ru · MIT License

