Хабр накопил за 20 лет уникальный корпус технических статей — но его интерфейс устроен как лента соцсети: побеждает свежее, а через две недели статья фактически исчезает. Автор igor_suhorukov решил это исправить: собрал 34 270 лучших публикаций, прогнал через LLM и сгруппировал по смыслу — без опоры на хабы и теги.

Почему теги и хабы не решают задачу

Хабр позволяет автору указать до пяти хабов и произвольные теги. Проблема в том, что эти метки субъективны: один автор пишет «машинное обучение», другой — «ML», третий вообще ставит хаб «Хабр». Поиск по ключевым словам возвращает шум.

Параллельно растёт объём контента с коммерческой мотивацией: продвижение HR-бренда, реклама облачных сервисов, нейросгенерированные тексты ради SEO. Они вытесняют из ленты качественные технические материалы, на которые автор тратит дни. Алгоритм ленты не различает глубину — он реагирует на частоту и вовлечённость.

Семантическая кластеризация решает обе проблемы сразу: статьи группируются по реальному смыслу, а не по тому, что автор написал в поле «теги».

Какие статьи попали в выборку

Из всего архива Хабра отобрано около 10% публикаций — те, что прошли хотя бы один из четырёх порогов качества:

КритерийПорог
Рейтинг≥ 100
Комментарии≥ 100
Закладки≥ 100
Просмотры≥ 100 000

Итог — 34 270 статей за 2006–2026 годы. Каждая хранится как markdown-файл с метаданными: заголовок, дата, автор, хабы, теги, просмотры, рейтинг, закладки, комментарии. Приложение обходит папку и раскладывает поля в базу автоматически.

Извлечение признаков через LLM

Каждую статью читает модель google/gemma-4-31b-it через OpenAI-совместимый API (в данном случае — OpenRouter). На выходе — JSON примерно из 30 полей:

json
{
  "title": "...",
  "summary": "...",
  "meaning": "...",
  "tldr": "...",
  "keywords": ["...", "..."],
  "topics": ["...", "..."],
  "key_findings": ["...", "..."],
  "audience": "...",
  "tone": "neutral",
  "genre": "tutorial",
  "document_type": "article",
  "domain": "...",
  "completeness": 0.85,
  "contradictions": false,
  "demagogy_techniques": [],
  "advertising": false,
  "personal_data": false,
  "nsfw": false
}

Запрос идёт в 30 параллельных потоков. Медианный вызов занимает 53 секунды, весь корпус обработан за 30 часов активной работы (двое суток с перерывами).

bash
# Пример запуска через OpenRouter-совместимый эндпоинт
# Приложение поддерживает также локальные Ollama и llama.cpp
python process_articles.py \
  --input ./articles/ \
  --model google/gemma-4-31b-it \
  --api-base https://openrouter.ai/api/v1 \
  --workers 30 \
  --output ./features.duckdb

На 34 270 статей ушло 266 млн входных и 52 млн выходных токенов — в среднем 7 800 входных и 1 500 выходных на статью. Итоговая стоимость: $75,30, или около $0,002 за статью.

Эмбеддинги и кластеризация

После извлечения признаков строятся векторные представления. Отдельный вектор создаётся для аннотации, смысла, аудитории, каждой темы, топика и вывода. Модель — Qwen3-VL-Embedding-2B, размерность вектора — 2 048.

python
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("Qwen/Qwen3-VL-Embedding-2B")

texts = [article["summary"] + " " + article["meaning"] for article in articles]
embeddings = model.encode(texts, batch_size=32, show_progress_bar=True)

Всего получается 384 756 векторов. Всё хранится в одной базе DuckDB размером около 13,6 ГБ.

Кластеризацию выполняет HDBSCAN с ускорением на GPU (CUDA или Vulkan). Минимальный размер кластера — 3 статьи. Названия групп генерирует отдельный вызов LLM по содержимому кластера.

python
import cuml  # RAPIDS cuML для GPU-ускорения
from cuml.cluster import HDBSCAN

clusterer = HDBSCAN(min_cluster_size=3, metric="euclidean")
labels = clusterer.fit_predict(embeddings)

Где ломается и что учесть

Стоимость растёт нелинейно. $75 за 34 тысячи статей — это при использовании облачного API с выгодным тарифом. Если обрабатывать весь Хабр (около 340 тысяч статей), счёт вырастет до ~$750. Локальные модели через Ollama или llama.cpp снимают эту проблему, но требуют GPU с достаточным VRAM для Gemma-4-31B.

Качество JSON не гарантировано. Модель иногда нарушает схему. Нужна валидация ответа перед записью в базу — иначе битые записи тихо ломают эмбеддинги.

DuckDB на 13,6 ГБ — не для слабых машин. Для локального запуска нужно минимум 16 ГБ RAM, комфортно — 32 ГБ.

Эмбеддинги устаревают. Если добавить новые статьи, пересчитывать придётся только их векторы, но кластеризацию — заново по всему корпусу.

Что попробовать дальше

Тот же pipeline применим к любому корпусу: внутренняя wiki, Confluence, личные заметки в Obsidian. Данные не уходят в облако, если использовать локальные модели. AI-агент получает структурированный индекс с точными «рецептами» решений вместо размытого полнотекстового поиска.

Следующий шаг — RAG-интерфейс поверх DuckDB: задаёшь вопрос, агент ищет ближайшие векторы, возвращает конкретные статьи с аннотациями.


FAQ

Сколько реально стоит обработать 34 000 статей через облачный API?

$75,30 через OpenRouter с моделью Gemma-4-31B — около $0,002 за статью. Локальный запуск через Ollama или llama.cpp обходится дешевле, но нужна видеокарта с достаточным VRAM.

Можно ли запустить это без GPU?

Извлечение признаков через API не требует GPU на вашей стороне. HDBSCAN-кластеризацию можно запустить на CPU через пакет hdbscan, но на 384 тысячах векторов это займёт значительно больше времени, чем GPU-версия через RAPIDS cuML.

Почему результаты анализа хранятся на английском?

Английский снижает расход токенов и улучшает качество работы компактных локальных LLM. Русскоязычные статьи анализируются корректно, но JSON с результатами возвращается на английском.

Подходит ли этот подход для личной базы знаний из заметок?

Да. Pipeline не привязан к Хабру: любой корпус markdown-файлов с метаданными обрабатывается по той же схеме. Локальные модели позволяют не отправлять данные в облако.

Что такое HDBSCAN и чем он лучше K-means для этой задачи?

HDBSCAN (Hierarchical Density-Based Spatial Clustering) не требует задавать количество кластеров заранее и умеет помечать выбросы как «шум». Для тематической кластеризации текстов это важно: количество реальных тем в корпусе неизвестно заранее, а часть статей не попадает ни в одну группу.

Источники