TRENDSHIFT · 2026.06.15

Horizon 딥다이브
— 흩어진 뉴스를 "나만의 AI 뉴스 레이더"로 모아 매일 브리핑하는 파이프라인의 설계

HorizonHacker News·Reddit·RSS·Telegram·Twitter/X·GitHub·금융 뉴스 등 흩어진 정보원을 한 파이프라인으로 끌어모아, 중복 병합 → AI 점수 매기기 → 임계값 필터 → 배경지식 보강 → 댓글 요약 → 이중언어(영/중) 일일 브리핑 생성까지 자동으로 처리하는 파이썬 프로젝트다. 결과물은 GitHub Pages 사이트·이메일 뉴스레터·웹훅(Feishu/Slack/Discord)·MCP 서버로 배달된다. "좋은 뉴스는 흩어져 있고, 나쁜 뉴스는 끝이 없다"는 문제의식에서 출발해, AI로 노이즈를 줄이되 "신뢰하는 출처·사람의 취향"이라는 인간 레이어를 끝까지 유지하는 것이 핵심 철학이다. (저장소: Thysrael/Horizon · Python 3.11+ · MIT · uv 패키징)

이 문서는 "멀티소스 수집 + AI 정제 파이프라인의 아키텍처에서 무엇을 배울 수 있는가"에 초점을 둔 한국어 해설이다. README만 베끼지 않고 orchestrator.py·models.py·ai/·scrapers/·mcp/ 소스까지 직접 읽어 정리했다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 시스템 / 환경 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 레포가 무엇을 하는 물건인가.

핵심 메시지

"여러 사이트를 매일 직접 돌며 읽던 일을,
수집 → AI 큐레이션 → 브리핑 발행으로 자동화한 것."

당신이 매일 아침 Hacker News, 좋아하는 서브레딧, 블로그 RSS, 텔레그램 채널을 일일이 열어 "오늘 볼 만한 게 뭐지?"를 가린다면, Horizon은 그 "여러 출처에서 가져오기 → 같은 이야기 합치기 → 중요도 점수 매기기 → 배경·댓글 붙이기 → 한 편의 요약 만들기" 전 과정을 하나의 비동기 파이프라인으로 자동화한다.

핵심은 단순 요약기가 아니라는 점이다. AI는 노이즈를 줄이는 데는 뛰어나지만, 어떤 출처를 신뢰할지·어떤 댓글이 기사를 다르게 보게 하는지는 사람의 취향이 필요하다. Horizon은 출처·임계값·모델·언어·배달 채널을 모두 하나의 JSON 설정으로 사용자가 통제하게 해서 그 인간 레이어를 유지한다.

Horizon은 한 줄로 말하면 "설정 가능한 개인용 AI 뉴스 어그리게이터"다. API 호출 한 번으로 끝나는 도구가 아니라, 7단계 워크플로(정의 → 수집 → 중복제거 → 점수·필터 → 보강 → 요약 → 배달)HorizonOrchestrator가 조율한다. AI 제공자는 Claude·GPT·Gemini·DeepSeek·Doubao·MiniMax·Ollama 등 OpenAI 호환 API를 자유롭게 꽂을 수 있고, 결과는 영어·중국어로 동시 생성된다. GitHub Actions 크론으로 매일 자동 실행해 Pages에 게시하는 것이 대표 사용 시나리오다.

용어
뉴스 어그리게이터 (News Aggregator)
여러 출처에 흩어진 콘텐츠를 한곳으로 모아 정리해 주는 시스템. Horizon은 단순히 모으는 데 그치지 않고 AI로 점수를 매겨 걸러내고(filter), 배경지식과 커뮤니티 토론을 붙여(enrich) "읽을거리 리스트"로 가공한다는 점이 일반 RSS 리더와 다르다.
용어
일일 브리핑 (Daily Briefing)
하루치 중요 뉴스를 요약·태깅·참조링크와 함께 묶은 구조화된 마크다운 보고서. Horizon은 이를 영/중 두 언어로 생성하고 GitHub Pages·이메일·웹훅으로 배달한다.

2왜 주목받는가

트렌딩 이유 · 일반 RSS 리더/요약봇 대비 장점.

