한국어 RAG, 제대로 만들어보자: urstory-rag 오픈소스 프로젝트를 시작합니다

"RAG 구현했는데 한국어 답변 품질이 영 별로입니다." 커뮤니티에서 이런 글을 심심치 않게 봅니다. 영어 기반 튜토리얼을 따라 만들면 영어로는 잘 되는데, 한국어 문서를 넣는 순간 검색 정확도가 뚝 떨어지는 경험. RAG를 실무에 도입해본 개발자라면 한 번쯤 겪어봤을 겁니다.
문제의 원인은 대부분 비슷합니다. 한국어 토크나이징이 제대로 안 되고, 리랭킹 모델이 한국어를 잘 이해하지 못하고, 가드레일 없이 할루시네이션이 그대로 사용자에게 노출됩니다. 현업 개발자에게 들어보니 한국어 RAG에서 가장 고통스러운 부분이 리랭킹이라고 합니다. 영어 리랭커를 그대로 쓰면 "서울특별시"와 "서울대학교"를 같은 맥락으로 묶어버리는 식의 문제가 자주 생기거든요. 해결하려면 각 문제를 하나씩 파고들어야 하는데, 이걸 처음부터 끝까지 혼자 구현하기란 쉽지 않습니다.
그래서 하나 만들었습니다. 한국어에 최적화된, 프로덕션을 목표로 설계된 RAG 시스템 urstory-rag를 오픈소스로 공개합니다. 그리고 이 프로젝트를 함께 키워갈 개발자를 찾고 있습니다.
블로그 시리즈 4편에서 시작된 프로젝트
이 프로젝트의 시작점은 "소규모 기업을 위한 제대로 된 RAG 시스템"이라는 블로그 시리즈 4편입니다. RAG 실패 원인 분석부터 시작해서, 데이터 전처리, 하이브리드 검색과 가드레일, MCP 통합까지 한 편씩 쌓아올린 시리즈입니다.
글을 쓰다 보니 코드가 쌓였고, 코드가 쌓이다 보니 하나의 시스템이 되었습니다. 그런데 혼자 만든 코드는 한계가 있습니다. CI 파이프라인도 없고, 테스트 커버리지도 부족하고, 프로덕션 배포 가이드도 없습니다. 핵심 로직은 돌아가는데, "프로덕션에 올려도 되냐"고 물으면 자신 있게 대답하기 어려운 상태였습니다.
그래서 오픈소스로 전환했습니다. 핵심 파이프라인은 검증됐으니, 나머지 인프라와 안정성 작업을 함께 만들어가자는 겁니다. GitHub에 이슈 20개를 등록해뒀는데, 입문자용 퍼스트 이슈(Good First Issue)부터 경험자가 도전할 만한 아키텍처 이슈까지 골고루 준비했습니다.
아키텍처 한눈에 보기
다음 그림은 urstory-rag의 전체 시스템 구조를 보여줍니다. 백엔드, 프론트엔드, 인프라 세 축과 하이브리드 검색 파이프라인이 어떻게 연결되는지 한눈에 파악할 수 있습니다.

