← 블로그 목록
뉴스클러스터링임베딩LLMRAGFastAPIPython증분처리

증분 뉴스 클러스터링 파이프라인 설계 — 기존 이벤트에 새 기사 합류시키기

2026년 4월 16일

뉴스를 하루에 수백 건 수집하면 새로운 문제가 생깁니다.

비트코인 급등 소식이 10개 소스에서 들어오면 타임라인이 같은 사건 기사로 가득 찹니다. 정작 다른 중요한 이슈는 묻혀버립니다. 목표는 하나였습니다. 같은 사건에 대한 기사들을 묶어서 이벤트 단위로 보여주기.

이 글에서는 그 파이프라인과 매시간 실행할 때 기존 이벤트를 유지하는 증분 방식을 설명합니다.


왜 3단계 파이프라인인가

단순한 방법부터 시도했습니다.

  • 키워드 그루핑: 특정 코인명이 들어간 기사를 전부 하나로 묶어버려서 전혀 다른 사건들이 합쳐졌습니다
  • 발행 시간 그루핑: 같은 사건을 다른 날에 다룬 기사를 연결하지 못했습니다

결국 임베딩 → 클러스터링 → LLM 병합 3단계로 구성했습니다.


1단계: 임베딩

기사 제목 + 요약을 벡터로 변환합니다.

모델은 BAAI/bge-m3를 사용합니다. 4096차원이지만 Matryoshka 방식으로 512차원으로 truncate해서 저장합니다. 정확도는 거의 그대로이고 저장 공간과 검색 속도에서 유리합니다.

본문 전체 대신 제목+요약만 쓰는 이유가 있습니다. 기사 본문에는 광고 문구, 관련 없는 링크, 편집자 주석 같은 노이즈가 많습니다. 핵심 정보는 제목과 요약에 충분히 담겨 있습니다.

임베딩 서버는 LLM 서버(LM Studio)와 분리된 Infinity라는 별도 서비스를 사용합니다. 처음엔 하나로 처리했는데 LLM 요청이 들어오는 동안 임베딩이 블로킹되는 문제가 있었습니다. 서버를 분리하니 둘이 독립적으로 처리됩니다.


2단계: Centroid Agglomerative Clustering

코사인 유사도가 0.88 이상이면 같은 클러스터로 묶습니다.

각 클러스터의 centroid(중심 벡터)를 유지하면서 가장 유사한 두 클러스터를 반복 병합합니다. 유사도가 0.88 미만이 되면 멈춥니다.

이 단계의 역할은 텍스트가 거의 동일한 기사를 먼저 제거하는 것입니다.

0.88을 선택한 이유: 0.85로 낮추면 미묘하게 다른 사건(예: 비트코인 ETF 출시와 이더리움 ETF 출시)이 합쳐졌습니다. 0.90으로 올리면 실제로 같은 사건인데 표현이 달라서 분리되는 경우가 생겼습니다.


3단계: LLM으로 거시 이슈 병합

임베딩 클러스터링은 텍스트 유사도 기반이라 한계가 있습니다.

이란-이스라엘 교전과 호르무즈 해협 선박 억류는 같은 지정학적 사건의 다른 측면이지만 문장이 달라서 유사도가 낮게 나옵니다. 이런 거시적 연결은 LLM이 더 잘 처리합니다.

클러스터링 결과에서 상위 60개를 뽑아 LLM에 넘깁니다. LLM은 같은 거시 이슈에 속하는 클러스터들을 하나의 이벤트로 묶습니다.

이란 전쟁 + 호르무즈 해협 봉쇄 + 중동 유가 급등
→ 하나의 이벤트: 중동 지정학적 긴장

이 단계에서는 **thinking 모드(fast=False)**를 사용합니다. 매시 정각 한 번만 실행하고, 한번 생성된 이벤트 제목은 이후에 변경하지 않기 때문에 첫 번째 판단이 정확해야 합니다.

가드레일 두 개를 적용합니다.

  • 단독 기사(1건짜리 클러스터)의 평균 관련도 점수가 60 미만이면 이벤트 생성 제외
  • LLM 병합 후에도 단독 기사 이벤트는 관련도 점수 80 이상일 때만 생성

증분 클러스터링 — 기존 이벤트는 건드리지 않는다

타임라인은 매시 정각 자동 업데이트됩니다.

처음엔 매번 전체를 재처리했습니다. 오전 8시 이벤트를 삭제하고 오전 9시 기사까지 합쳐서 처음부터 다시 만드는 방식이었는데, 이벤트 제목이 매번 달라지고 이벤트 ID도 바뀌는 문제가 있었습니다.