뉴스 요약봇은 흔하다. Horizon이 주목받는 이유는 "진짜 실전에서 골치 아픈 부분"을 파이프라인 단계마다 풀었기 때문이다. 첫째 7개 이질적 출처를 단일 모델(ContentItem)로 통합, 둘째 교차 출처 URL 병합 + AI 의미론적 중복 제거(같은 사건의 여러 보도 합치기), 셋째 0~10 점수화 후 임계값 필터로 노이즈 차단, 넷째 웹 검색으로 배경지식을 보강하고 댓글까지 요약, 다섯째 4가지 배달 채널 + MCP 노출이다.

비교 항목일반 RSS 리더 / 요약봇Horizon
출처 범위RSS 위주, 단일 종류HN·RSS·Reddit·Telegram·X·GitHub·OpenBB·OSSInsight 7+종 통합
중복 처리없음(같은 기사 여러 번 노출)URL 정규화 병합 + AI 의미론적 토픽 중복제거
중요도 판단최신순/인기순AI 0~10 점수 + 임계값 필터 + 카테고리별 쿼터
배경 보강제목·요약만웹 검색(DDGS)으로 낯선 개념·기업·용어 배경 추가
커뮤니티 의견없음HN/Reddit 상위 댓글 수집·요약(여론 맥락)
언어원문 그대로영어·중국어 동시 생성
AI 제공자고정(보통 OpenAI)Claude·GPT·Gemini·DeepSeek·Ollama 등 교체 가능
배달앱 내 피드GitHub Pages·이메일·웹훅(Feishu/Slack/Discord)·MCP
기존 방식의 한계
"그냥 모아서 요약하기"의 세 가지 함정

출처를 단순히 다 긁어 LLM에 던지면 세 가지가 무너진다. ① 같은 사건이 HN·Reddit·X에 중복되어 같은 요약이 반복되고, ② 중요도 구분 없이 사소한 업데이트와 패러다임 전환이 같은 비중으로 나오며, ③ 맥락 없는 제목만 읽고는 "이게 왜 중요한지" 알 수 없다. 게다가 출처마다 API·인증·페이지네이션이 전부 달라 코드가 스파게티가 된다.

Horizon의 해결
"단계별 파이프라인" + "통합 모델" + "사람이 통제하는 설정"

Horizon은 수집을 모든 스크래퍼가 공통 BaseScraper 인터페이스를 구현하고 결과를 단일 ContentItem으로 반환하게 통일했다. 그 위에서 병합 → 점수 → 필터 → 보강 → 요약을 순차 단계로 쪼개, 각 단계가 명확한 입출력을 갖는 파이프라인을 만들었다. 무엇을 신뢰하고 얼마나 엄격히 거를지는 전부 data/config.json으로 사용자가 정한다. "넓게 모으되, 사람 기준으로 정확하게 거른다"가 설계 철학이다.

3기술 스택 전체 지도

백엔드(파이프라인 엔진)·프론트엔드(브리핑/배달)·인프라 각각.

① 핵심 엔진 (백엔드) — 비동기 수집 + 멀티 LLM

Horizon은 100% 파이썬(3.11+)이며, 수집은 본질적으로 I/O 바운드(여러 사이트 응답 대기)라 httpx.AsyncClient + asyncio.gather로 모든 출처를 동시에 긁는다. AI 단계는 제공자 추상화 계층(ai/client.py)으로 어떤 LLM이든 같은 complete(system, user) 인터페이스로 부른다.

요소역할
Python 3.11+전체 언어. 타입힌트·async/await·dataclass 적극 사용.
httpx비동기 HTTP 클라이언트. AsyncClient로 모든 스크래퍼가 커넥션 공유.
asyncio동시성 런타임. gather(*tasks, return_exceptions=True)로 출처 동시 수집.
pydantic v2핵심 데이터 모델(ContentItem·Config) 검증·직렬화. HttpUrl 타입 등.
anthropic·openai·google-genai3대 LLM SDK. 그 외는 OpenAI 호환 base_url로 흡수(DeepSeek·Doubao·Ollama 등).
tenacityAI 호출 재시도(retry + wait_exponential). LLM 일시 오류 대응.
feedparserRSS/Atom 파싱.
ddgsDuckDuckGo 검색 — 배경지식 보강(enrich) 단계의 웹 검색기.
beautifulsoup4스크래핑한 HTML에서 본문 추출.
rich터미널 진행 막대·컬러 로그(파이프라인 단계별 출력).
mcpModel Context Protocol 서버 — 파이프라인 단계를 AI 어시스턴트 도구로 노출.

