RSS 수집 → LLM 요약 → 관심사 점수 필터 → 일일 다이제스트 이메일을 자동화하는 비동기 배치 파이프라인. Collects RSS, summarizes with an LLM, scores against your interests, and emails a daily digest.
백엔드 포트폴리오 프로젝트입니다. 화려한 기능이 아니라 비동기 파이프라인 설계 · 실패 처리 · 비용 최적화의 깊이를 증명하는 것이 목적입니다.
설계 단일 진실 공급원(SSOT): docs/DESIGN.md · 기획: docs/PROJECT_PLAN.md
| 기둥 | 무엇을 / 어디서 |
|---|---|
| 비동기 파이프라인 설계 | API(FastAPI) / 워커(Arq) / 스케줄러(APScheduler)를 분리. 전 구간 async. docker compose up 한 줄로 재현 |
| 실패 처리 | 재시도 백오프(LLM=full jitter / RSS·SES=결정론적, 의도적 구분), 실패 격리, dead-letter retry-then-park, 구조화 JSON 로깅 |
| 비용 최적화 | LLM 추상화로 공급자 교체, content_hash 캐싱(재요약 0회), HTML 제거(토큰 절감), 읽기시간 직접 계산(LLM 미사용), 구조화 출력, 로컬 모델($0) |
| idempotency | 파이프라인 전체 재실행 안전 — content_hash UNIQUE(재요약 0), (user_id, digest_date) UNIQUE(중복 발송 0) |
flowchart TD
SCH["⏰ APScheduler<br/>(매일 정해진 시각)"] -->|run_pipeline enqueue| REDIS[("Redis<br/>큐 + 캐시")]
REDIS --> WORKER["⚙️ Arq Worker (async, 멱등)"]
WORKER --> COLLECT["collect_feeds<br/>RSS 수집 · content_hash 중복차단"]
COLLECT --> SUMM["summarize_pending<br/>LLM 요약 · 캐싱"]
SUMM --> EMBED["embed_pending<br/>임베딩 · pgvector"]
EMBED --> DIGEST["build_and_send_digest<br/>점수(Jaccard·코사인) → 이메일"]
COLLECT --> PG[("PostgreSQL<br/>+ pgvector")]
SUMM --> PG
EMBED --> PG
DIGEST --> PG
API["🌐 FastAPI<br/>/interests · /dead-letters · /docs"] --> PG
LLMP["LLM provider<br/>fake · anthropic · ollama"] -.요약.-> SUMM
EMBP["임베딩 provider<br/>fake · ollama(bge-m3)"] -.임베딩.-> EMBED
MAIL["EmailSender<br/>fake · (SES)"] -.발송.-> DIGEST
- 분리 원칙: API는 즉시 응답, 무거운 일(수집·요약·임베딩·발송)은 워커/스케줄러로 위임.
- 데이터 흐름: 수집 → 요약 → (임베딩) → 점수(추천) → 전달 전 과정 자동화.
- 점수 전략:
SCORING_STRATEGY로 Jaccard(토큰 겹침) ↔ 임베딩 코사인(의미) 교체. - provider 추상화: LLM·임베딩·이메일 모두 인터페이스 뒤에 → 설정으로 교체.
cp .env.example .env # 필요 시 값 수정 (LLM_PROVIDER 등)
docker compose up -d --build # postgres + redis + app + worker(+scheduler) 일괄 기동- worker 컨테이너가 기동 시 마이그레이션(alembic) + seed를 자동 수행하고, 스케줄러(별도 프로세스)와 Arq 워커를 띄웁니다.
- API: http://localhost:8000/docs (Swagger UI = 관리자 콘솔), 헬스:
/health
| 값 | 비용 | 준비 |
|---|---|---|
fake |
$0 | 없음 (테스트·개발 기본) |
ollama |
$0 (로컬) | ollama pull exaone3.5:2.4b — docs/OLLAMA.md 참고 |
anthropic |
유료 | ANTHROPIC_API_KEY 설정 (Claude Haiku) |
한국어 콘텐츠라 로컬은 한국어 특화 EXAONE 3.5를 기본 모델로 둡니다. 컨테이너에서 host의 Ollama를 쓰도록
OLLAMA_BASE_URL=http://host.docker.internal:11434로 연결됩니다.
- LLM 추상화 — SDK는
app/llm/providers/에서만 import. 파이프라인은LLMProvider프로토콜만 안다. 그래서 Anthropic↔Ollama 전환이 코드 0줄 수정. 테스트는 항상FakeProvider주입. - idempotency를 DB 제약으로 —
contents.content_hashUNIQUE = 요약 캐시 키,digests(user_id, digest_date)UNIQUE = 발송 1회.INSERT ... ON CONFLICT로 재실행 안전. - 점수 함수 = Jaccard —
score>0중 top-N(기본 5), 동점은published_at최신순. 관심사·키워드는 동일 정규화 함수(소문자+별칭)로 같은 공간에. (한계·근거는 DESIGN §5) - 실패 처리 — LLM 재시도엔 full jitter(thundering herd 방지), RSS·SES엔 결정론적 백오프(의도적 구분). 한 건 실패는 격리하고 계속, N회 초과 시 dead-letter park.
- 관측 — Flower 대신 구조화 JSON 로그 + 작업 결과/dead-letter 테이블.
-
테스트 비용 $0 —
pytest40개,FakeProvider주입으로 실 LLM 호출 0회. -
로컬 추론 $0 — Ollama(EXAONE 3.5 2.4b) 기사 1건 요약: prompt ~500–3700 tok / 출력 ~60–150 tok / 비용 $0(로컬). JSON 로그 예:
{"msg":"llm_usage","provider":"ollama","model":"exaone3.5:2.4b","prompt_tokens":2460,"eval_tokens":116,"cost_usd":0} -
content_hash캐싱 전/후 — 같은 콘텐츠를 파이프라인 재실행할 때, 캐시가 재요약을 0으로 만든다. EXAONE 3.5(로컬) 3건 기준 실측:LLM 호출 토큰 시간 캐시 미적용(재요약) 3 4,229 79.7s 캐시 적용(재실행) 0 0 ~0s 매 스케줄 실행마다 이미 요약한 글을 다시 부르지 않는다 — 소스·콘텐츠가 쌓일수록 절감폭이 커진다.
-
중복 발송 0회 — 다이제스트 잡 재실행 시
already_sent로 재발송 없음. -
Anthropic 경로(Claude Haiku) 단가 $1/$5 per 1M tok — 크레딧 충전 시 동일 코드로 측정 가능.
-
점수 함수 진화 근거 (Jaccard → 임베딩, V3 방향) — 토큰이 안 겹쳐 Jaccard가 0으로 놓치는 의미 동의어를, 임베딩 코사인(bge-m3, 로컬)이 잡아낸다. 실측:
관심사 ↔ 콘텐츠 Jaccard 코사인 AI ↔ 인공지능 0.000 0.828 주식 ↔ 증시 0.000 0.757 AI ↔ 떡볶이 (무관) 0.000 0.452 알려진 표기 변형(
도커↔Docker)은 정규화(별칭맵)가, 의미 동의어는 임베딩이 잡는다 → 상보적. DESIGN §5의 "Jaccard 한계 → tf-idf/임베딩(V3)"을 실측으로 뒷받침.
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /health |
헬스 체크 |
| GET/POST/DELETE | /interests |
관심 키워드 등록·조회·삭제 (정규화 적용) |
| GET | /dead-letters |
실패한 요약 작업 관측 |
| GET | /docs |
Swagger UI (관리자 콘솔) |
docker compose up -d --build # 전체 스택
pytest # 전체 테스트 (실 LLM 호출 0회)
pytest -x -q tests/test_pipeline.py # 파이프라인 빠른 검증
alembic upgrade head # 마이그레이션
ruff check . && ruff format . # 린트 + 포맷Python 3.12 · FastAPI · Arq · APScheduler · PostgreSQL · Redis · SQLAlchemy(async)/asyncpg · Alembic · httpx · feedparser · Docker Compose · pytest · ruff
Celery는 검토 후 의도적으로 기각(async 일관성). 근거: DESIGN §2.
구현 완료: RSS 다중 소스(어피티·토스·네이버 D2) + content_hash 중복 제거 / LLM 요약(캐싱) / 관심사 등록 + 점수 전략 스위치(Jaccard ↔ 임베딩 코사인) / 일일 다이제스트(idempotent) / 실패 처리(재시도·dead-letter park) / 스케줄 자동화 / 구조화 JSON 로깅 / 임베딩 저장(pgvector) + embed_pending / CI(단위+통합) + 통합 테스트.
| 버전 | 계획 |
|---|---|
| V3 (일부 구현) | 임베딩 provider + pgvector + 점수 전략 스위치 완료 → 유사 콘텐츠 추천·재랭킹·threshold 튜닝은 확장 여지 |
| V2 | 가입/로그인/JWT, 멀티유저, Slack/Discord 연동, SES 프로덕션 |
| V4 | user_events 기반 개인화 추천 |
상세: docs/DESIGN.md §10
- docs/DESIGN.md — 기술 결정·메커니즘·성공 기준 (SSOT)
- docs/PORTFOLIO.md — 포트폴리오 정리본 · docs/ONE_PAGER.md — 1페이지 요약
- docs/INTERVIEW.md — 예상 면접 질문 & 답변 포인트
- docs/samples/ — 산출물 예시(렌더된 다이제스트 이메일)
- docs/PROJECT_PLAN.md — 기획 · docs/OLLAMA.md — 로컬 LLM(Ollama) 설정
MIT — LICENSE