RAG에 대한 흔한 오해들 — 벡터 검색이 마법은 아니다
RAG(Retrieval-Augmented Generation)는 LLM 기반 서비스에서 가장 널리 쓰이는 아키텍처다. 그런데 "벡터 검색이니까 의미를 다 이해하겠지"라는 기대로 시작하면 꽤 빨리 벽에 부딪힌다. 나도 처음에 RAG를 접했을 때 "임베딩만 하면 다 되는 거 아냐?"라고 생각했는데, 실제로 해보니 생각보다 까다로운 점이 많았다.
이 글에서 다루는 오해가 전부는 아니지만, 가장 자주 마주치는 것들 위주로 정리해봤다.
임베딩이 의미를 "완벽하게" 이해한다는 착각
이게 가장 충격적인 부분이었다. "의미가 반대인 단어는 벡터 공간에서도 멀리 떨어져 있겠지"라고 자연스럽게 생각하게 되는데, 현실은 정반대다.
천국 ↔ 지옥 → 코사인 유사도: ~0.85 이상
좋다 ↔ 나쁘다 → 코사인 유사도: ~0.80 이상
성공 ↔ 실패 → 코사인 유사도: ~0.78 이상왜 이런 일이 생기나? 임베딩 모델은 단어가 등장하는 문맥(context)을 학습한다. "천국"과 "지옥"은 종교, 사후세계, 도덕 같은 거의 동일한 문맥에서 함께 등장하기 때문에, 벡터 공간에서 매우 가깝게 위치한다. 분포적 의미론(distributional semantics)이라고 하는데, 처음 알았을 때 솔직히 좀 당황했다.
이게 실무에서 왜 문제가 되냐면, 고객 리뷰를 RAG로 검색할 때 "이 제품은 최고입니다"를 쿼리하면 "이 제품은 최악입니다"도 높은 유사도로 같이 검색된다. 감성이 정반대인 문서가 결과에 섞이는 거다.
동음이의어도 비슷한 문제를 일으킨다. "배가 아프다"를 검색하면 복통 관련 문서뿐 아니라 선박 사고 문서도 올라올 수 있다. 문맥이 충분히 길면 괜찮은데, 짧은 쿼리에서는 구분을 잘 못한다.
청크 사이즈, 생각보다 중요하다
많은 팀이 "500토큰으로 자르면 되겠지"라고 대충 정하는데, 청킹 전략이 검색 품질에 미치는 영향은 생각보다 크다.
청크가 너무 작으면 문맥이 잘린다. "2024년 3분기 매출은 150억원으로 전년 대비 20% 증가했으며, 영업이익률은 12%를 기록했다"라는 문장이 여러 청크로 쪼개지면, "3분기 영업이익률은?"이라는 질문에 매출 맥락 없이 단편적인 정보만 검색된다.
반대로 청크가 너무 크면 여러 주제가 한 청크에 뒤섞여서, 임베딩이 어떤 주제도 제대로 표현하지 못한다. 특정 주제를 검색할 때 관련 없는 내용이 딸려오고, LLM 컨텍스트를 쓸데없는 정보로 낭비하게 된다.
내가 해본 결과 가장 나았던 방식은 문서의 자연스러운 구조(단락, 섹션)를 활용한 시맨틱 청킹이었다. 그리고 청크 간에 10~20% 정도 오버랩을 두면 문맥 단절이 좀 완화된다. 다만 이건 데이터 성격마다 다르니까, 결국 자기 데이터로 직접 실험해보는 수밖에 없다.
RAG 붙이면 할루시네이션이 사라진다?
이건 정말 위험한 오해라고 생각한다. "외부 문서를 넣어주니까 거짓말할 이유가 없지 않나?"라고 생각하기 쉬운데, RAG 환경에서도 할루시네이션은 발생한다.
관련 문서를 못 찾으면 LLM은 자기 학습 지식이나 추측으로 답변을 메꾼다. 쿼리와 70%만 관련 있는 문서가 검색되면 나머지 30%를 지어내서 답변을 완성하기도 한다. 여러 청크가 서로 모순되는 정보를 담고 있으면 LLM이 임의로 하나를 선택하거나 둘을 합성하는 경우도 있다.
검색된 청크: "2024년 매출은 100억원이다"
LLM 응답: "2024년 매출은 약 100억원으로, 전년 대비 15% 성장했습니다"
^^^^^^^^^^^^^^^^^^^^^^^^
검색 결과에 없는 정보를 추가이런 문제를 줄이려면 Generation 단계의 프롬프트 설계가 중요하다. "제공된 문서에만 기반해서 답변하세요. 문서에 정보가 없으면 모른다고 하세요"라는 지시를 명시하는 것만으로도 꽤 차이가 난다. 근데 이것도 완벽하지는 않다. 결국 검색 품질 자체를 높이는 게 근본적인 해결책이다.
벡터 검색이 키워드 검색보다 항상 나은 건 아니다
이것도 처음에 오해했던 부분인데, 제품 코드나 에러 코드, 법률 조항 번호처럼 정확한 텍스트 매칭이 필요한 경우에는 오히려 키워드 검색(BM25)이 더 정확하다.
"ERR-5023 해결 방법"을 검색했을 때, 시맨틱 검색은 "에러 해결", "오류 대응" 같은 의미적으로 비슷한 문서를 끌어오는데, 정작 ERR-5023이 아니라 ERR-4011에 대한 문서가 나올 수 있다. 키워드 검색은 "ERR-5023"이 정확히 들어있는 문서를 찾으니까 이런 경우에 더 낫다.
실무에서는 벡터 검색과 키워드 검색을 결합한 하이브리드 검색이 보통 가장 좋은 성능을 보인다. 최종 점수 = α × 시맨틱 유사도 + (1-α) × BM25 점수 형태로, α값을 데이터 특성에 맞게 튜닝한다. Elasticsearch나 Weaviate 같은 대부분의 벡터 DB가 이미 하이브리드 검색을 지원한다.
그 외에 놓치기 쉬운 것들
임베딩 모델은 다 비슷하지 않다. OpenAI의 text-embedding-3-large와 Cohere의 embed-v3은 완전히 다른 벡터 공간을 만든다. 서로 다른 모델로 생성된 벡터를 같은 DB에서 비교하면 의미 없는 결과가 나온다. 그래서 프로젝트 중간에 임베딩 모델을 바꾸면 기존 벡터를 전부 재생성해야 한다. 한국어 데이터를 다룬다면 BGE-m3이나 KoSimCSE 같은 한국어 특화 모델을 고려해보는 게 좋다.
차원이 높다고 무조건 좋은 것도 아니다. 차원이 올라가면 거리 집중 현상(Distance Concentration)이 생겨서, 가장 가까운 벡터와 가장 먼 벡터의 거리 차이가 미미해진다. 문서 수가 수천 건이라면 768차원으로도 충분한 경우가 많다.
Top-K를 늘리면 좋아질 것 같지만, LLM에는 "Lost in the Middle" 현상이 있다. 긴 컨텍스트에서 처음과 끝에 있는 정보에는 집중하지만 중간에 있는 건 무시하는 경향이 있어서, Top-20으로 검색하면 정작 중요한 문서가 중간에 묻혀버릴 수 있다.
한 번 구축하면 끝이 아니다. 소스 문서가 업데이트되면 벡터도 갱신해야 하고, 임베딩 모델이 바뀌면 전체 재인덱싱이 필요하다. 검색 품질을 주기적으로 모니터링하는 파이프라인도 갖춰야 한다. RAG는 살아있는 시스템이다.
결국 중요한 건
RAG는 강력한 도구지만, 마법은 아니다. 벡터 검색의 한계를 이해하고, 자기 데이터와 유스케이스에 맞게 각 단계를 세밀하게 튜닝해야 실무에서 기대하는 성능이 나온다.
개인적으로는 처음에 "임베딩하면 다 되겠지"라는 막연한 기대에서 시작해서, 하나씩 벽에 부딪히면서 이런 것들을 알게 됐다. 아직도 청킹 전략 같은 건 프로젝트마다 새롭게 고민해야 하는 부분이고, 정답이 있다기보다는 계속 실험하면서 맞춰가는 영역이라고 느낀다.