② 프론트엔드 · 출력 채널

요소역할
Markdown 브리핑최종 산출물. DailySummarizer가 요약·태그·참조로 구조화한 .md 생성.
GitHub Pages (Jekyll)생성된 md를 docs/_posts/에 front matter 붙여 복사 → Pages가 매일 사이트 발행.
이메일 (SMTP/IMAP)services/email.py — 셀프호스팅 뉴스레터. 구독/해지를 IMAP으로 자동 처리.
웹훅services/webhook.py — Feishu/Lark·DingTalk·Slack·Discord·커스텀 엔드포인트로 푸시.
markdownmd → HTML 변환(이메일/웹 표시용).

③ 인프라 · 배포 · 자동화

요소역할
uv + pyproject.tomlastral-sh의 초고속 패키지 매니저. uv sync --frozen + uv.lock으로 재현 가능 설치.
Dockerpython:3.11-slim 기반. uv로 의존성 설치, data/를 볼륨 마운트.
docker-compose--hours 24 윈도우로 일일 실행. restart: unless-stopped.
GitHub Actionsdaily-summary.yml 크론 → 브리핑 생성 → Pages 자동 배포(deploy-docs.yml).
설정 마법사horizon-wizard — 관심사 입력 → config.json 자동 생성(대화형).
선택 의존성(extras)openbb(금융), twitter(Playwright), dev(pytest)를 extra로 분리.
용어
I/O 바운드 (I/O-bound)
작업 시간의 대부분을 CPU 계산이 아니라 외부 응답 대기(네트워크)에 쓰는 작업. 이런 문제는 스레드를 늘리기보다 asyncio처럼 "기다리는 동안 다른 출처를 동시에 긁는" 협력적 동시성이 효율적이다. Horizon이 7개 출처를 한 번에 수집하는 비결.
용어
OpenAI 호환 API (OpenAI-compatible)
OpenAI의 /chat/completions 요청·응답 형식을 그대로 따르는 LLM 엔드포인트. DeepSeek·Doubao·MiniMax·Ollama·Together 등 수많은 제공자가 이 규격을 지원하므로, Horizon은 base_url만 바꿔 끼우면 같은 코드로 다른 모델을 쓸 수 있다.

4아키텍처 심화 분석

설정 → 동시 수집 → 병합 → 점수·필터 → 보강 → 이중언어 요약 → 배달.

전체 흐름 한눈에

입력은 JSON 설정 1개 + 시간 윈도우(기본 24시간). HorizonOrchestrator.run()이 전 단계를 조율한다. 설정에 켜진 스크래퍼들이 asyncio.gather로 동시에 콘텐츠를 긁어 와 모두 ContentItem으로 정규화하고, URL 병합 → AI 점수 → 임계값 필터 → AI 토픽 중복제거 → 카테고리 쿼터 → 웹검색 보강 → 언어별 요약 → 배달 순으로 흐른다. 각 단계가 명확한 "입력 리스트 → 출력 리스트" 변환이라 추적과 디버깅이 쉽다.

