
RAG가 잘못된 답변을 내놓는 이유 (그리고 검색 실패를 해결하는 방법)
요약
RAG 시스템이 잘못된 답변을 생성하는 주요 원인인 검색 실패 문제를 분석합니다. 특히 청킹(chunking) 전략의 부재로 인해 모델이 올바른 문서를 찾았음에도 불구하고 불충분한 정보를 전달받아 발생하는 오류를 다룹니다.
핵심 포인트
- RAG 오류의 주된 원인은 모델 자체보다 검색 단계의 잘못된 증거 전달에 있음
- 청크(Chunk) 크기가 너무 작으면 핵심 정보가 누락되어 불완전한 답변 생성
- 효과적인 RAG 구축을 위해 적절한 청킹 및 검색 전략 수립이 필수적임
이전 포스트에서 우리는 처음부터 RAG 시스템을 구축했습니다. 단 60줄의 Python 코드. 6개의 온보딩 문서를 청킹 (chunking)하고, 임베딩 (embedding)하여 검색 가능하게 만들었습니다. 두 개의 질문을 던졌고, 실제 정책 문서에 근거한 두 개의 정확한 답변을 얻었습니다.
매우 잘 작동했습니다. 그러니 당연히, 이를 개선하는 방법을 배우기 위해 무엇이 잘못되었는지 파헤쳐 봅시다.
그리고 영상에서 다룰 시간이 부족했던 부분보다 더 깊이 들어가 보겠습니다. 청킹 (chunking) 및 검색 (retrieval) 전략의 전체 툴킷과, 실제로 각 전략을 언제 사용해야 하는지에 대해서 말이죠.
모든 코드는 제 GitHub 저장소의
ep06-rag-failures폴더에 있습니다. 각 실패 사례와 그 해결책은 정확히 한 가지만 변경된 별도의 파일로 구성되어 있어, 그 차이점 자체가 핵심 내용입니다.
RAG가 실패하는 두 가지 방식
RAG에 대해 알아두어야 할 점은 이것입니다. 모델이 잘못된 답변을 내놓을 때, 본능적으로 모델을 탓하게 된다는 것입니다. 하지만 대부분의 경우, 모델은 지시받은 대로 정확히 수행했습니다. 문제는 모델에게 주어진 것입니다. 검색 (retrieval) 단계에서 잘못된 증거를 전달한 것입니다. 잘못된 증거가 입력되면, 확신에 찬 잘못된 답변이 출력됩니다.
저는 동일한 시스템에 대해 두 가지 새로운 질문을 던졌습니다. 두 질문 모두 실패했으며, 서로 다른 계층에서 서로 다른 이유로 실패했습니다.
실패 1: 청크 (Chunks)가 너무 작음
질문: "온보딩 기간 동안 홈 오피스 설정을 비용으로 처리할 수 있나요?"
시스템은 5개의 청크 (chunks)를 검색했습니다. 비용 정책의 교차 참조 내용: "홈 오피스 수당에 대한 자세한 내용은 혜택 가이드 섹션 5.3을 참조하십시오." 도입 문구만 있는 섹션 제목: "5.3 하이브리드 및 원격 근무 직원을 위한 홈 오피스 설정." 인체공학적 평가에 대한 언급. 점수는 약 0.47 정도로 괜찮았습니다. 시스템은 올바른 문서를 찾아냈습니다.

하지만 실제 답변인 채용 시 750달러의 수당, 연간 250달러의 갱신 비용, 1,200달러의 인체공학적 예산 중 그 어느 것도 검색된 청크 (chunks)에 포함되지 않았습니다. 그 숫자들은 상위 5위 안에 들지 못한 청크에 들어 있었습니다.
그리고 모델은 이에 대해 솔직했습니다. 모델은 온보딩 (onboarding) 기간 중 홈 오피스 설정을 비용 처리할 수 있는지에 대한 구체적인 정보가 없으며, 해당 세부 사항은 복리후생 가이드(Benefits Guide) 섹션 5.3에 있지만 "제공된 컨텍스트 (context)에 완전히 표시되지 않았다"라고 말했습니다.
이것은 환각 (hallucination)이 아니라, 불완전한 응답입니다. 왜냐하면 저(또는 여기서의 RAG)가 불완전한 증거를 제공했기 때문입니다.
진단: 단락 기반 분할 (paragraph-based split)이 해당 섹션을 너무 짧게 잘라냈습니다. 위치는 맞았지만, 답변이 불완전했습니다.
실패 2: 모호한 매칭 (Ambiguous match)
질문: "90일 규칙이 무엇인가요?"

