46,000개의 청크(chunk)로 RAG 기술 메뉴를 측정했습니다. 중요한 네 가지 요소.
요약
46,000개의 청크를 활용한 대규모 실험을 통해 RAG 기술의 실질적인 효용성을 분석합니다. 기술을 단계별(수집, 변환, 검색, 후처리)로 분류하고, 쿼리 시점의 비용과 성능 사이의 트레이드오프를 고려한 최적의 조합을 제안합니다.
핵심 포인트
- RAG 기술은 단계별로 결합되며, 동일 단계 내 기술은 서로 경쟁 관계임
- 쿼리 시점의 비용(지연 시간, 토큰)은 수집 시점의 비용보다 훨씬 중요함
- 단순히 성능 지표만 볼 것이 아니라 코퍼스 규모에 따른 측정 데이터가 필수적임
- 하이브리드 검색이나 리랭커가 항상 정답은 아니며 측정 결과에 따라 거부될 수 있음
“advanced RAG techniques(고급 RAG 기술)”를 검색하면 하이브리드 검색 (hybrid search), 리랭킹 (reranking), HyDE, 쿼리 분해 (query decomposition), 문맥적 검색 (contextual retrieval), RAPTOR, GraphRAG, 부모-자식 청킹 (parent-child chunking), 멀티 벡터 검색 (multi-vector retrieval), 시맨틱 청킹 (semantic chunking) 등 20가지 목록을 보게 될 것입니다. 이들은 마치 하나씩 적용할 때마다 시스템을 조금씩 개선해 주는, 차례대로 수행하는 메뉴처럼 제시됩니다.
하지만 이들은 그런 방식으로 결합되지 않습니다. 동일한 파이프라인 단계에 있는 기술들은 대부분 서로 _경쟁_합니다. 즉, 동일한 실패 지점을 해결하려 하기 때문에, 두 번째 기술을 추가하면 더 이상 해결할 문제가 남지 않게 됩니다. 그리고 당신의 시스템에 실제로 어떤 기술이 필요한지 알 수 있는 유일한 방법은 당신만의 코퍼스 (corpus)에서 직접 측정하는 것뿐입니다.
저는 62권의 고대사 서적(~46,000개의 청크)을 바탕으로 RAG 시스템을 구축하고, 가능한 모든 검색 기술을 동일한 과정에 통과시켰습니다: 구현하고, 고정된 161개 질문 테스트 세트에 대해 측정하고, 유지하거나 거부하며, 그 수치를 기록했습니다. 이 글은 그 장부의 검색(retrieval) 절반에 대한 내용입니다. 무엇이 채택되었고, 무엇이 거부되었으며, 거부 과정에서 무엇을 배웠는지에 대한 기록입니다. 저의 프로덕션 시스템에는 하이브리드 검색 (hybrid search)도, 리랭커 (reranker)도 없습니다. 이는 지름길을 택한 것이 아니라, 측정의 결과입니다.
지도: 4단계, 2개의 장부
LLM이 단 한 단어를 쓰기 전의 모든 과정은 다음 네 가지 단계 중 하나에 속합니다:
A. INGEST (수집) → B. QUERY TRANSFORM (쿼리 변환) → C. RETRIEVAL CORE (검색 핵심) → D. POST-RETRIEVAL (검색 후 처리)
동일한 단계 내에서는 기술들이 경쟁합니다. 서로 다른 단계에서는 기술들이 결합됩니다. 이 하나의 규칙이 대부분의 리스트 형식 글들이 암시하는 순서를 무너뜨립니다.
두 번째 규칙은 비용에 관한 것이며, 비대칭적입니다:
- 수집 시점의 비용 (Ingest-time cost)은 오프라인에서, 이미 보유한 하드웨어를 통해 단 한 번만 지불됩니다. 코퍼스에 대해 한 번 영리하게 처리해 두면 영원히 저렴합니다.
- 쿼리 시점의 비용 (Query-time cost)은 시스템이 유지되는 동안, 모든 요청마다 지연 시간 (latency)과 토큰 (tokens)의 형태로 지불됩니다.
따라서 쿼리 측면의 영리함은 수집 측면의 영리함보다 훨씬 더 높은 기준을 통과해야 합니다. 매 쿼리마다 유료 API 호출을 통해 +1.7점을 얻는 기술은, 밤샘 배치 작업(batch job) 한 번으로 +1.7점을 얻는 기술보다 더 나쁜 거래입니다. 비록 리더보드의 수치는 동일하게 보일지라도 말입니다.
한 가지 더, 그리고 아무도 라벨을 붙이지 않는 부분입니다. 증거에는 세 가지 등급이 있으며, 이를 혼합하는 것이 잘못된 조언이 퍼지는 방식입니다:
- 여기서 측정됨 (Measured here) — 이 코퍼스(corpus), 46k 청크(chunk), 161개 질문을 대상으로 함.
- 더 작게 측정됨 (Measured smaller) — 나의 이전 프로젝트(책 4권, 950개 청크, 50개 질문)를 대상으로 함. 더 약한 근거임. 발견된 내용이 규모(scale)에 민감해 보일 때마다 재테스트함.
- 추론으로 대체됨 (Reasoned away) — 측정되지 않음. 비용/편익에 따른 판단이며, 그런 경우에만 그렇게 명시할 것임.
판결이 포함된 전체 검색 메뉴(retrieval menu)는 다음과 같습니다. 나머지 기사에서는 흥미로운 행(row)들에 대해 설명합니다.
| 기술 (Technique) | 단계 (Stage) | 판결 (Verdict) | 증거 (Evidence) |
|---|---|---|---|
| 강력한 임베딩 모델 (Strong embedding model) | C | 배포 (Ship) — +18 recall@5 | 여기서 측정됨 |
| ... |
모든 측정의 기준이 되는 베이스라인(baseline): 단순 밀집 검색 (naive dense retrieval), 500-토큰 청크, top-5 — recall@5 = 35.2%. 질문 3개 중 2개는 적절한 구절(passage)과 함께 모델에 도달하지 못했습니다.
단계 A — 인제스트(ingest), 보상이 따르는 단계
책 4권 정도라면 모든 특이점을 눈으로 확인할 수 있습니다. 하지만 62권이라면 불가능하며, 가공되지 않은 Gutenberg 텍스트에 하나의 스플리터(splitter)를 실행한다는 것은 페이지 헤더, 목차, 번역가 각주가 모두 청크로 유출되어 마치 본문인 것처럼 임베딩된다는 것을 의미합니다.
확장 가능한 아키텍처(architecture)는 중간에 엄격한 인터페이스를 둡:
가공되지 않은 책 ──(포맷별 파서)──▶ 정규화된 문서 트리 ──(단일 통일된 청커)──▶ 청크
모든 책별 특이사항은 파서 어댑터(parser adapters)에 존재합니다. 청커(chunker)는 자신이 어떤 책을 처리하고 있는지 절대 학습하지 않는, 잘 테스트된 단일 함수입니다: 우선순위에 따라 구조적 경계(섹션 → 단락 → 문장)에서 분할하고, 약 500토큰까지 탐욕적(greedily)으로 채우며, 섹션 벽을 넘어 병합하지 않고, 문장 중간에서 자르지 않습니다.
대규모 환경에서의 청킹(Chunking)은 토큰 수의 문제가 아니라 파싱(parsing)의 문제입니다. 크기에 대한 논의가 모든 관심을 받지만, 이 단계에서 가장 흥미롭지 않은 결정입니다.
제가 다시 한번 강조하고 싶은 두 가지가 있습니다. 모든 청크(chunk)는 정규화된 텍스트(normalized text) 내의 문자 오프셋(character offsets)을 포함합니다. 이는 테스트 세트의 골드 스팬(gold spans)을 청킹 방식에 무관하게(chunking-invariant) 만들어, 테스트 세트를 다시 작성하지 않고도 청킹 전략을 변경할 수 있게 해줍니다. 또한, 모든 청크는 고유한 인용 위치(Caesar, Gallic War 4.25)를 포함합니다. 고전 문헌은 모든 판본을 관통하는 안정적인 참조 체계를 가지고 있기 때문입니다. 이는 파서(parser)에게 권/장(book/chapter) 번호 매기기를 인식하도록 가르치는 비용을 들인 대가로, 답변 내의 전문적인 인용과 기계적인 테스트 세트 저작을 가능하게 했습니다.
문맥적 검색(Contextual retrieval): 출시되었으며, 숫자를 읽는 법에 대한 교훈을 줍니다
12장의 중간에서 추출한 가공되지 않은 조각은 "그는 그 후 북쪽으로 행군했다"라고 읽힙니다. 임베더(embedder)는 "그"가 누구인지, 이것이 어떤 전쟁인지 전혀 알지 못합니다. 해결책은 수집(ingest) 단계에서의 처리입니다. 저렴한 로컬 LLM이 각 청크의 상황을 설명하는 1~2개의 문장을 작성하며, 임베딩은 단순한 텍스트 대신 context_note + heading_path + chunk_text를 커버합니다. 소비자용 GPU를 사용한 단 한 번의 로컬 배치 패스(batch pass)로 46,170개 중 46,159개의 청크가 보강되었으며, **쿼리 시점의 비용은 제로(zero)**였습니다.
주요 결과는 recall@5 기준 +1.7였습니다. 이는 노이즈 수준입니다. 하지만 내부 지표는 그렇지 않았습니다:
| 카테고리 | Δ recall@5 |
|---|---|
| 합성 (synthesis) | +18.2 |
| ... |
이 프로젝트에서 가장 성적이 좋지 않았던 카테고리인 합성(Synthesis)이 완전히 변했습니다. 도서 간(Cross-book) 성능은 악화되었습니다. 전체 순위는 노이즈를 훨씬 상회하며 정교해졌습니다 (recall@1 +5.8, MRR +0.060). ±18포인트의 내부 변화를 숨기고 있는 평이한 헤드라인 수치는 예외가 아니라 일반적인 사례입니다. 만약 제가 집계된 수치만 보았다면, 이를 아무런 변화가 없는 작업(no-op)이라 치부하고 넘어갔을 것입니다.
생성(generation) 측면의 수치는 훨씬 더 오해의 소지가 있었습니다. 답변 완결성(Answer completeness)은 4.45 → 4.30으로 감소한 것처럼 보였습니다. 하지만 실제로는 그렇지 않았습니다. 두 실행 모두에서 답변이 가능했던 113개의 질문에 대해 완결성은 일정했습니다 (4.46 → 4.40). 실제로 일어난 일은 다음과 같습니다. 문맥적 검색(contextual retrieval)이 이전에 거부되었던 12개의 질문을 답변 가능한 질문으로 전환했고, 검색 부족으로 어려움을 겪었던 이 12개의 질문이 3.42점을 기록하면서 기존의 모든 답변 점수는 유지된 채 평균을 낮춘 것입니다. 범위 내 오거부(In-scope false refusals)는 15.6%에서 7.4%로 떨어졌습니다.
회귀(regression)의 탈을 쓴 승리입니다. 거절(refusals)을 답변으로 전환하는 모든 변화는 이와 같은 결과를 초래하며, 유일한 방어책은 두 실행(run) 모두가 답변한 질문 세트에 대해서만 항상 지표를 계산하는 것입니다.
나머지 단계들은 거부하기에 비용이 저렴했습니다. 부모-자식 검색 (Parent-child retrieval) (작은 청크를 임베딩하고, LLM에게 그들의 부모 섹션을 전달)은 이전 프로젝트에서 완전성(completeness)을 3.22에서 2.67로 퇴보시켰습니다. 재테스트는 가능하지만, 저는 먼저 문맥 결핍(context starvation)에 대한 증거를 확인하고 싶습니다. **ColBERT 스타일의 멀티 벡터 (ColBERT-style multi-vector)**는 벡터 저장 공간을 10~50배 더 소모합니다. 측정조차 하지 않고 저장 공간 예산 문제로 거부했으며, 이를 있는 그대로 받아들입니다. **의미론적 청킹 (Semantic chunking)**과 **청크 대신 임베딩 요약본 사용 (embedding summaries instead of chunks)**은 예상 ROI(투자 대비 수익) 문제로 건너뛰었습니다. 후자는 원본 텍스트를 유지하면서 문맥을 추가하는 문맥적 노트(contextual notes)에 의해 포함됩니다.
단계 B — 쿼리 변환 (query transforms), 건너뛰기에 가장 저렴한 단계
여기 있는 모든 기술은 검색을 시작하기도 전에 추가적인 LLM 호출 비용을 발생시킵니다.
HyDE — 모델이 가상의 답변을 작성하게 하고 질문 대신 그것을 임베딩하는 방식 —은 이전 프로젝트에서 recall@5 점수가 -9.7을 기록했습니다. 이 메커니즘은 일반화가 가능하기 때문에 이해할 가치가 있습니다. HyDE는 사용자가 실제로 제공한 판별적 용어(discriminative terms)를 버리고 쿼리를 대체합니다. 이는 쿼리와 문서 간의 어휘 불일치(vocabulary mismatch)를 위해 구축되었습니다. 만약 당신의 임베더(embedder)가 그러한 불일치가 없을 정도로 충분히 강력하다면, 당신은 신호를 버리면서 그 특권을 위해 추가적인 지연 시간(latency)을 지불하고 있는 것입니다.
멀티 쿼리 확장 (Multi-query expansion) (질문을 n가지 방식으로 바꾸어 표현하고 결과를 합집합함)은 +2.1을 기록했습니다. 실제 효과는 있지만 미미하며, 모든 요청마다 추가 호출과 n번의 검색이 발생합니다. 기본적으로 꺼져 있는(off-by-default) 플래그로 남겨두었습니다.
**쿼리 분해 (Query decomposition)**와 **스텝백 프롬프팅 (step-back prompting)**은 다른 이유로 독립적인 기술로서 건너뛰었습니다. 한 번 이상 검색할 수 있는 검색 루프(retrieval loop)는 실제로 찾은 내용에 따라 이 두 가지를 적응적으로 수행합니다. 루프가 무료로 제공하는 동작을 정적인 버전으로 직접 구축하지 마세요.
단계 C — 임베더(embedder)가 승부의 전부입니다
이것이 가장 중요한 요소였으며, 단 한 줄의 설정만으로 결정됩니다.
기본 임베더(embedder)를 강력한 모델(호스팅된 qwen3-embedding-8b)로 교체했을 때, recall@5 수치가 35.2%에서 53%로 상승했습니다. 가장 큰 타격을 입었던 카테고리인 '빅토리아 시대 번역 산문에 대한 현대 영어 질문'의 경우 +41.7 포인트의 상승을 기록했습니다. 이 글의 다른 어떤 요소도 이와 비교할 수 없습니다.
제가 모델을 선택한 방식이 바로 전이 가능한(transferable) 핵심입니다: _제약 조건(constraints)_으로 후보를 압축하고, _절제 실험(ablation)_으로 결정하십시오. 제약 조건은 구체적이었습니다 — 저렴한 CPU 컨테이너에서 쿼리를 처리할 수 있는가 아니면 API를 사용해야 하는가, 라이선스는 무엇인가, 지시어 접두사(instruction prefixes)가 필요한가, 컨텍스트 윈도우(context window)가 문맥적 노트에 적합한가 등입니다. 이를 통해 네 개의 후보가 도출되었습니다. 리더보드(leaderboard)는 최종 결정 과정에서 투표권을 얻지 못했습니다. 왜냐하면 어떤 리더보드도 빅토리아 시대 번역 산문을 포함하고 있지 않기 때문입니다. 당신의 코퍼스(corpus)만이 유일하게 의미 있는 리더보드입니다.
여기서 두 가지 실수(footguns)는 실제 프로젝트의 품질을 심각하게 저하시킵니다:
- 접두사 일치(Prefix parity). 대부분의 현대적 임베더는 쿼리(query)와 문서(document)에 대해 서로 다른 지시어 접두사를 요구하며, 오픈 웨이트(open-weight) 모델을 서비스하는 호스팅 API는 일반적으로 이를 자동으로 주입해 주지 않습니다. 접두사가 포함된 상태로 임베딩된 코퍼스와 접두사가 없는 쿼리를 사용하는 것은, 겉으로는 드러나지 않지만 수 퍼센트의 손실을 일으키는 조용한 패배입니다. 접두사 정책을 관리하는 하나의 모듈로 임베딩을 래핑(wrap)하고, 두 군데 이상의 장소에서 모델을 호출하지 마십시오.
- 동일 모델 ≠ 동일 벡터. 런타임 차이(sentence-transformers vs llama.cpp vs 호스팅 API), fp16 vs fp32, 그리고 양자화(quantization)는 모두 벡터를 변화시킵니다. 저렴한 방어책은 다음과 같습니다: 두 스택 모두에서 20개의 고정된 문장을 임베딩해 보고, 이를 하나의 인덱스로 신뢰하기 전에 코사인 유사도(cosine similarity)가 0.999 이상인지 확인하십시오.
Hybrid BM25 + RRF: 거부됨, 그리고 이것이 프로젝트에서 얻은 가장 값진 교훈입니다
2024년 기준의 표준적인 조언은 하이브리드 검색(hybrid search) — 키워드 BM25와 벡터 검색(vector search)의 결합 — 이 대규모 환경, 특히 희귀한 고유명사에 대해 항상 승리한다는 것입니다. 제 코퍼스(corpus)는 일관되지 않은 빅토리아 시대 철자로 표기된 희귀 고유명사(Vercingetorix, Pharsalus)로 가득 차 있습니다. 저는 이미 950개의 청크(chunk) 단계에서 하이브리드 방식을 한 번 거부한 적이 있었고, 실행 전 다음과 같은 예측을 적어두었습니다: 46k 청크 규모가 되면 결과가 승리로 뒤집힐 것이다.
하지만 뒤집히지 않았습니다. 아무것도 반환하지 못했습니다.
| 지표 (metric) | 밀집 (dense) | 하이브리드 (hybrid) | Δ |
|---|---|---|---|
| recall@1 | 32.5 | 30.7 | −1.8 |
| ... |
"대략 비슷함" 수준이 아니었습니다 — **카테고리별로 바이트 단위까지 동일한 풀 리콜(pool recall)**이었습니다. BM25가 정확한 토큰 매칭(token match)을 통해 찾아낼 수 있었던 모든 답변을 8B 임베더(embedder)가 이미 가지고 있었습니다. 그리고 퓨전(fusion)은 상위 순위를 약간 더 나쁘게 만들었는데, 이는 RRF가 키워드 노이즈(keyword-noise) 청크를 주입하여 순위가 높은 밀집(dense) 검색 결과들을 밀어냈기 때문입니다.
이 발견 자체가 메커니즘입니다: 강력한 밀집 검색기(dense retriever)가 놓치는 지점에서는, 정답이 키워드로 찾을 수 있는 형태가 아니라 분산되어 존재한다 — 따라서 BM25 역시 정답에 도달할 수 없습니다. "하이브리드가 항상 승리한다"는 조언은 약한 어휘적(lexical) 1단계 과정을 가정합니다. 현대적인 8B 임베더를 사용하는 이 코퍼스에서는 그 가정이 틀렸습니다.
이 결과를 읽을 수 있게 만든 요소에 주목하십시오: _풀 리콜(pool recall)_로 취급된 recall@50입니다. Recall@1만 보았다면 -0.4라는 수치가 나왔을 것이고, 저는 BM25가 새로운 후보를 제공했는데 퓨전 과정에서 순위가 잘못 매겨진 것인지 추측만 하며 헤맸을 것입니다. "풀을 확장했는가"와 "풀의 순서를 재조정했는가"를 구분하도록 설계된 지표 덕분에, 모호한 무승부 상태를 명확한 거부로 전환할 수 있었습니다. 단순히 결과를 점수화하는 것이 아니라, 메커니즘을 구분할 수 있도록 지표를 설계하십시오.
단계 D — 스스로의 논리에 실패한 리랭커(reranker)
크로스 인코더(cross-encoder)는 상위 50개를 다시 점수 매겨 최적의 5개를 반환합니다. 이는 RAG에서 가장 권장되는 기술이며, 저는 다섯 가지의 리랭커를 측정했습니다.
| reranker | host | recall@5 vs no-rerank |
|---|---|---|
| qwen3-reranker-0.6b | local | −3.0 |
| ... | ||
| 이것은 임베더 게이트 (embedder gate)와 정확히 반대되는 현상입니다. 임베더 게이트에서는 컴포넌트가 너무 약해서 조금이라도 더 나은 것이 큰 승리였습니다. 반면 여기서는 임베더가 너무 강력해서 0.6B 크로스 인코더 (cross-encoder)가 8B 임베더 자체의 순위보다 더 나쁜 결과를 초래했습니다. 즉, 노이즈를 추가하는 셈입니다. 오직 최첨단 (state-of-the-art) 호스팅 리랭커 (hosted reranker)만이 도움이 되는데, 이는 리랭킹 (reranking) 기능을 도입한다는 것이 영구적으로 쿼리당 비용이 발생하는 유료 종속성을 도입함을 의미합니다. |
이 과정에서 하나의 아키텍처 법칙이 도출되었으며, 이는 비용 없이 준수할 수 있는 것입니다: 임베딩한 것과 동일한 텍스트를 리랭크하십시오. 인덱스 (index)에는 문맥화된 (contextualized) 텍스트가 들어있는데 정작 청크 (chunk)의 순수 텍스트만 점수를 매기면, 리랭커가 리트리버 (retriever)와 싸우게 되어 문맥화로 얻은 이득을 완전히 상쇄해 버립니다 (이전 실험에서 47.9% vs 51.6%). 검색 (Retrieval)과 리랭크 (rerank)는 표현 (representation)을 공유해야 합니다.
흥미로운 점은 제가 왜 이를 제외했는가 하는 부분입니다. 리랭커는 특정한 명시적 이유 때문에 잠정적으로 유지되었습니다. 문맥적 검색 (contextual retrieval)으로 인해 여러 권의 책을 가로지르는 질문 (cross-book questions)에서 9점의 손실이 발생했고, 상위 50개를 리랭크하면 이를 회복할 수 있을 것이라 예상했기 때문입니다. 하지만 결과는 그렇지 않았습니다. 여러 권의 책을 가로지르는 질문의 점수는 최저치인 34.4점 대비 26.0점에 머물렀으며, 리랭커가 없어도 회복되지 않았습니다. 비록 후보군 (pool) 내에 정답이 분명히 존재했음에도 말입니다. 여러 권의 책을 가로지르는 질문은 순서 (ordering)의 문제가 아니라 후보군 (candidate-pool)의 문제였으며, 리랭킹은 후보군에 없는 것을 끌어올릴 수 없습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기