data/config.json + 시간 윈도우(--hours 24) │ ┌───────────────▼────────────────┐ │ HorizonOrchestrator.run() │ │ (전 단계 조율 · rich 로그) │ └───────────────┬────────────────┘ │ ══════ ① FETCH : 모든 출처 동시 수집 (asyncio.gather) ══════ ┌─────────┬─────────┬─────────┬─────────┬─────────┐ ▼ ▼ ▼ ▼ ▼ ▼ HN RSS Reddit Telegram X/Twitter GitHub ... (각 스크래퍼 = BaseScraper 구현 → List[ContentItem] 반환) └─────────┴─────────┴────┬────┴─────────┴─────────┘ ▼ 모두 ContentItem 으로 통일 ══════ ② MERGE : 교차 출처 URL 중복 병합 ══════ 정규화 URL(www·슬래시·프래그먼트 제거)로 그룹핑 → 내용 가장 풍부한 것을 primary, 댓글/메타 합치기 │ ══════ ③ SCORE : AI 0~10 중요도 점수 (1차 패스) ══════ ContentAnalyzer.analyze_batch() · 세마포어로 동시성 제한 → item.ai_score / ai_reason / ai_summary / ai_tags │ ══════ ④ FILTER : 임계값 컷 + 내림차순 정렬 ══════ ai_score >= ai_score_threshold(예: 6.0) 만 통과 │ ══════ ⑤ DEDUP : AI 의미론적 토픽 중복제거 ══════ "같은 사건 보도"끼리 묶어 최고점만 남기고 댓글 병합 ("Gemma 4 출시" vs "Gemma 4 탈옥"은 별개로 유지) │ ══════ ⑥ BALANCE : 카테고리 쿼터 + 전체 상한 ══════ 그룹별 limit / max_items 로 한쪽 주제 쏠림 방지 │ ══════ ⑦ ENRICH : 웹검색 배경지식 보강 (2차 패스) ══════ DDGS 검색 → AI로 낯선 개념·기업·용어 배경 생성 │ ══════ ⑧ SUMMARIZE : 언어별 일일 브리핑 ══════ for lang in [en, zh]: DailySummarizer.generate_summary() │ ══════ ⑨ DELIVER : 다채널 배달 ══════ ┌──────────┬──────────┬──────────┬──────────┐ ▼ ▼ ▼ ▼ ▼ data/ GitHub Email Webhook MCP summaries Pages (SMTP) (Feishu 등) (도구 노출)

핵심 #1 — 통합 데이터 모델 ContentItem

아키텍처의 토대는 "7개 이질적 출처를 단 하나의 모델로 통일"한 것이다. HN 스토리든 트윗이든 GitHub 릴리스든, 스크래퍼는 전부 같은 ContentItem(pydantic 모델)을 반환한다. 이후 모든 단계는 출처가 무엇인지 신경 쓰지 않고 이 모델만 다룬다.

ContentItem 모델 (models.py)
class ContentItem(BaseModel):
    id: str              # "{source}:{subtype}:{native_id}"
    source_type: SourceType   # github / hackernews / rss / reddit ...
    title: str
    url: HttpUrl
    content: Optional[str] = None
    author: Optional[str] = None
    published_at: datetime
    metadata: Dict[str, Any] = {}   # score, comments, subreddit ...
    # ── AI 분석 결과(나중에 채워짐) ──
    ai_score: Optional[float] = None   # 0~10 중요도
    ai_reason: Optional[str] = None
    ai_summary: Optional[str] = None
    ai_tags: List[str] = []

출처별 고유 정보(서브레딧·좋아요 수·댓글 등)는 자유로운 metadata 사전에 담아 모델을 더럽히지 않는다. ai_* 필드는 처음엔 비어 있다가 점수·요약 단계에서 채워지는 "단계가 진행되며 풍부해지는 객체"다.

핵심 #2 — 두 종류의 중복제거

Horizon은 중복을 두 단계로 처리한다. 단순한 것은 코드로, 어려운 것은 AI로.

단계방식잡는 것
② 교차 출처 병합URL 정규화 후 문자열 비교(코드)같은 링크가 HN·Reddit·X에 동시 등장 → 하나로
⑤ 토픽 중복제거제목·태그·요약을 AI에 한 번에 보내 판정다른 링크지만 같은 사건 보도 → 최고점만 남김

⑤가 영리한 점은 프롬프트(TOPIC_DEDUP_SYSTEM)에서 "같은 제품이라도 다른 사건이면 중복 아님"("Gemma 4 출시" vs "Gemma 4 탈옥")이라고 못 박고, 애매하면 분리 쪽으로 기우는(err on the side of keeping separate) 보수적 설계라는 점이다. 또 점수 내림차순으로 정렬된 상태에서 처리해 각 중복 그룹의 첫 번째(최고점)가 자동으로 대표가 되게 한다.

비유 — 두 단계 중복제거

