All projects

AI 경제·시사 토론 EfraX

LIVE

A daily pipeline that curates market-moving headlines with an LLM, maps their ripple effects onto a news-industry-company knowledge graph, and opens each one up as a guided debate.

시장을 움직이는 뉴스를 LLM으로 선별하고, 그 파급을 뉴스-산업-기업 지식그래프로 매핑해 단계별 토론까지 이어가는 일간 파이프라인

Role · Full-stack architect — curation pipeline, news→industry→company graph, async backend, dual-LLM routing, billing, deployment.

RAGAI AgentGraphDBLLM

Key Metrics

25+
RSS Sources Curated
5
Socratic Debate Stages
1080×1350
Instagram Card Render

Why it matters

Shipped and live at efrax.org: 5-service Docker stack, dual-LLM routing with fallback (Claude / GPT / Gemini → OpenAI direct), Korean PayApp billing, Google OAuth, and every external failure mode handled gracefully — paywalls, rate limits, slow graph jobs.

efrax.org에 실제 배포·운영 중. 5-service Docker 스택, dual-LLM 라우팅(Claude/GPT/Gemini → OpenAI fallback), 한국 결제(PayApp), Google OAuth. 외부 장애(페이월·레이트리밋·느린 그래프 연산) 모두 graceful fallback으로 처리.

Problem

News platforms drown readers in volume without showing why a story matters or what it touches. EfraX selects the day's market-moving stories (Reuters/Bloomberg/FT → Korean paywall fallback), traces ripple effects onto a news→industry→company knowledge graph, and turns each story into a 5-stage Socratic dialogue. Engineering challenge: RSS ingest every 30 min, LLM curation every 2h with diversity constraints, async graph submission via ARQ, dual-LLM with cost tracking and prompt caching — all without the user ever seeing a failure.

뉴스 플랫폼은 '왜 중요한지, 무엇에 영향을 주는지'를 보여주지 못한 채 양으로 압도합니다. EfraX는 30분 RSS 수집 → 2시간 주기 LLM 큐레이션(다양성 제약) → 뉴스→산업→기업 그래프 파급효과 분석 → 5단계 소크라테스 토론으로 이어지는 파이프라인을 구축했습니다.

What I built

Key Decisions (trade-offs)

1

Global agenda, Korean fallback bodies

Reuters/Bloomberg/FT set the agenda but paywall bodies. Rather than drop a market-moving story, the pipeline re-locates the same topic in Korean media — trading source purity for coverage completeness. Non-news URLs (/quote/, /profile/) are pattern-blocked so they never contaminate the set.

글로벌 매체가 의제를 정하지만 본문은 페이월. 같은 주제를 한국 매체에서 다시 찾아냅니다. '소스 순수성'보다 '커버리지 완전성'을 선택했습니다.

2

Diversity enforced at curation, not after

Left alone, an LLM floods the feed with one hot event. Five mutually distinct topics is a hard output constraint — breadth is guaranteed by the contract, not patched by post-filtering.

'서로 다른 5개 주제'를 출력 단계의 강제 제약으로 두어, 사후 필터링이 아니라 계약(contract)으로 다양성을 보장했습니다.

3

Don't trust the LLM to honour quotas — enforce them in code

The curation prompt specifies a per-category quota, and the model violated it often enough that the daily digest skewed toward whichever category had the most coverage. Prompt tuning moved the failure rate but never removed it. I stopped treating the model output as authoritative: `_enforce_quota` re-balances the selection in code after the call, filling macro slots first and tagging any leftover as quota-filled. The prompt still asks, but the guarantee lives in Python.

큐레이션 프롬프트에 카테고리별 쿼터를 명시했으나 모델이 자주 어겼고, 그 결과 다이제스트가 기사량이 많은 카테고리로 쏠렸습니다. 프롬프트를 다듬으면 위반 빈도는 줄지만 사라지지는 않았습니다. 모델 출력을 최종 결과로 신뢰하지 않기로 하고, 호출 이후 코드(`_enforce_quota`)에서 선정을 재보정합니다. 프롬프트는 여전히 요청하되 보장은 파이썬이 합니다.

4

Reject the prompt, or warn and continue

Prompt-injection filtering has a false-positive cost that is invisible in tests: a user asking a pointed question about a policy article gets blocked and assumes the product is broken. Hard-blocking everything suspicious protects the model but breaks the core loop of the service. I split the response by confidence — obvious abuse is rejected outright, ambiguous input is passed through with a warning injected into the system prompt. Safety that silently kills legitimate debate is not safety for a debate product.

프롬프트 인젝션 필터링에는 테스트에서 드러나지 않는 오탐 비용이 있습니다. 정책 기사에 날 선 질문을 던진 사용자가 차단되면 서비스가 고장 난 것으로 받아들입니다. 의심스러운 입력을 전부 하드 차단하면 모델은 지켜지지만 서비스의 핵심 흐름이 끊깁니다. 확신도로 응답을 나눴습니다. 명백한 오남용만 거부하고, 애매한 입력은 시스템 프롬프트에 경고를 주입한 채 통과시킵니다. 정상 토론까지 막는 안전장치는 토론 제품에서 안전이 아닙니다.

5

Bundle the font instead of downloading it in CI

The Instagram card renderer needs a Korean font inside headless Chromium. Pulling Noto-CJK from a CDN at build time (61MB) failed often enough to make publishing unreliable, and switching CDNs only moved the failure. I committed Pretendard to the repository. It costs repo size permanently and removes an external dependency from every publish — the right trade when the alternative is a pipeline that fails on someone else's uptime.

인스타 카드 렌더러는 헤드리스 Chromium 안에서 한글 폰트를 필요로 합니다. 빌드 시점에 CDN 에서 Noto-CJK(61MB)를 받는 방식은 실패가 잦아 발행 자체를 불안정하게 만들었고, CDN 을 바꿔도 실패 지점만 옮겨졌습니다. Pretendard 를 저장소에 직접 넣었습니다. 저장소 용량을 영구히 쓰는 대신 매 발행에서 외부 의존을 제거합니다. 대안이 남의 가동률에 좌우되는 파이프라인이라면 이쪽이 맞는 교환입니다.

6

Async graph submission via ARQ

Graph ripple-effect analysis is too heavy to run inline — pushed to ARQ job queue. Discussion UX responds immediately; the graph fills in behind it. JWT bridging (string sub ↔ integer sub) stitches two subsystems under one OAuth login.

그래프 분석을 ARQ 잡 큐로 분리해 토론 UX는 즉시 응답하고 그래프는 뒤에서 채워집니다. JWT 브리징으로 두 서브시스템을 OAuth 1번 로그인으로 연결했습니다.

Stack

Python 3.12FastAPIasyncpgARQPostgreSQL 16Redis 7React 19ViteOpenRouter (Claude/GPT/Gemini)PayApp / LemonSqueezyPlaywrightInstagram Graph APIFly.io / Cloudflare PagesDocker Compose