KB API — HTTP-мост ChromaDB между Lyumi и Pushkin
Построен 28 апреля 2026 как ответ на вопрос: «Как обогатить посты Пушкина каноном из ChromaDB Люми, не раздувая Pushkin до 6.88GB?»
Проблема
Pushkin (lyumi-pushkin, ~200MB) не имеет ChromaDB и embedding-модели e5-large на борту — это сделано специально, чтобы держать его лёгким и независимо деплоить.
Но для качественных постов нужен канон IOGP/OSHA/ISO/НПА РК — то что уже лежит в ChromaDB Люми (167,970 чанков после чистки 28 апреля).
Три варианта были рассмотрены:
| Вариант | Плюс | Минус |
|---|---|---|
| А. Раздуть Pushkin (chromadb + e5-large) | Независимый сервис | Образ 6+GB, дублирование RAM, тяжёлый старт |
| Б. HTTP API в Lyumi → Pushkin клиент | Pushkin 200MB, embed-модель используется один раз | +1 точка отказа (если Lyumi падает — KB недоступен) |
| В. Отказаться от enrichment | Простота | Теряем канон в постах, остаётся только web search |
Выбрали Б. Зрелая архитектура: Pushkin лёгкий, Lyumi уже держит модель в RAM 24/7, добавить HTTP-эндпоинт — пара сотен строк.
Архитектура
┌──────────────────────────┐ ┌─────────────────────────────┐
│ Pushkin (200MB) │ │ Lyumi (~6.88GB) │
│ lyumi-pushkin-bot │ │ lyumi-hse-bot │
│ │ HTTP │ │
│ digest_retriever.py ────┼────────▶│ kb_api.py (aiohttp.web) │
│ (httpx client) │ GET │ port 8000 │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ retriever.py │
│ │ │ (vector search, │
│ │ │ e5-large in RAM) │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ChromaDB (lyumi-chroma) │
│ │ │ 167,970 чанков │
└──────────────────────────┘ └─────────────────────────────┘
▲ ▲
│ │
└────────── docker network ─────────────┘
lyumi-net (external)
Эндпоинты Lyumi (kb_api.py)
GET /healthz
Пинг для проверки что сервис жив.
docker exec lyumi-pushkin-bot python -c \
"import httpx; print(httpx.get('http://lyumi-hse-bot:8000/healthz').json())"
# {"ok": true, "service": "lyumi-kb-api"}
GET /kb_search?q=...&k=15&max_chars=10000
Vector search по 5 коллекциям ChromaDB. Возвращает форматированный текст с источниками, готовый для инжекта в LLM-контекст.
Response:
{
"text": "[#1 | score=0.87 | коллекция=hard_space | тип=npa | юрисдикция=KZ]\nИсточник: ...\n...",
"chunks": 3,
"top_score": 0.87
}
Параметры:
- q — поисковый запрос (обязательный)
- k — лимит чанков в выдаче (default 15, max 50)
- max_chars — максимальный размер итогового текста (default 10000, max 30000)
Использует retriever.search_with_threshold_async — голый vector search без BM25/Cohere rerank (для канона хватит, экономим время на простых запросах).
Pushkin клиент (digest_retriever.py)
Компактный httpx-клиент:
KB_API_URL = os.getenv("KB_API_URL", "http://lyumi-hse-bot:8000")
KB_API_TIMEOUT = float(os.getenv("KB_API_TIMEOUT", "30"))
async def fetch_from_knowledge_base(query, k=15, max_chars=10000):
url = f"{KB_API_URL}/kb_search"
async with httpx.AsyncClient(timeout=KB_API_TIMEOUT) as client:
resp = await client.get(url, params={"q": query, "k": k, "max_chars": max_chars})
...
Используется в digest_bot.enrich_with_knowledge_base() для:
- /topic
- /analytics
- /case
- Все автодрафты по расписанию (npa, trends, case, topic, weekly)
Fallback: если Lyumi недоступна (timeout, 5xx, network error) — возвращает только web_content из Sonar. Пушкин не падает, просто без канона в этом посте.
Docker network
Общая сеть lyumi-net создаётся вручную как external:
docker network create lyumi-net
Оба compose-файла подключают сервисы к этой сети:
/opt/lyumi/docker-compose.yml:
services:
bot:
container_name: lyumi-hse-bot
networks: [lyumi-net]
# + volumes, env_file как раньше
networks:
lyumi-net:
external: true
/opt/lyumi-pushkin/docker-compose.yml:
services:
pushkin:
container_name: lyumi-pushkin-bot
networks: [lyumi-net]
networks:
lyumi-net:
external: true
DNS работает по container_name: Pushkin зовёт http://lyumi-hse-bot:8000, Docker сам резолвит.
Запуск HTTP-сервера в Lyumi
В bot.py основной await dp.start_polling(bot) обёрнут в asyncio.gather параллельно со стартом aiohttp:
from kb_api import start_kb_api
await asyncio.gather(
dp.start_polling(bot),
start_kb_api(retriever),
)
start_kb_api поднимает aiohttp.web.AppRunner на порту 8000 и держит coroutine живой через asyncio.Event().wait(). Один event loop — два сервера (Telegram polling + HTTP).
Боевая проверка
Первый прогон 28 апреля 2026, 03:13:51:
status: 200
chunks: 3
top_score: 0.87
text head: [#1 | score=0.87 | коллекция=hard_space | тип=npa | юрисдикция=KZ]
Источник: Об утверждении Правил по обеспечению безопасности и охраны труда при работе на...
Запрос «наряд-допуск на высоте» отвечает за ~1 секунду, top score 0.87 — высокая релевантность.
Стоимость
- Pushkin — те же 200MB, +30 строк httpx-клиента, плюсовая зависимость только httpx (уже была)
- Lyumi — +
aiohttp(уже транзитивно через chromadb), +130 строк kb_api.py - Сеть — внутренняя docker-net, наружу 8000 НЕ публикуется
- CPU — 1-2 запроса в день при /topic и /analytics, копейки
- Latency — ~500-1500мс на запрос (vector search + сериализация)
Что НЕ закрыто
- Кеширование запросов — повторные запросы Pushkin'a с похожими q не кешируются. Можно добавить SQLite-кеш в kb_api как в основном semantic_cache.
- Rate limiting — нет защиты от стороннего сервиса в lyumi-net (если в сеть подключат третий контейнер). Сейчас только Pushkin использует — норм.
- Reranking — kb_api использует только vector search. Если качество выдачи будет страдать — добавить опциональный
?rerank=trueпараметр с Cohere. - TLS — внутри docker-net не нужно, наружу не публикуется. Если когда-то понадобится — TLS terminating через nginx-proxy.
Связанные
- [[lyumi/pushkin]] — Pushkin AI-newsroom
- [[lyumi/bot_v3]] — Lyumi бот
- [[lyumi/cleanup_ru_apr28]] — чистка RU контента (тот же день)
- Memory
project_pushkin_workflow_stable_apr28— workflow стабилизировался