핵심 원칙: 한번 생성된 이벤트의 제목·감정·태그는 변경하지 않는다.

새 기사가 들어오면 두 단계로 처리합니다.

Step 1 — 분류: LLM이 각 기사를 기존 이벤트 번호에 분류합니다.

def build_incremental_classification_messages(
    existing_events: list[dict],
    new_articles: list[dict],
) -> list[AIMessage]:
    # 증분 클러스터링 Step 1: 새 기사를 기존 이벤트 번호에 분류.
    # existing_events: [{number, title, sentiment, count}, ...]
    # new_articles:    [{id, title, summary, relevance_score, published_at}, ...]
    # LLM은 숫자(이벤트 번호)만 출력하며 title을 생성/변경하지 않음.
    events_text = ""
    for e in existing_events:
        sentiment_str = f" [{e['sentiment']}]" if e.get('sentiment') else ""
        events_text += f"{e['number']}. {e['title']}{sentiment_str} (기사 {e.get('count', 0)}건)\n"
    # 프롬프트 규칙:
    # - event_number: 기존 이벤트 번호(1~N) 또는 'UNCLASSIFIED'
    # - 기존 이벤트의 title은 출력하지 말 것 (숫자만 사용)
    # - 확실하지 않으면 'UNCLASSIFIED'로 분류
    return [...]

Step 2 — 신규 생성: UNCLASSIFIED 기사들만 모아서 새 이벤트를 만듭니다.

def build_incremental_clustering_messages(
    existing_events: list[dict],
    new_articles: list[dict],
    max_new_events: int = 5,
) -> list[AIMessage]:
    # 기존 이벤트에 새 기사를 합류/신규 생성하는 증분 클러스터링 프롬프트.
    # 판단 규칙:
    # - 새 기사가 기존 이벤트와 같은 사건 → 기존 title 그대로, article_ids에 새 기사 번호 추가
    # - 어떤 기존 이벤트에도 해당하지 않으면 → 새 이벤트 생성
    # - 기존 이벤트의 title과 sentiment는 절대 변경하지 말 것
    # - article_ids에는 새 기사 번호만 포함 (기존 기사 번호는 넣지 말 것)
    # - 신규 이벤트는 최대 max_new_events개까지만 생성
    return [...]

Redis에 cutoff 타임스탬프를 저장해서 중복 처리를 막습니다. published_at > cutoff 기사만 처리하기 때문에 같은 기사가 두 번 LLM을 거치지 않습니다.


RAG — 과거 기사로 맥락 생성

각 이벤트에 배경 설명 텍스트가 붙습니다.

비트코인 ETF 자금 유출이라는 이벤트만 보면 맥락이 없습니다. 3월에도 유사한 패턴이 있었고 그때는 2주 뒤 반등했다는 과거 맥락이 있어야 의미가 생깁니다.

  1. 이벤트 제목을 임베딩 벡터로 변환합니다
  2. pgvector로 과거 90일 기사 중 유사한 것 상위 20개 검색합니다
  3. Reranker로 실제 관련성이 높은 5개로 추립니다 (score 0.3 이상)
  4. LLM이 이 과거 기사들을 참고해서 2~3문장 맥락 텍스트를 생성합니다

Reranker를 별도로 사용하는 이유가 있습니다. 임베딩 검색은 의미 유사도 기반이라 표면적으로 비슷한 기사를 가져오지만, 실제로 맥락에 쓸 만한 기사는 다를 수 있습니다. Reranker는 쿼리와 기사를 함께 놓고 교차 어텐션으로 평가하기 때문에 더 정확합니다.


타임라인 최종 구조

각 이벤트에 다음이 붙습니다.

  • 감정 배지: 호재 / 악재
  • 태그: BTC, ETF, 규제 등
  • 맥락 텍스트: LLM이 생성한 2~3문장 배경 설명
  • 근거 기사: 이 이벤트를 구성한 실제 기사들
  • 관련 과거 기사: RAG로 찾아온 과거 맥락

하루 전체를 종합한 **시장 심리 지수(0~100)**와 요약이 상단에 표시됩니다.


각 단계가 필요한 이유

단계역할없으면
임베딩 + 클러스터링텍스트 중복 제거LLM에 중복 기사가 과다하게 넘어감
LLM 거시 병합표현이 다른 같은 사건 연결지정학·거시 이슈가 분산됨
증분 방식기존 이벤트 안정성 유지매시간 이벤트 제목·ID가 바뀜
RAG과거 맥락 주입이벤트가 배경 없이 단편적으로 표시됨
0

댓글 0

Ctrl+Enter