네 개의 서로 다른 문서에서 다섯 개의 청크가 검색되었으나, 모두 0.27에서 0.39 사이의 낮고 유사한 점수를 기록했습니다. 눈에 띄는 것이 없었는데, 이것이 바로 문제입니다. "90일"이라는 문구는 이 정책들 전반에 걸쳐 완전히 다른 문맥 (contexts)에서 나타납니다:
- 수습 기간 (Probation period): 매니저와 함께하는 90일 검토
- RRSP 매칭 (RRSP matching): 고용 후 90일 이후부터 시작
- 비용 청구 (Expense claims): 90일 이내에 제출
- 보안 액세스 (Security access): 승인된 권한은 90일 후 만료
여러 가지 서로 다른 "90일 규칙"이 존재하는 것입니다. 임베딩 (embeddings)은 모두 동일한 문구를 포함하고 있습니다. 의미론적 검색 (Semantic search)은 이들을 구분할 수 없습니다.

모델은 이를 상당히 합리적으로 처리했습니다. 제공된 청크 (chunks) 중에서 보안 액세스 규칙(security access rule)과 수습 기간 검토(probation review)라는 두 가지를 찾아냈고, "어떤 문맥에 대해 묻고 계신가요?"라고 물었습니다. 하지만 어떤 일이 일어났는지 생각해 보십시오.
시스템은 모든 것을 검색하고, 네 개의 서로 다른 문서에서 정보를 가져오고, 토큰 (tokens)을 소모했지만, 모델이 할 수 있는 최선은 저에게 명확한 설명을 요구하는 것이었습니다.
문서가 6개라면 괜찮습니다. 하지만 6,000개라면, 모든 모호한 질문이 모델을 무관한 문맥 (context)으로 범람하게 만듭니다. 비용은 상승하고, 지연 시간 (latency)은 늘어나며, 품질은 저하됩니다.
진단: 같은 단어, 다른 의미. 임베딩 (embeddings)이 이를 구분하지 못하고 있습니다.
핵심 통찰 (The key insight)
두 가지 실패 모두 모델의 문제는 아닙니다. 모델은 지시받은 대로 정확히 수행했습니다: 제공된 문맥 (context)에서 답변하라. 두 실패 모두 검색 (retrieval) 단계에서 발생했습니다. 좋은 소식은, 검색은 조정 가능하다는 점입니다.
또한 이 두 가지 실패는 서로 다른 두 계층 (layers)에 존재합니다.
하나는 문서를 어떻게 분할 (split) 하느냐, 즉 청킹 (chunking)에 관한 것입니다. 다른 하나는 문서를 어떻게 검색 (search) 하느냐, 즉 검색 (retrieval)에 관한 것입니다. 이 차이를 이해하는 것이 중요한데, 각 문제에 대한 해결책이 다르기 때문입니다.
해결책 1: 헤딩 기반 청킹 (Heading-based Chunking)
홈 오피스 문제는 청킹 (chunking) 문제였습니다. 제 단락 분할 방식은 섹션을 너무 짧게 잘라냈습니다. 하지만 이 온보딩 (onboarding) 문서들은 구조를 가지고 있습니다. 헤딩 (Headings), 섹션 (Sections) 말입니다.
헤딩 기반 청킹 (Heading-based chunking)은 하나의 섹션을 함께 유지합니다. 즉, 각 헤딩과 다음 헤딩이 나오기 전까지의 모든 내용이 하나의 청크 (chunk)가 됩니다. "5.3 홈 오피스 설정"과 도입 문구, 그리고 금액이 적힌 불렛 리스트 (bullet list)가 모두 하나의 덩어리가 됩니다.
import re
HEADING_RE = re.compile(r"^#{2,6}\s") # ## 및 그보다 깊은 레벨에서 분할하되, # 제목은 붙여둡니다
...
다시 같은 질문을 던져봅니다: "온보딩 중에 홈 오피스 설정 비용을 청구할 수 있나요?"

이번에는 검색된 청크 (chunk)가 전체 섹션을 포함하고 있습니다. 채용 시 $750, 연간 갱신 시 $250, 인체공학적 평가를 위해 최대 $1,200까지, 이 모든 내용이 포함되어 있습니다.

