도서관 맥락 기반 AI 도서 큐레이션 시스템
멀티턴 대화로 사용자의 독서 의도를 파악하고, 하이브리드 검색 → 리랭킹 → 실시간 대출 가능 여부 확인을 거쳐 "지금 이 도서관에서 빌릴 수 있는 책"을 추천합니다.
4인 팀 프로젝트(6주)의 개인 포트폴리오 스냅샷입니다. 본 문서는 제가 담당한 평가 골드셋 구축과 리랭커 모델 선정·운영 전환을 중심으로 정리했습니다.
flowchart TD
A["멀티턴 대화<br/>슬롯 추출 · CLOVA HCX"] --> B["RAG 쿼리 생성<br/>keyword · semantic · filters · constraints · score_boost"]
B --> C["Hybrid 검색 (Elasticsearch)<br/>BM25 0.3 + Dense 0.7 · KURE 임베딩"]
C --> D["★ BGE Cross-Encoder 리랭킹<br/>Score Fusion (retrieval 0.2 + rerank 0.8)"]
D --> E["대출 가능 여부 조회<br/>정보나루 API"]
E --> F["최종 추천 + 추천 사유"]
style D fill:#ffe0b2,stroke:#e65100,stroke-width:2px
style C fill:#e3f2fd,stroke:#1565c0
기술 스택: FastAPI · React(Vite) · Elasticsearch · PostgreSQL · CLOVA Studio(HCX) · sentence-transformers(PyTorch) · Docker / k8s
"검색·리랭킹의 성능을 측정하려면 '이 질의에 어떤 도서가 실제로 적합한가'라는 정답이 있어야 한다"는 문제를 해결.
| 방식 | 장점 | 한계 |
|---|---|---|
| 사람만 | 신뢰도 높음 | 80권 × 42시나리오 = 3,360건, 비용·시간 과다 |
| LLM만 | 빠르고 저렴 | 일관성·정확도 불확실 |
| LLM × 3 + 다수결 ✅ | 단일 LLM 오류 완화, 효율·신뢰 균형 | 불일치 케이스만 사람 검수로 보완 |
flowchart LR
A["① 시나리오 작성<br/>다양한 RAG 쿼리 설계"] --> B["② 검색기로 후보 추출<br/>Top-K 후보군"]
B --> C["③ 관련도 라벨링<br/>LLM-judge × 3회 (0~3점)"]
C --> D["④ 다수결 합의"]
D --> E["⑤ 불일치 케이스만<br/>사람(사서) 검수"]
E --> F["최종 GoldSet"]
- 리랭킹 평가셋: 42개 시나리오 → 품질 검수 후 최종 39개, 시나리오당 후보 20권(관련 5 · 비관련 15),
final_grade ≥ 2= 관련 - 평가 지표 유틸 직접 구현: Hit@K · Recall@K · MRR@K · NDCG@K · HardNegative@K (
metrics.py)
관련 코드: 01a_goldset_retrieval · 01b_goldset_llm_judge · 03_goldset_consensus_grading
외부 API 의존(비용·지연·제어 불가) 리랭커를 자체 호스팅 Cross-Encoder로 교체.
평가 설계: 39개 시나리오 · 시나리오당 후보 20권 · Primary NDCG@10 / Secondary MRR@10 · Hit@3
| Variant | 포함 필드 | 설계 의도 |
|---|---|---|
| A | 도서명 + 책소개 (baseline) | 카테고리 제거 baseline. “카테고리 없어도 충분한가?” |
| B | A + 카테고리 | 카테고리 포함 시 효과 비교 |
| C | B + 목차 | 목차가 도움되는가? |
| BD | B + 리뷰 (추가실험 진행) | 목차가 도움이 되지 않는가? |
| D | B + 목차 + 리뷰 | 리뷰가 도움되는가? |
| E | D + 저자 + 출판연도 + 페이지 | 전체 필드 |
| Variant | BGE | GTE | Jina |
|---|---|---|---|
| A (baseline) | 0.7199 | 0.7196 | 0.6858 |
| B (+카테고리) | 0.7503 | 0.7385 | 0.7012 |
| BD (+리뷰) | 0.7883 | 0.7609 | 0.6944 |
| C (+목차) | 0.7494 | 0.7354 | 0.7089 |
| D (+목차+리뷰) | 0.7735 | 0.7702 | 0.7039 |
| E (+메타) | 0.7733 | 0.7644 | 0.7242 |
모델별 성능이 좋은 입력 데이터 조합이 다름
xychart-beta
title "리랭커 모델별 NDCG@10 (best variant)"
x-axis ["BGE", "GTE", "Jina", "CLOVA", "EXAONE"]
y-axis "NDCG@10" 0 --> 1
bar [0.788, 0.770, 0.724, 0.653, 0.644]
| 모델 | 유형 | Best Variant | NDCG@10 | MRR@10 |
|---|---|---|---|---|
| BGE ✅ | Cross-encoder | BD | 0.788 | 0.912 |
| GTE | Cross-encoder | D | 0.770 | 0.858 |
| Jina | Cross-encoder | E | 0.724 | 0.793 |
| CLOVA | LLM (RAG API) | D | 0.653 | 0.795 |
| EXAONE | LLM (Listwise) | BD | 0.644 | 0.709 |
가설 기각: LLM 리랭커가 Cross-Encoder를 능가하지 못함. CLOVA는 RAG API 구조상 20개 후보 중 1~2개만 인용(나머지 0점 이진분포), EXAONE은 프롬프트 무시(파싱 실패). 후보 수(20개)가 적어 listwise 이점도 희석.
final = 0.2 × norm(retrieval) + 0.8 × norm(BGE) — α=0.2가 최적.
Retrieval baseline 대비 최종 성능:
| 지표 | Retrieval | BGE(BD) | 향상 |
|---|---|---|---|
| NDCG@10 | 0.7365 | 0.7883 | +0.07 |
| MRR@10 | 0.8329 | 0.9124 | +0.079 |
| Hit@3 | 0.8718 | 0.9744 | +0.103 |
운영 반영 (bge_reranker.py): BAAI/bge-reranker-v2-m3 + BD variant, GPU 자동 감지(CPU fallback), 모델 싱글턴 로딩, 의존성 부재 시 hybrid 결과로 graceful fallback. 실험 상세는 experiments/rerank/ 참고.
팀 공동 작업이지만, 위 골드셋이 검색기 선정에도 사용되었습니다 (39개 시나리오, Recall@20 기준):
| 실험 | 선정 | Recall@20 |
|---|---|---|
| Embedding | KURE(한국어 특화) > CLOVA 0.383 > BGE-M3-KO 0.317 | 0.400 |
| Chunking | strategy0 | 0.311 |
| Hybrid 가중치 | BM25 0.3 / Dense 0.7 | 0.351 |
| Retriever | Hybrid > Dense 0.332 > BM25 0.236 | 0.351 |
├── backend/ # FastAPI 서버, 검색·리랭킹·대출조회 파이프라인
├── frontend/ # React UI
├── data_pipeline/ # 서지·리뷰 데이터 ETL, 임베딩/인덱싱
├── evaluation/ # ★ 평가 프레임워크: 골드셋, 메트릭, 평가 노트북
├── experiments/ # ★ 리랭커 모델 선정 실험 (ablation, 최종 비교)
└── docker-compose.yml
★ = 본인 주도 영역. 상세 설계는 evaluation/README.md, experiments/rerank/README.md 참고.
- Python 3.11+
- Node.js 18+
- Docker & Docker Compose
cd backend
pip install -r ../requirements.txtcd frontend
npm installcp .env.example .env
# 필요한 키 입력docker-compose up -d