편집장이 기사 더미를 정리한다고 하자. ② 단계는 "똑같은 기사 복사본"을 손으로 골라 버리는 일이다(URL이 같으니 기계도 한다). ⑤ 단계는 "제목과 출처는 다르지만 같은 사건을 다룬 기사들"을 알아보는 일이라 사람(=AI)의 판단이 필요하다. Horizon은 쉬운 건 기계에, 어려운 건 AI에 맡겨 비용과 정확도를 동시에 잡는다.

핵심 #3 — LLM 제공자 추상화

Horizon은 어떤 모델을 쓰든 코드가 바뀌지 않게 추상 기반 클래스 AIClient를 두고, complete(system, user) 하나로 통일했다. create_ai_client(config) 팩토리가 설정의 provider에 따라 Anthropic/OpenAI/Gemini 구현체를 만들어 준다. DeepSeek·Doubao·Ollama처럼 OpenAI 호환인 것들은 base_url만 바꿔 OpenAI 클라이언트를 재사용한다.

핵심 설계 패턴 정리

패턴Horizon에서의 구현
파이프라인(단계 분리)run()이 fetch→merge→score→filter→dedup→balance→enrich→summarize→deliver를 순차 호출. 각 단계 = 리스트 변환.
어댑터 / 통합 모델모든 스크래퍼가 BaseScraper 구현 → 단일 ContentItem 반환.
전략(Strategy) / 팩토리create_ai_client()가 provider별 AIClient 구현 선택. complete() 인터페이스 통일.
동시 수집asyncio.gather(*tasks, return_exceptions=True) — 한 출처가 실패해도 나머지는 진행.
동시성 제한분석/보강은 asyncio.Semaphore(concurrency)로 API 폭주·레이트리밋 방지.
재시도@retry(stop_after_attempt(3), wait_exponential) — LLM 일시 오류 흡수.
설정 주도출처·임계값·모델·언어·배달을 모두 config.json으로 외부화. ${ENV} 치환 지원.
우아한 실패중복제거 AI 실패 시 원본 리스트 그대로 반환(파이프라인 안 멈춤). 단계별 try/except.

5디렉토리 구조 해부

src/ 안에 스크래퍼·AI·서비스·MCP가 책임별로 나뉘어 있다.

Horizon/ ├── src/ ★ 핵심 패키지 │ ├── main.py CLI 진입점 (horizon 명령) │ ├── orchestrator.py ★★ 전 파이프라인 조율 (HorizonOrchestrator) │ ├── models.py ★ pydantic 모델 (ContentItem·Config·SourceType 등) │ ├── search.py 웹 검색 헬퍼 (보강용) │ ├── scrapers/ ★ 출처별 수집기 (모두 BaseScraper 구현) │ │ ├── base.py 추상 BaseScraper · _generate_id() │ │ ├── hackernews.py HN 상위 스토리 + 댓글 │ │ ├── rss.py RSS/Atom (feedparser) │ │ ├── reddit.py 서브레딧·유저 + 상위 댓글 │ │ ├── telegram.py 공개 채널 메시지 │ │ ├── twitter.py X/트위터 (Apify 모드) │ │ ├── twitter_playwright.py X (Playwright 모드) │ │ ├── github.py 유저 이벤트·레포 릴리스 │ │ ├── openbb.py 금융 뉴스 (OpenBB SDK) │ │ └── ossinsight.py OSS Insight 트렌딩 레포 │ ├── ai/ ★ AI 단계들 │ │ ├── client.py ★ AIClient 추상화 + create_ai_client() 팩토리 │ │ ├── analyzer.py ★ ContentAnalyzer — 0~10 점수 (1차) │ │ ├── enricher.py ContentEnricher — 웹검색 배경보강 (2차) │ │ ├── summarizer.py DailySummarizer — 최종 브리핑 생성 │ │ ├── prompts.py ★ 모든 시스템/유저 프롬프트 (점수·중복·요약) │ │ ├── tokens.py 토큰 사용량 집계 │ │ └── utils.py parse_json_response() 등 (LLM 응답 파싱) │ ├── services/ 배달 채널 │ │ ├── email.py SMTP/IMAP 뉴스레터 + 구독 관리 │ │ ├── webhook.py Feishu/Slack/Discord/커스텀 │ │ └── webhook_cli.py horizon-webhook 명령 │ ├── setup/ 대화형 설정 마법사 │ │ ├── wizard.py horizon-wizard 진입점 │ │ ├── presets.py 관심사 → 출처 프리셋 매칭 │ │ └── ai_recommend.py AI 기반 출처 추천 │ ├── storage/ │ │ └── manager.py StorageManager — 요약/구독자 저장 │ └── mcp/ ★ MCP 서버 │ ├── server.py FastMCP — hz_* 도구 13종 노출 │ ├── service.py HorizonPipelineService (단계 래핑) │ └── run_store.py 실행 이력 저장(run_id별 단계 결과) ├── data/ config.json · summaries/ (볼륨 마운트 대상) ├── docs/ GitHub Pages (Jekyll) + _posts/ ├── tests/ pytest ├── Dockerfile / docker-compose.yml ├── pyproject.toml / uv.lock uv 패키징 └── README.md / README_zh.md
읽는 순서 추천