백엔드(FastAPI + Haystack 2.x)가 검색 파이프라인의 핵심을 담당하고, 프론트엔드(Next.js 15)가 어드민 UI를 제공하며, 인프라(Redis + Celery)가 비동기 작업을 처리합니다. 검색은 PGVector 벡터 검색과 Elasticsearch 키워드 검색을 RRF로 결합한 뒤, CrossEncoder 리랭킹과 6중 가드레일을 거쳐 최종 답변을 생성하는 구조입니다.
urstory-rag는 백엔드, 프론트엔드, 인프라 세 축으로 구성됩니다.
백엔드는 FastAPI와 Haystack 2.x 조합입니다. Haystack은 파이프라인 기반 RAG 프레임워크인데, 검색·리랭킹·생성을 각각 독립 컴포넌트로 만들고 파이프라인으로 연결하는 구조입니다. LangChain보다 구조가 명확해서 프로덕션에서 디버깅하기 편하고요.
데이터 저장소는 두 가지를 병행합니다. PostgreSQL 17 + PGVector로 벡터 검색을, Elasticsearch 8.x + Nori 토크나이저로 한국어 키워드 검색을 처리합니다. 두 결과를 RRF(Reciprocal Rank Fusion)로 합치는 하이브리드 검색 구조인데, 벡터 검색만으로는 한국어 동음이의어나 고유명사 검색이 약하거든요. Nori 기반 키워드 검색이 이걸 보완합니다.
여기서 한 가지 더 짚어야 할 게 있습니다. Nori는 Elasticsearch 플러그인이라 인덱싱 시점에 동작하지만, 쿼리 분석이나 Python 기반 BM25 검색에는 별도의 형태소 분석기가 필요합니다. urstory-rag는 이 역할을 kiwipiepy 0.18+에 맡깁니다. kiwipiepy는 Python 네이티브 한국어 형태소 분석기로, 검색 모듈 중 keyword_kiwi.py에서 쿼리를 형태소 단위로 분해하여 BM25 검색을 수행합니다. Nori가 인덱싱 쪽을, kiwipiepy가 쿼리 쪽을 담당하는 구조라고 보면 됩니다.
비동기 처리는 Redis 7과 Celery 5.4를 씁니다. 문서 인덱싱 같은 무거운 작업은 Celery 워커로 넘기고, API 응답은 빠르게 돌려줍니다.
프론트엔드는 Next.js 15 + React 19 + Tailwind CSS 4 + shadcn/ui 조합인데, 단순한 챗 인터페이스가 아니라 어드민 UI입니다. 문서 관리, RAG 응답 테스트, 임베딩/리랭킹/HyDE/가드레일 설정, RAGAS 평가 실행, 모니터링까지 웹 UI에서 전부 할 수 있습니다. 코드를 수정하지 않고도 RAG 파이프라인의 각 파라미터를 실시간으로 조정할 수 있는 구조입니다.
[사용자] → [Next.js 15 어드민 UI] ↓ [FastAPI + Haystack 2.x] ↓ [HyDE 가상 문서 생성 (선택)] ↓ ┌─────────┴──────────────┐ ↓ ↓ [PGVector] [Elasticsearch/Nori + kiwipiepy/BM25] (벡터 검색) (키워드 검색) └─────────┬──────────────┘ ↓ [RRF 결합] ↓ [Korean CrossEncoder 리랭킹] ↓ [6중 가드레일 검증] ↓ [LLM 답변 생성] ↓ [MCP 서버 (Claude 통합)]
한국어에 특화된 세 가지 기술
urstory-rag가 일반적인 RAG 튜토리얼 코드와 다른 지점은 한국어 처리입니다.
한국어 CrossEncoder 리랭킹
검색 결과의 품질을 결정하는 건 리랭킹 단계입니다. 벡터 검색으로 30~50개 후보 문서를 뽑고, 리랭커가 이걸 5~10개로 정제하는데, 여기서 영어 모델을 쓰면 한국어 문맥을 제대로 판단하지 못합니다. 한국어 RAG 커뮤니티에서도 "리랭커만 한국어 모델로 바꿔도 체감 품질이 확 올라간다"는 이야기가 자주 나오죠.
urstory-rag는 dragonkue/bge-reranker-v2-m3-ko 모델을 씁니다. 한국어 AutoRAG 벤치마크에서 F1 스코어 0.9123으로 전체 1위를 기록한 모델인데, 단순히 갖다 쓴 게 아니라 calibrated 모드를 추가했습니다.
# 캘리브레이션 모드: 시그모이드 변환 + 순위 신호 결합 # final = α × sigmoid(logit) + (1-α) × rank_score # 기본 α=0.7로 모델 점수에 더 높은 가중치 def _calibrated_rerank(self, query, documents, top_k, alpha=0.7): pairs = [(query, doc.content) for doc in documents] logits = self.model.predict(pairs) sigmoid_scores = 1 / (1 + np.exp(-logits)) rank_scores = np.array([(len(logits) - i) / len(logits) for i in range(len(logits))]) final_scores = alpha * sigmoid_scores + (1 - alpha) * rank_scores # ...
시그모이드로 변환한 모델 점수와 순위 기반 점수를 결합하는 방식입니다. 모델이 "이 문서가 얼마나 관련 있는지"를 판단한 절대 점수와, 후보군 내 상대 순위를 함께 반영하는 셈이죠. 블로그 시리즈 3편에서 설명한 것처럼, 리랭킹만 잘 해도 검색 정확도가 20~35% 올라갑니다.
HyDE(Hypothetical Document Embeddings)
"한국에서 가장 큰 도서관은?"이라는 질문을 벡터 검색에 그대로 넣으면, "한국", "도서관", "크다" 같은 키워드가 포함된 문서가 나옵니다. 그런데 정답 문서에는 "국립중앙도서관"이라는 고유명사가 들어있을 수 있고, 질문과 답변의 임베딩 벡터가 충분히 가깝지 않을 수 있습니다.
HyDE는 이 문제를 우회합니다. 사용자 질문을 받으면 LLM이 먼저 "가상의 답변 문서"를 생성하고, 그 가상 문서의 임베딩으로 검색하는 거죠. 질문의 임베딩보다 답변의 임베딩이 실제 정답 문서와 더 가까울 확률이 높기 때문입니다.
urstory-rag의 HyDE 구현은 한국어에 맞게 세 가지 적용 모드를 둡니다. all은 모든 쿼리에 적용, long_query는 50자 초과 쿼리에만 적용, complex는 복합 질문에만 적용합니다. complex 모드가 한국어 처리의 핵심인데, 한국어 접속사를 감지해서 복합 질문인지 판단합니다.
# backend/app/services/hyde/generator.py # complex 모드: 한국어 접속사로 복합 질문 판별 KOREAN_CONJUNCTIONS = ["그리고", "하지만", "또는", "그래서", "그러나", "그런데", "및", "또한", "혹은", "그러면"] def _is_complex_query(self, query: str) -> bool: """한국어 접속사가 포함되어 있으면 복합 질문으로 판단합니다.""" for conjunction in KOREAN_CONJUNCTIONS: if conjunction in query: return True return len(query) > 100 # 접속사 없어도 100자 초과면 복합 질문
"한국어 RAG의 성능을 높이는 방법 그리고 비용을 줄이는 전략"처럼 접속사가 포함된 질문은 단일 가상 문서로 양쪽을 커버하기 어렵습니다. complex 모드는 이런 질문을 감지해서 HyDE를 선택적으로 적용하는 건데, 모든 쿼리에 HyDE를 걸면 LLM 호출 비용과 지연이 늘어나니까 이런 조건부 적용이 실용적입니다.
6중 가드레일
프로덕션 RAG에서 가장 무서운 건 할루시네이션입니다. 사용자가 "우리 회사 보안 정책이 뭐야?"라고 물었는데, RAG가 없는 내용을 지어내서 답변하면 큰 문제가 됩니다.
다음 그림은 urstory-rag의 6중 가드레일이 파이프라인에서 어떤 순서로 동작하는지 보여줍니다.

