RAG(Retrieval-Augmented Generation) 파이프라인을 구축하다 보면 어느 순간 묘한 벽에 부딪힌다. LLM은 GPT-4급을 쓰고 있고, 문서도 수만 건을 인덱싱했는데, 정작 사용자가 던진 질문에 대해 모델은 엉뚱한 답변을 내놓는다. 이때 대부분의 개발자는 프롬프트를 고치거나 더 큰 모델로 교체하는 방향을 먼저 떠올린다. 하지만 진짜 문제는 대체로 다른 곳에 있다. 검색 단계가 엉망이면 LLM에 무엇을 집어넣어도 소용없다.
검색 품질이 LLM 출력을 지배하는 이유
RAG의 동작 방식을 단순화하면 이렇다. 사용자의 질문을 임베딩 벡터로 변환하고, 사전에 인덱싱해둔 문서 청크(chunk) 벡터들과 유사도를 비교한 뒤, 가장 가까운 k개의 청크를 LLM의 컨텍스트 창에 넣어 답변을 생성한다. 이 흐름에서 LLM이 개입하는 건 마지막 단계뿐이다. 앞선 두 단계 — 임베딩 변환과 벡터 검색 — 에서 잘못된 문서가 올라오면, LLM은 그 잘못된 정보를 기반으로 그럴듯하게 포장된 오답을 생성한다. “Garbage In, Garbage Out”의 벡터 버전이다.
흥미로운 건 이 문제가 단순히 “관련 없는 문서를 가져왔다”는 수준에 그치지 않는다는 점이다. 검색된 문서가 주제적으로는 관련이 있지만, 질문이 요구하는 세부 사실은 포함하지 않는 경우도 있다. 예를 들어 “2024년 3분기 매출이 얼마냐”는 질문에, 검색 시스템이 “2024년 매출 동향”을 다룬 요약 문서를 상위에 올린다면, LLM은 정확한 수치 없이 어물쩍 넘어가거나 할루시네이션을 일으킨다. 올바른 청크가 검색됐는데도 LLM이 틀린 답변을 내는 것은 별개의 실패 모드인데, 이건 뒤에서 다시 짚겠다.
임베딩 모델이 결정적인 이유
벡터 유사도 검색의 품질은 결국 임베딩 모델이 의미 공간을 어떻게 표현하느냐에 달려 있다. 같은 문장 쌍이라도 어떤 모델을 쓰느냐에 따라 코사인 유사도 값은 크게 달라진다. 예컨대 “사과를 먹었다”와 “과일을 섭취했다”는 문장은 의미상 매우 가깝지만, 일반 목적 임베딩 모델에서는 0.65 수준의 유사도가 나올 수 있고, 문장 의미 표현에 특화된 모델(SBERT 계열, OpenAI의 text-embedding-3-large 등)에서는 0.88 이상이 나오기도 한다.
더 까다로운 문제는 도메인 특수성이다. 법률 문서에서 “약정”과 “계약”은 거의 같은 의미로 쓰이지만, 일반 목적 임베딩 모델은 이 두 단어의 표현 벡터를 충분히 가깝게 배치하지 못할 수 있다. 반대로 의학 문서에서 “저혈압”과 “혈압이 낮다”처럼 동의어 수준의 표현도 일반 모델에서는 충분한 유사도가 나오지 않을 수 있다. 이 경우 도메인 특화 파인튜닝이 필요하거나, MTEB(Massive Text Embedding Benchmark) 같은 벤치마크에서 해당 도메인 태스크 점수가 높은 모델을 선택하는 것이 중요하다.
임베딩 차원 수도 실용적인 고려 사항이다. 1536차원(OpenAI ada-002)이나 3072차원(text-embedding-3-large)은 표현력이 높지만 저장 비용과 ANN 검색 속도에 영향을 준다. 768차원 모델(BGE, E5 계열)도 충분한 품질을 내면서 비용을 낮출 수 있다. 무조건 차원이 높다고 좋은 게 아니라, 실제 도메인 데이터에 대한 recall 지표로 검증해야 한다.
Dense와 Sparse — 각각 언제 실패하는가
벡터 임베딩 기반의 Dense 검색은 의미적으로 유사한 문장을 잘 찾지만, 정확한 키워드나 고유명사를 매칭해야 하는 상황에서 의외로 취약하다. “SKU-29481 부품의 재고 현황”이라는 쿼리를 처리할 때, Dense 검색은 “재고 현황”이라는 의미적 근접성에 끌려 엉뚱한 부품 정보를 올려놓을 수 있다. 이것이 소위 semantic gap이 아닌 lexical gap 문제다 — 의미는 연결됐지만 정확한 식별자(identifier)가 틀린 문서가 올라오는 것이다.
반대로 BM25 같은 Sparse 검색은 키워드 일치에 강하지만, 어휘 변형이나 의미적 우회에 취약하다. “돈을 아끼는 방법”이라는 쿼리에 “비용 절감 전략”이라는 제목의 문서가 있다면, BM25는 이 문서를 거의 찾지 못한다. 두 쿼리 사이에 공통 단어가 없기 때문이다. semantic gap 문제다.
실무에서는 어느 한 쪽만으로는 충분하지 않다. 고유명사와 의미 검색을 동시에 잘 처리해야 하는 기업 문서 검색, 제품 카탈로그, 기술 매뉴얼 등의 도메인에서는 두 방식을 함께 써야 한다.
Hybrid Search와 RRF의 현실
두 방식을 합치는 가장 대중적인 방법이 RRF(Reciprocal Rank Fusion)다. 원리는 간단하다. Dense 검색 결과와 Sparse 검색 결과 각각의 순위를 역수 점수(1/(rank + k), 보통 k=60)로 변환한 뒤 합산해 최종 순위를 정한다. 상위 랭크 문서는 높은 점수를, 하위 랭크 문서는 낮은 점수를 받기 때문에, 두 방식 모두에서 상위에 오른 문서가 자연스럽게 최상위를 차지한다.
RRF의 장점은 두 검색 시스템의 점수 스케일을 통일할 필요 없이 순위만 사용한다는 것이다. Dense 점수가 0.0~1.0이고 BM25 점수가 0~100이더라도 상관없다. 다만 한계도 있다. 두 방식 각각의 순위를 동등하게 취급하기 때문에, 특정 쿼리 유형(키워드 위주 vs 의미 위주)에 따른 가중치 조절이 어렵다. 일부 시스템에서는 각 검색기에 가중치를 부여하는 weighted RRF 변형을 쓰지만, 이 가중치를 쿼리별로 동적으로 조정하는 건 여전히 열린 문제다.
ANN 알고리즘: HNSW, IVF-PQ, LSH의 구조적 차이
수백만 건 이상의 벡터를 실시간으로 검색하려면 정확한 nearest neighbor 탐색(brute force)은 현실적으로 불가능하다. 이 때문에 Approximate Nearest Neighbor(ANN) 알고리즘이 필요하고, 어떤 ANN 인덱스를 쓰느냐에 따라 recall과 latency 사이의 트레이드오프가 달라진다.
HNSW(Hierarchical Navigable Small World)는 현재 가장 널리 쓰이는 그래프 기반 알고리즘이다. 여러 레이어의 근접 그래프를 계층적으로 쌓아두고, 상위 레이어에서 빠르게 근접 영역을 찾은 뒤 하위 레이어에서 정밀 탐색한다. 높은 recall(95% 이상도 가능)과 낮은 쿼리 레이턴시를 동시에 달성할 수 있지만, 인덱스를 메모리에 올려야 하기 때문에 대규모 벡터셋에서는 메모리 비용이 문제가 된다. efConstruction과 M 파라미터를 조정해 인덱스 품질과 빌드 속도를 조율할 수 있다.
IVF-PQ(Inverted File Index + Product Quantization)는 대규모 벡터셋에 더 적합한 방식이다. 벡터 공간을 클러스터(보로노이 셀)로 나눈 뒤, 쿼리가 들어오면 가장 가까운 몇 개의 클러스터만 탐색한다. PQ는 벡터를 압축 표현으로 저장해 메모리를 아끼고 거리 계산을 빠르게 한다. 트레이드오프는 명확하다 — recall이 HNSW보다 낮고, nprobe(탐색할 클러스터 수) 파라미터를 높이면 recall은 올라가지만 속도가 느려진다. 수천만~수억 건 벡터를 다루는 환경에서 메모리 효율이 중요하다면 IVF-PQ가 현실적인 선택이다.
LSH(Locality-Sensitive Hashing)는 비슷한 벡터가 같은 버킷으로 해시될 확률을 높이는 방식으로, 구조가 단순하고 구현이 쉽다. 하지만 recall이 HNSW나 IVF-PQ에 비해 낮아서 최근에는 실무에서 잘 선택하지 않는다. 학술 연구나 특수 목적 환경에서 여전히 쓰이는 편이다.
이미지 출처: Unsplash
청킹 전략이 검색 품질을 결정하는 방식
문서를 어떻게 나누느냐는 임베딩 모델 선택 못지않게 중요하다. 고정 크기 청킹(fixed-size chunking)은 구현이 쉽고 예측 가능하지만, 문장이나 단락의 의미 경계를 무시하고 잘라버린다. 512 토큰짜리 청크를 만들다 보면, 어떤 청크의 마지막 문장이 다음 청크의 첫 문장과 논리적으로 연결된 경우가 비일비재하다. 이 두 청크 중 어느 쪽도 해당 정보를 완전하게 담고 있지 않아 검색 후 LLM이 맥락을 조립하지 못하는 상황이 생긴다.
overlap을 두는 이유가 여기 있다. 보통 128~256 토큰 정도 이전 청크와 겹치도록 설정해, 경계에 걸친 정보가 적어도 한 청크에는 온전히 담기도록 한다. 다만 overlap이 너무 크면 인덱스 크기가 불필요하게 커지고 중복 정보가 검색 결과에 다수 올라온다.
Semantic chunking은 문장 임베딩 사이의 유사도 급락 지점을 감지해 자연스러운 의미 경계에서 나누는 방식이다. LangChain의 SemanticChunker나 LlamaIndex의 SentenceSplitter가 이 접근을 구현한다. 고정 크기 청킹보다 검색 품질이 좋은 경우가 많지만, 청킹 자체에 임베딩 연산이 필요해 인덱싱 비용이 높아진다. 문서 크기가 수백만 건을 넘는다면 비용 대비 효과를 따져봐야 한다.
청크 크기 자체도 쿼리 유형과 문서 구조에 따라 달리 설정해야 한다. 짧고 명확한 사실 질문(“CEO가 누구냐”)에는 작은 청크(256 토큰 내외)가 유리하고, 개념 이해나 요약이 필요한 질문에는 큰 청크(512~1024 토큰)가 더 잘 작동하는 경향이 있다. 일부 시스템에서는 “small-to-big” 전략을 쓴다 — 작은 단위로 검색해 정확도를 높이고, 실제 LLM에는 상위 청크(부모 단락이나 섹션 전체)를 넘겨주는 방식이다.
Reranking — 두 단계로 나누는 이유
검색 1단계에서 bi-encoder(임베딩 모델)로 후보 k개를 빠르게 뽑고, 2단계에서 cross-encoder로 정밀 재정렬하는 two-stage reranking은 현재 고품질 RAG 시스템의 사실상 표준 구조다.
bi-encoder는 쿼리와 문서를 각각 독립적으로 인코딩하기 때문에 수백만 건 규모에서도 빠른 검색이 가능하다. 반면 cross-encoder는 쿼리와 문서를 함께 입력으로 받아 관련성 점수를 계산한다. 두 텍스트 사이의 상호작용을 직접 모델링하기 때문에 관련성 판단이 훨씬 정확하다. 그 대신 모든 후보 문서에 대해 개별적으로 추론을 돌려야 해서 수백만 건 전체에 적용하면 레이턴시가 폭발한다.
따라서 1단계에서 bi-encoder로 50~200개 후보를 걸러내고, 2단계에서 cross-encoder가 그 후보들만 재정렬하면 정확도와 속도를 동시에 잡을 수 있다. Cohere Rerank, Jina Reranker, FlashRank 같은 서비스와 라이브러리가 이 2단계 구조를 지원한다.
벡터 DB 제품 선택의 실용적 기준
| 제품 | 호스팅 방식 | 주요 특징 | 적합한 상황 |
|---|---|---|---|
| Pinecone | Managed SaaS | 완전 관리형, 서버리스 옵션 | 인프라 관리 부담 없이 빠르게 시작 |
| Weaviate | Self-hosted / Cloud | 멀티모달, 모듈형 구조, GraphQL API | 다양한 데이터 타입, 유연한 확장 |
| Qdrant | Self-hosted / Cloud | Rust 구현, 고성능 필터링, payload 인덱싱 | 복잡한 메타데이터 필터링이 중요한 경우 |
| pgvector | Self-hosted (PostgreSQL 확장) | 기존 PG 인프라 재활용, SQL 통합 | 이미 PostgreSQL을 운영 중인 팀 |
Managed SaaS(Pinecone)는 운영 복잡도를 낮추지만 벤더 종속과 데이터 주권 이슈가 있다. Self-hosted(Qdrant, Weaviate)는 완전한 제어권을 갖지만 클러스터 관리, 샤딩, 백업 전략을 직접 설계해야 한다. pgvector는 “PostgreSQL만 운영할 줄 알면 된다”는 단순함이 강점이지만, 수천만 건 이상에서는 HNSW 인덱스 품질과 필터링 성능이 전용 벡터 DB에 못 미친다.
최근에는 DiskANN 기반 인덱스나 Milvus의 분산 아키텍처처럼 수십억 건 규모를 겨냥한 선택지도 성숙해지고 있다.
올바른 청크가 올라와도 LLM이 틀리는 이유
RAG의 마지막 실패 모드는 꽤 미묘하다. 검색된 청크가 실제로 정답을 포함하고 있는데도 LLM이 엉뚱한 답변을 내놓는 경우다. 가장 흔한 원인은 컨텍스트 창 안에서의 위치 편향(positional bias)이다. LLM은 컨텍스트 앞부분과 뒷부분에 있는 정보에 더 집중하고, 중간에 있는 정보를 상대적으로 무시하는 “Lost in the Middle” 현상을 보인다. 정답이 담긴 청크가 5번째 컨텍스트 항목으로 들어갔다면 LLM이 그 내용을 효과적으로 활용하지 못할 수 있다.
두 번째 원인은 청크 내 노이즈다. 정답 문장 주변에 관련 없는 내용이 섞여 있으면, LLM이 핵심 정보를 뽑아내는 과정에서 혼란을 겪는다. 세 번째는 지시 충돌이다. 시스템 프롬프트나 사용자 지시가 검색된 문서의 내용과 충돌할 때 LLM은 종종 학습된 지식을 우선시하거나 두 정보를 부적절하게 합성한다.
이 실패 모드들은 검색 시스템을 고쳐서 해결할 수 없다. 컨텍스트 정렬 전략(가장 관련 높은 청크를 맨 앞에), 청크 사후 정제(필요 없는 내용 제거), 명확한 출처 기반 응답 지시 같은 프롬프트 엔지니어링 및 후처리 접근이 필요하다.
결국 RAG 시스템의 성능을 끌어올리는 것은 LLM 교체나 프롬프트 튜닝 이전에, 임베딩 모델 선택, 청킹 전략, ANN 인덱스 설정, 하이브리드 검색, 리랭킹으로 이어지는 검색 파이프라인 전반을 체계적으로 평가하고 개선하는 작업이다. 앞으로 RAG 평가 프레임워크(RAGAS, TruLens 등)와 함께 검색 품질 지표를 자동화된 방식으로 추적하는 관행이 더 빠르게 확산될 것으로 보인다. 검색 단계의 recall과 precision을 오프라인으로 측정하고 회귀 테스트에 포함시키지 않으면, 파이프라인 변경이 성능에 어떤 영향을 미쳤는지 파악하기 어렵다는 인식이 점점 넓어지고 있기 때문이다.
출처
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (Lewis et al., 2020)
- MTEB: Massive Text Embedding Benchmark (Muennighoff et al., 2022)
- Lost in the Middle: How Language Models Use Long Contexts (Liu et al., 2023)
- Efficient and Robust Approximate Nearest Neighbor Search Using HNSW (Malkov & Yashunin, 2018)
- RAGAS: Automated Evaluation of Retrieval Augmented Generation (Es et al., 2023)
- Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods (Cormack et al., 2009)
- Qdrant Documentation — Hybrid Search
- Weaviate Blog — Chunking Methods
- Cohere Rerank Documentation
- pgvector GitHub Repository