models.pyContentItem·SourceType으로 "데이터가 어떻게 흐르나" 파악 → ② scrapers/base.py로 수집기 계약 확인 → ③ orchestrator.pyrun()으로 전체 단계 흐름 정독 → ④ ai/analyzer.py·prompts.py로 점수 로직 → ⑤ ai/client.py로 멀티 LLM 추상화. 이 다섯 파일이면 핵심 통찰의 90%를 얻는다.

6학습 포인트 (기술별)

멀티소스 + AI 파이프라인에서 무엇을 배우나.

A. 파이프라인 아키텍처 — 큰 작업을 단계로 쪼개기

Horizon의 가장 큰 교훈은 "복잡한 일을 명확한 입출력을 가진 작은 단계의 사슬로 만들라"이다. run()을 읽으면 fetch→merge→score→filter→dedup→balance→enrich→summarize→deliver가 각각 "리스트를 받아 리스트를 반환"하는 변환임이 보인다. 이렇게 하면 단계마다 단위 테스트가 쉽고, 중간 결과를 로깅·검사할 수 있으며(실제로 MCP는 각 단계를 개별 도구로 노출), 새 단계 삽입도 쉽다.

실습: "리스트 → 변환 → 리스트" 함수 3~4개(예: 수집·필터·정렬·포맷)를 만들고 한 함수에서 순차 호출하는 미니 파이프라인을 짜 보라. 각 단계를 독립적으로 테스트할 수 있는지 확인.

B. 어댑터 패턴 — 이질적 출처를 하나의 모델로

7개 출처는 API·인증·응답 형식이 전부 다르다. Horizon은 이를 BaseScraper 추상 클래스 + 단일 ContentItem으로 흡수했다. 새 출처를 추가해도 fetch(since) -> List[ContentItem]만 구현하면 파이프라인 나머지는 손댈 필요가 없다. "외부 세계의 다양성을 경계에서 정규화하고, 내부는 단일 모델로 단순하게"의 모범이다.

실습: 서로 다른 두 공개 API(예: HN의 Algolia API와 임의의 RSS)에서 데이터를 가져와 공통 dataclass 하나로 정규화하는 어댑터 2개를 만들어 보라.

C. LLM 제공자 추상화 — 모델 종속 끊기

ai/client.py"애플리케이션 코드가 특정 LLM SDK에 묶이지 않게 하는" 실전 예제다. 추상 AIClient.complete() 뒤로 Anthropic·OpenAI·Gemini 구현을 숨기고, OpenAI 호환 제공자는 base_url로 흡수한다. 이 덕분에 사용자는 설정 한 줄로 Claude ↔ GPT ↔ 로컬 Ollama를 갈아탈 수 있다. 벤더 락인을 피하는 설계를 배우기 좋다.

실습: complete(system, user) -> str 추상 메서드를 가진 베이스 클래스를 만들고, OpenAI용·더미(에코)용 두 구현을 작성한 뒤 설정값으로 어느 쪽을 쓸지 고르는 팩토리를 짜 보라.

D. 견고한 비동기 수집 — 실패 격리와 동시성 제한

한 출처가 죽어도 전체가 멈추면 안 된다. Horizon은 gather(..., return_exceptions=True)예외를 결과로 받아 출처별로 격리하고, AI 단계는 Semaphore로 동시 호출 수를 제한해 레이트리밋·풀 고갈을 막는다. 또 tenacity로 지수 백오프 재시도를 건다. "외부에 의존하는 시스템은 반드시 실패를 전제로 설계하라"는 원칙의 실물.