검색 관문(Retrieval Gate)에서 시작하여 충실성, 환각 탐지, 숫자 검증, 주입 방지, PII 보호까지 6단계를 순차적으로 통과합니다. 특히 1~3단계는 DoorDash 패턴을 적용한 핵심 검증 구간으로, 할루시네이션을 90% 줄이는 데 기여합니다.
urstory-rag는 6개의 가드레일 모듈을 파이프라인에 배치합니다. 각 모듈이 독립적으로 동작하고, 역할이 구분되어 있습니다.
가장 먼저 동작하는 건 검색 게이트(retrieval_gate)입니다. 검색된 문서가 질문과 관련 있는지 판단하는 1차 관문으로, 관련성이 낮으면 아예 답변 생성을 차단합니다. "엉뚱한 문서로 답변하는" 상황을 원천 차단하는 거죠.
그 다음 충실성 검증(faithfulness)이 LLM 답변이 검색 문서에 근거하는지 확인하고, 할루시네이션 탐지(hallucination)는 문서에 없는 내용을 LLM이 지어냈는지에 초점을 맞춥니다. 비슷해 보이지만 검증 방향이 다릅니다.
숫자 검증(numeric_verifier)은 좀 독특한데, 답변의 수치가 원본 문서 수치와 일치하는지 별도로 검증합니다. "매출 1억"을 "매출 10억"으로 잘못 생성하는 실수를 잡아내는 모듈입니다. 나머지 둘은 프롬프트 인젝션 방지(injection)와 개인식별정보 보호(PII)로, 각각 악의적 프롬프트 차단과 주민등록번호·전화번호 같은 민감 정보 필터링을 담당합니다.
블로그 시리즈 3편에서 설명한 DoorDash 패턴의 2단계 검증을 적용했습니다. 시맨틱 유사도로 1차 필터링하고 LLM으로 2차 심층 검증하는 구조인데, DoorDash 엔지니어링 팀은 이 방식으로 할루시네이션을 90% 줄였다고 보고했습니다.
이 가드레일은 코드 수정 없이 어드민 UI에서 실시간으로 켜고 끌 수 있습니다. 개발 중에는 느슨하게, 프로덕션에서는 전부 켜는 식으로 운영하면 됩니다.
MCP 통합으로 Claude에서 바로 쓰기
RAG 파이프라인을 잘 만들어 놓아도, 매번 API를 직접 호출해야 하면 쓰기 번거롭습니다. urstory-rag는 FastMCP 3.0 기반 MCP 서버를 내장하고 있어서, Claude Desktop이나 Claude Code에서 RAG를 도구로 직접 호출할 수 있습니다.
MCP 서버는 세 계층으로 구성됩니다. Tools 계층에서 문서 검색, 유사 문서 탐색, 컬렉션 조회 같은 실제 작업을 수행하고, Resources 계층에서 인덱싱된 문서 목록이나 시스템 설정 같은 읽기 전용 메타데이터를 제공합니다. Prompts 계층은 "이 문서 컬렉션을 분석해줘" 같은 요청에 쓸 수 있는 분석 템플릿을 담고 있습니다.
로컬에서는 stdio, 원격에서는 SSE 방식으로 통신합니다. 개발 중에는 stdio로 빠르게 테스트하고, 배포 후에는 SSE로 전환하면 됩니다. MCP 생태계가 빠르게 확장되고 있으니, Claude 외에도 MCP 호환 도구들과 바로 연동할 수 있다는 점도 이점입니다.
90개 QA 테스트셋으로 검증한 성능
"잘 돌아갑니다"라는 말은 수치 없이는 의미가 없습니다. urstory-rag는 90개 QA 테스트셋으로 성능을 측정했습니다.
LLM Judge 평균 점수 94.4점(100점 만점), 전체 응답의 97%가 GOOD 등급, 검색 리콜 100%. 검색 리콜 100%라는 건 테스트셋의 모든 질문에 대해 정답 문서를 빠짐없이 찾아냈다는 뜻입니다.
다만 이 수치의 맥락을 짚어야 합니다. 90개 QA 테스트셋은 프로젝트 자체 문서 대상이고, 동시 사용자 부하 테스트나 응답 지연 벤치마크는 아직 안 했습니다. "프로덕션에서 검증된 수치"가 아니라 "로컬에서 RAG 파이프라인 품질을 측정한 결과"로 이해해야 정확합니다. 프로덕션 성능 검증은 앞으로 해야 할 숙제입니다.
평가에는 RAGAS 프레임워크를 쓰고, Langfuse로 각 쿼리의 처리 과정을 추적합니다. 어드민 UI의 Evaluation 탭에서 RAGAS 평가를 직접 실행하고 결과를 비교할 수 있어서, 파라미터를 바꿔가며 AB 테스트하기 편합니다.
물론 90개 테스트셋이 모든 상황을 대변하지는 않습니다. 특정 도메인의 문서를 넣으면 성능이 달라질 수 있고, 테스트셋 자체를 늘려야 할 수도 있습니다. 그래서 이슈 중에도 평가 관련 항목이 있습니다.
함께 만들어갈 이슈 20개
GitHub에 20개 이슈를 등록해뒀습니다. 난이도별로 나눠서, 자기 수준에 맞는 이슈를 골라 참여할 수 있습니다.
퍼스트 이슈(Good First Issue) 7개
오픈소스 기여가 처음이어도 도전할 수 있는 이슈입니다.
API 문서 강화(#14)는 부족한 응답 예제와 에러 코드를 추가하는 작업입니다. 코드 로직을 깊이 이해하지 않아도 FastAPI의 OpenAPI 스키마를 다뤄본 경험이 있으면 충분하고요.
프론트엔드 접근성 개선(#20)은 어드민 UI의 웹 접근성(a11y)을 보강하는 작업입니다. ARIA 레이블 추가, 키보드 네비게이션 보강 같은 작업이라 프론트엔드에 익숙한 분이라면 바로 시작할 수 있습니다.
의존성 버전 관리 및 보안 스캔 자동화(#19)는 Dependabot이나 Renovate 설정 경험이 있으면 빠르게 처리할 수 있습니다.
CORS 화이트리스트 및 보안 헤더 설정(#2), Rate Limiting 구현(#3)은 보안 관련 이슈인데, FastAPI 미들웨어를 다뤄본 경험이 있으면 무난합니다. priority가 critical이라 해결 즉시 기여도가 눈에 보이는 작업입니다.
Graceful Shutdown(#9)과 커넥션 풀링 최적화(#11)는 운영 안정성 관련 이슈로, 백엔드 경험이 좀 있는 분에게 적합합니다.
도전적인 이슈들
JWT 인증/인가 시스템(#1)은 현재 인증 없이 동작하는 API에 JWT 기반 보안을 입히는 작업입니다. FastAPI의 보안 의존성과 SQLAlchemy 사용자 모델을 함께 설계해야 해서 규모가 있습니다.
GitHub Actions CI 파이프라인(#4)은 현재 없는 CI를 처음부터 구성하는 작업입니다. 린트, 타입체크, 테스트, Docker 빌드까지 전체 파이프라인을 설계할 수 있습니다.
Redis 응답 캐싱(#10), Circuit Breaker(#12), 구조화된 로깅(#6) 같은 이슈는 프로덕션 운영 경험이 있는 개발자에게 맞습니다. 각각 독립적이라 다른 이슈와 충돌 없이 진행할 수 있고요.
전체 이슈 목록은 GitHub Issues 페이지에서 확인할 수 있습니다.
에이전틱 코딩으로 참여하기
기여 방식에 정해진 규칙은 없습니다. 직접 코드를 작성해도 좋고, AI 도구를 활용한 에이전틱 코딩(Agentic Coding)으로 참여해도 좋습니다.
요즘 Claude Code, Cursor, GitHub Copilot Workspace 같은 도구로 코드를 작성하는 개발자가 빠르게 늘고 있습니다. 이슈를 읽고, AI 에이전트에게 컨텍스트를 전달하고, 생성된 코드를 검토해서 PR을 올리는 흐름이죠. 코드를 한 줄도 직접 타이핑하지 않더라도, 이슈를 분석하고 AI 출력을 검증하고 테스트하는 과정 자체가 의미 있는 기여입니다.
예를 들어 API 문서 강화(#14) 이슈를 에이전틱 코딩으로 처리한다면 이런 흐름이 됩니다. 프로젝트 디렉토리에서 Claude Code를 열고, 이슈 내용을 자연어로 전달합니다.
# 프로젝트 디렉토리에서 Claude Code 실행 cd urstory-rag claude # Claude Code 안에서 이슈 기반 작업 지시 > 이슈 #14를 확인하고, backend/app/main.py의 API 엔드포인트에 > OpenAPI 응답 예제와 에러 코드를 추가해줘. > 기존 라우터 파일들을 먼저 분석한 뒤 작업해줘.
Claude Code가 기존 코드를 분석하고 수정안을 만들면, 생성된 코드를 리뷰하고 make dev-backend로 로컬에서 확인한 뒤 PR을 올리면 됩니다.
이 프로젝트 자체가 에이전틱 코딩 연습에도 괜찮은 대상입니다. 이슈가 명확하게 정의되어 있고, 범위가 적절하고, Makefile로 로컬 환경 설정이 간단하거든요. AI 에이전트에게 넘길 컨텍스트가 깔끔하면 결과물 품질도 올라갑니다.
개발 환경 설정은 5분이면 충분합니다
기여를 시작하려면 먼저 로컬 환경을 세팅해야 하는데, Makefile에 주요 명령이 정리되어 있어서 복잡하지 않습니다.
# 리포지토리 클론 git clone https://github.com/urstory/urstory-rag.git cd urstory-rag # 전체 초기 설정 (Docker 필요) make setup # 인프라만 띄우기 (PostgreSQL + Elasticsearch + Redis) make infra-up # 백엔드 개발 서버 (포트 8000) make dev-backend # 프론트엔드 개발 서버 (포트 3500) make dev-frontend # 테스트 실행 make test
Docker가 설치되어 있으면 make setup 하나로 PostgreSQL, Elasticsearch, Redis가 전부 뜹니다. 백엔드와 프론트엔드를 각각 따로 띄울 수 있으니, 자기가 작업할 영역만 실행하면 됩니다.
PR을 올리기 전에 make test로 기존 테스트가 깨지지 않는지 확인해주세요. 아직 CI가 없어서(#4 이슈) 수동 확인이 필요합니다. CI 구축 자체가 기여 대상이기도 합니다.
블로그 시리즈로 배경 지식 쌓기
프로젝트의 설계 철학과 기술적 배경이 궁금하다면 블로그 시리즈 4편을 읽어보시길 권합니다.
1편: 왜 당신의 RAG는 실패하는가에서는 RAG 실패의 5가지 근본 원인과 해결 방향을 다룹니다. 하이브리드 검색으로 15~30%, 리랭킹으로 20~35% 추가 정확도 향상이 가능하다는 수치와 함께 왜 이런 구조를 선택했는지 설명하고 있습니다.
2편: 데이터가 전부입니다는 데이터 전처리 전략 편입니다. 4가지 청킹 전략 비교와, Anthropic이 제안한 Contextual Retrieval로 검색 실패율을 5.7%에서 1.9%로 줄인 사례를 다룹니다.
3편: 검색, 평가, 그리고 신뢰할 수 있는 시스템에서는 하이브리드 검색과 가드레일 구현을 구체적으로 다룹니다. DoorDash 패턴의 2단계 가드레일로 할루시네이션을 90% 줄인 방법과 RAGAS 자동 평가 도입 과정이 나옵니다.
4편: MCP로 완성하기는 FastMCP 3.0 기반 Claude 통합 편입니다. MCP 세 계층 구조와 로컬/원격 통신 방식을 설명합니다.
코드만 봐서는 "왜 이렇게 구현했는지"가 잘 안 보이는데, 시리즈를 함께 읽으면 각 모듈의 설계 의도가 잡힙니다.
마무리: 같이 만들면 더 좋은 코드가 됩니다
한국어 RAG는 영어 RAG와 다른 문제를 풀어야 합니다. 형태소 분석, 교착어 특성에 맞는 토크나이징, 한국어에 최적화된 리랭킹 모델 선택. 27년 동안 여러 프로젝트를 거치면서 느낀 건, 이런 문제는 각자 해결하기보다 커뮤니티가 함께 풀 때 훨씬 빠르다는 겁니다.
urstory-rag는 이러한 문제에 대한 하나의 해답입니다. 90개 테스트셋에서 94.4점, 97% GOOD 응답. 핵심 파이프라인은 이미 동작합니다. 다만 프로덕션에 올리려면 인증, CI/CD, 모니터링, 캐싱 같은 인프라가 필요하고, 이건 혼자 하기엔 넓은 영역입니다.
GitHub에 등록된 20개 이슈가 로드맵입니다. 퍼스트 이슈 7개, help wanted 4개가 열려 있습니다. 직접 코드를 작성하든, 에이전틱 코딩으로 AI와 함께 작업하든, 방식은 상관없습니다. 관심 있는 이슈에 코멘트를 남기는 것부터 시작해보세요.
프로젝트 리포지토리: https://github.com/urstory/urstory-rag
이슈 목록: https://github.com/urstory/urstory-rag/issues
참고 자료






댓글
댓글을 작성하려면 이 필요합니다.