모델은 완전한 증거를 확보했으며 완전한 답변을 제공합니다.
해결책은 더 나은 모델을 사용하는 것이 아니었습니다. 더 나은 분할 (split)이었습니다.
청킹 툴킷 (Chunking toolkit): 의도에 따라 분할 방식을 선택하세요
헤딩 기반 청킹 (Heading-based chunking)이 우리의 첫 번째 실패를 해결했습니다. 하지만 이것이 만능 해결책은 아닙니다. 문서에 헤딩 (heading)이 없다면 도움이 되지 않습니다. 만약 섹션이 너무 거대하다면, 컨텍스트 윈도우 포스트에서 다루었던 "너무 많은 컨텍스트" 문제로 다시 돌아가게 됩니다.
그럼 전체 메뉴를 살펴보겠습니다. 매번 동일한 방식입니다: 어떻게 분할하는지, 그리고 언제 이 방식을 선택해야 하는지 말이죠.
고정 크기 분할 (Fixed-size splitting). 매 N 토큰마다 자릅니다. 끝입니다. 모든 사람이 시작하는 기준점 (baseline)입니다. 전사 데이터 (transcripts)나 로그 (logs)와 같이 균일한 텍스트에는 효과적입니다. 하지만 문장 중간이나 문단 중간에서 잘라버리기 때문에, 답변이 경계선에 걸려 반으로 잘려 나갈 수 있습니다. 이것이 바로 우리가 방금 해결한 바로 그 실패 사례입니다.
재귀적 분할 (Recursive splitting). 먼저 헤딩 (heading)을 기준으로 분할을 시도합니다. 만약 청크 (chunk)가 여전히 너무 크다면, 문단 (paragraph) 단위로 분할합니다. 그래도 여전히 너무 크다면? 문장 (sentence) 단위로 나눕니다. 이는 단일 규칙이 아니라 폴백 체인 (fallback chain)입니다. 대부분의 프레임워크가 이를 기본값으로 사용하며, 데이터의 형태를 아직 모를 때 사용하기 좋은 방식입니다.
의미론적 청킹 (Semantic chunking). 구조에 따라 나누는 대신, 의미에 따라 나눕니다. LangChain (SemanticChunker) 및 LlamaIndex (SemanticSplitterNodeParser)와 같은 프레임워크는 이를 단일 호출로 제공합니다. 내부적으로는 각 문장을 임베딩 (embedding)하고, 연속된 문장들을 비교하며, 유사도가 떨어지는 지점에서 자릅니다. 텍스트가 주제가 바뀌는 지점을 알려주는 방식입니다. 문장 단위로 임베딩을 수행하기 때문에 초기 비용은 더 많이 들지만, 명확한 헤딩 (heading)이 없는 밀도 높은 자료의 경우 종종 그만한 가치가 있습니다.
부모-자식 청킹 (Parent-child chunking). 청크를 두 가지 수준으로 저장합니다. 작은 청크는 검색을 정밀하게 만들고, 큰 부모 청크는 모델에게 완전한 문맥 (context)을 제공합니다. 작은 청크가 매칭되면, 그 부모 청크를 모델에게 반환합니다. 작게 검색하고, 크게 제공하십시오 (Search small, feed big). 더 많은 파이프라인 (plumbing) 작업이 필요하지만, 정밀한 검색과 완전한 문맥을 동시에 얻을 수 있습니다.
트레이드오프 (tradeoffs)를 한눈에 확인할 수 있도록 나란히 비교하면 다음과 같습니다:

보편적인 정답은 없습니다. 검색을 위한 청킹 전략에 관한 Chroma 연구에 따르면, 어떻게 청킹하느냐가 어떤 임베딩 모델을 선택하느냐만큼 중요할 수 있습니다. 두 가지 조절 요소가 비슷한 비중을 갖는 셈입니다.
대부분의 시스템을 위한 합리적인 시작점은 다음과 같습니다: 재귀적 분할 (recursive splitting), 256512 토큰 (token) 단위의 청크, 1020%의 오버랩 (overlap). 이것은 기본값일 뿐, 법칙은 아닙니다. 청킹은 튜토리얼에서 복사해 오는 설정이 아니라, 데이터의 형태에 따라 내리는 결정입니다.
그리고 당신의 선택이 제대로 작동하는지 알 수 있는 유일한 방법은 측정하는 것입니다. 예상 답변이 포함된 20~30개의 테스트 질문 세트를 만드세요. 한 가지를 변경하고, 다시 실행하고, 비교하십시오. "느낌이 더 좋다"는 지표 (metric)가 아닙니다.
해결책 2: 검색을 위한 메타데이터 필터링 (Metadata filtering for Retrieval)
"90일 규칙" 문제는 청크 크기 (chunk size)의 문제가 아닙니다. 이는 모호성 해소 (disambiguation)의 문제입니다. 임베딩 (embeddings)이 모두 동일한 문구를 포함하고 있기 때문에, 의미론적 검색 (semantic search)만으로는 이들을 구분할 수 없습니다. 하지만 우리는 검색 엔진이 모르는 유용한 정보를 알고 있습니다. 바로 각 청크가 특정 문서에서 왔다는 사실입니다.
해결책은 청크를 저장할 때 각 청크에 출처 태그를 달고, 유사도 검색 (similarity search)이 실행되기 _전_에 하나의 출처로 필터링하는 것입니다. 먼저 범위를 좁히고, 그다음에 검색하십시오. 이를 프리필터링 (pre-filtering)이라고 부릅니다.
# 데이터 주입 (ingestion) 시, 모든 청크에 출처 라벨을 찍습니다
{"text": "...", "source_label": "employee-handbook"}
{"text": "...", "source_label": "expense-policy"}
...
동일한 질문인 "90일 규칙이 무엇인가요?"를 직원 핸드북 (employee handbook)으로 필터링합니다.

이제 전체 청크가 아닌 185개 중 26개의 청크만을 검색합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기