실습: 일부러 절반은 실패하는 비동기 작업 10개를 gather(..., return_exceptions=True)로 돌려, 실패는 로깅하고 성공만 모으는 코드를 짜 보라. 그다음 Semaphore(3)로 동시성을 3으로 제한해 보라.

E. 프롬프트 엔지니어링 — 점수화·중복판정을 데이터로 만들기

prompts.py"AI에게 시킬 판단을 어떻게 명세하나"의 교과서다. 점수 프롬프트는 9-10/7-8/5-6/3-4/0-2 각 구간을 구체 예시와 함께 정의하고, JSON으로만 응답하게 강제한다. 중복 프롬프트는 경계 사례("같은 제품 다른 사건")를 명시해 오판을 줄인다. 출력 포맷을 JSON으로 고정 + 파싱 실패 시 폴백(parse_json_response)하는 견고함도 배울 점이다.

실습: 임의의 글에 0~10 점수를 매기게 하는 프롬프트를 작성하되, 각 점수 구간의 기준과 "JSON으로만 응답" 지시를 넣어라. 그리고 응답에서 JSON만 안전하게 추출하는 파서를 붙여라.

7시스템 / 환경 요구사항

설치는 가볍지만 "API 키와 설정"이 실질적 진입 장벽이다.

항목요구사항
런타임Python 3.11 이상. uv sync(권장) 또는 pip install -e ..
AI 키(필수)최소 하나의 LLM API 키 — ANTHROPIC_API_KEY·OPENAI_API_KEY 등. 로컬 Ollama는 키 불필요.
네트워크인터넷(출처 수집·웹검색 보강·LLM 호출). 오프라인 운영은 사실상 불가.
GitHub 토큰(권장)GITHUB_TOKEN — 없으면 시간당 60요청, 있으면 5,000요청.
Twitter(선택)Apify 모드는 APIFY_TOKEN, 또는 twitter extra의 Playwright 모드.
금융 뉴스(선택)uv sync --extra openbb — OpenBB Platform SDK.
배포 옵션로컬 CLI · Docker(compose) · GitHub Actions 크론(서버 없이 무료 자동화).
비용 고려수집량 × 출처 수 × 점수/보강/요약 LLM 호출 = 토큰 비용. 임계값·동시성·max_items로 통제.
초보자 팁
가장 쉬운 시작: 마법사 + 작은 출처 + 저렴한 모델

uv run horizon-wizard로 관심사를 입력해 config.json을 자동 생성한 뒤, 처음엔 RSS 1~2개와 HN만 켜고 저렴한 모델(예: gpt-4o-mini·gemini-flash·로컬 Ollama)로 돌려 비용을 체감하라. 익숙해지면 출처·언어·배달 채널을 늘리고 GitHub Actions 크론으로 자동화하면 된다.

8직접 해볼 수 있는 실습 과제

난이도별 5단계 — 소스를 읽고 직접 손대 본다.

과제 1 난이도 ★☆☆☆☆

마법사로 첫 브리핑 만들기

uv sync 후 LLM 키 하나를 .env에 넣고 uv run horizon-wizard로 관심사를 입력해 config.json을 생성한다. RSS 한두 개와 HN만 켜고 uv run horizon --hours 48을 돌려 data/summaries/에 생성된 마크다운 브리핑을 읽어 본다.

과제 2 난이도 ★★☆☆☆

파이프라인 단계 흐름 읽기

orchestrator.pyrun()을 처음부터 끝까지 읽고, fetch→merge→score→filter→dedup→balance→enrich→summarize→deliver 각 단계가 어떤 리스트를 받아 어떤 리스트를 내보내는지를 표로 그려 본다. 임계값(ai_score_threshold)을 바꿔 통과 개수가 어떻게 변하는지 로그로 확인.

과제 3 난이도 ★★★☆☆

새 RSS 출처 추가 + 점수 프롬프트 조정

config.jsonsources.rss에 좋아하는 블로그 피드를 추가하고, prompts.pyCONTENT_ANALYSIS_SYSTEM 점수 기준을 본인 관심사(예: "프론트엔드보다 인프라를 높게")에 맞게 살짝 수정해 결과 순위 변화를 비교한다.

