Розробка AI-системи для генерації новинних дайджестів
Персоналізований новинний дайджест зі сотень джерел — завдання, яке людина не вирішить вручну в прийнятні терміни. AI-система моніторить джерела, кластеризує публікації по темам, видаляє дублі та генерує зв'язний дайджест для конкретного користувача або сегмента аудиторії.
Пайплайн збору та обробки
class NewsDigestPipeline:
def __init__(self, sources: list[NewsSource]):
self.crawler = NewsCrawler(sources)
self.deduplicator = SemanticDeduplicator(threshold=0.85)
self.clusterer = NewsClusterer()
self.summarizer = NewsSummarizer()
self.ranker = PersonalizedRanker()
async def generate_digest(
self,
user_profile: UserProfile,
period_hours: int = 24
) -> Digest:
# 1. Збір новин за період
articles = await self.crawler.fetch_since(
datetime.utcnow() - timedelta(hours=period_hours)
)
# 2. Видалення дублів (одна новина з 20 джерел → 1 запис)
unique_articles = self.deduplicator.deduplicate(articles)
# 3. Кластеризація по подіям
clusters = self.clusterer.cluster(unique_articles)
# 4. Персоналізований ранжирування кластерів
ranked_clusters = self.ranker.rank(clusters, user_profile)
# 5. Генерація резюме по кластеру (multi-document summarization)
summaries = [
self.summarizer.summarize_cluster(cluster)
for cluster in ranked_clusters[:user_profile.digest_size]
]
return Digest(items=summaries, generated_at=datetime.utcnow())
Дедублювання новин
Одна подія висвітлюється десятками видань. Near-duplicate detection:
class SemanticDeduplicator:
def __init__(self, threshold: float = 0.85):
self.encoder = SentenceTransformer("paraphrase-multilingual-mpnet-base-v2")
self.threshold = threshold
def deduplicate(self, articles: list[Article]) -> list[Article]:
# Кодування заголовків + лида
texts = [f"{a.title}. {a.lead}" for a in articles]
embeddings = self.encoder.encode(texts, batch_size=256)
# MinHash LSH для ефективного пошуку подібних
lsh = MinHashLSH(threshold=self.threshold, num_perm=128)
groups = lsh.find_groups(embeddings)
# З кожної групи беремо першоджерело (за часом публікації)
result = []
for group in groups:
primary = min(group, key=lambda a: a.published_at)
primary.alternative_sources = [a.url for a in group if a != primary]
result.append(primary)
return result
Multi-document summarization для кластера
Завдання: з 5-20 статей про одну подію скласти коротке резюме без втрати ключових деталей. Стратегія map-reduce:
def summarize_cluster(articles: list[Article]) -> ClusterSummary:
# Ранжирування статей за авторитетністю джерела та повнотою
ranked = rank_articles_by_quality(articles)
if len(articles) <= 3:
# Малий кластер — пряма суммаризація
combined = "\n\n".join(a.full_text for a in ranked[:3])
summary = llm.generate(f"Коротко виклади ключові факти:\n{combined}", max_tokens=200)
else:
# Великий кластер — map-reduce
individual_summaries = [
llm.generate(f"Виділіть ключові факти (2-3 речення):\n{a.full_text}", max_tokens=100)
for a in ranked[:10]
]
# Об'єднуємо унікальні факти
summary = llm.generate(
f"Складіть зв'язний абзац з цих фактів (без повторів):\n" +
"\n".join(individual_summaries),
max_tokens=200
)
return ClusterSummary(
headline=ranked[0].title,
summary=summary,
key_sources=[a.url for a in ranked[:3]],
article_count=len(articles),
topic_tags=extract_tags(articles)
)
Персоналізація
Три рівні персоналізації:
Тематичні інтереси: явні (користувач вибрав рубрики) + неявні (клики, час читання). Collaborative filtering для нових користувачів.
Глибина матеріалу: одні переважають короткий абзац, інші — розгорнутий аналіз. Визначається за поведінкою.
Формат доставки: email-дайджест, Telegram-бот, push-повідомлення в додатку, RSS-лента. Частота: вранці, увечері, раз на тиждень — за вибором користувача.
Метрики якості дайджестів
- CTR по статтях: який % матеріалів користувач відкриває — ціль 15%+
- Read-through rate: дочитуваність — ціль 60%+
- Diversity score: різноманітність тематик — не всі статті на одну тему
- Freshness: середній час від подіяви до дайджесту — ціль < 4 години для важливих новин