과제 4 난이도 ★★★★☆

나만의 미니 스크래퍼 작성

scrapers/base.pyBaseScraper를 상속해, 임의의 공개 JSON API(예: Lobsters·Dev.to)에서 글을 가져와 ContentItem으로 변환하는 새 스크래퍼를 만든다. orchestrator.fetch_all_sources()에 끼워 넣어 기존 파이프라인이 그대로 점수·요약까지 처리하는지 확인한다.

과제 5 난이도 ★★★★★

MCP 서버로 단계 도구 호출

horizon-mcp를 실행하고 MCP 클라이언트(예: Claude Desktop)에 연결해 hz_fetch_itemshz_score_itemshz_filter_itemshz_generate_summary를 단계별로 호출해 본다. mcp/service.py가 오케스트레이터 단계를 어떻게 래핑하는지, 실행 이력(run_store.py)이 어떻게 저장되는지 추적한다.

9관련 기술 심화 학습 로드맵

6주 코스 — 비동기 수집 기초부터 LLM 파이프라인 설계까지.

주차주제실습 · 참고
1주차asyncio / httpx 기초 — 코루틴·이벤트 루프·gather공식 asyncio 문서 · 여러 API 동시 수집 미니 스크립트
2주차견고한 동시성 — 실패 격리·세마포어·재시도(tenacity)orchestrator.fetch_all_sources() 정독 · return_exceptions 실험
3주차도메인 모델링 — pydantic으로 통합 모델 설계models.py 분석 · ContentItem 직접 확장
4주차어댑터 패턴 — 이질적 출처 정규화scrapers/ 비교 · 새 스크래퍼 구현(과제 4)
5주차LLM 통합 — 제공자 추상화·프롬프트·JSON 파싱ai/client.py·analyzer.py·prompts.py · 멀티 LLM 팩토리
6주차배달·자동화 — MCP·웹훅·GitHub Actions 크론mcp/server.py·services/ · 일일 자동 발행 워크플로 구성

10핵심 키워드 사전

본문·소스에 나온 용어 빠른 참조.

용어의미
HorizonOrchestrator전 파이프라인을 조율하는 핵심 클래스. run()이 9단계를 순차 실행.
ContentItem모든 출처를 통일한 pydantic 데이터 모델. 단계가 진행되며 ai_* 필드가 채워짐.
BaseScraper모든 수집기의 추상 부모. fetch(since) -> List[ContentItem] 구현 강제.
SourceType지원 출처 열거형: github·hackernews·rss·reddit·telegram·twitter·openbb·ossinsight.
교차 출처 병합같은 URL이 여러 출처에 등장하면 URL 정규화 후 하나로 합치는 코드 기반 중복제거(② 단계).
토픽 중복제거다른 링크지만 같은 사건을 다룬 기사들을 AI로 묶어 최고점만 남기는 의미론적 중복제거(⑤ 단계).
ai_scoreAI가 매긴 0~10 중요도 점수. ai_score_threshold 이상만 브리핑에 포함.
enrich (보강)임계값 통과 항목에 DDGS 웹검색으로 배경지식·맥락을 붙이는 2차 AI 패스.
balanced digest카테고리별 쿼터(limit)와 전체 상한(max_items)으로 주제 쏠림을 막는 단계(⑥).
AIClient / create_ai_clientLLM 제공자 추상화. complete(system, user)로 통일, 팩토리가 구현 선택.
OpenAI 호환OpenAI API 규격을 따르는 엔드포인트. base_url만 바꿔 DeepSeek·Ollama 등 재사용.
uvastral-sh의 초고속 파이썬 패키지 매니저. uv sync --frozen + uv.lock으로 재현 설치.
MCP (Model Context Protocol)AI 어시스턴트가 도구를 호출하는 표준. Horizon은 파이프라인 단계를 hz_* 도구로 노출.
parse_json_responseLLM 응답에서 JSON만 안전하게 추출하는 헬퍼. 여러 전략으로 파싱 실패에 대비.
GitHub Pages 발행생성된 md를 Jekyll front matter 붙여 docs/_posts/에 복사 → Pages가 일일 사이트 발행.

11참고 링크