본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 10. 11:08

나의 첫 RAG 시스템이 환각을 일으킨 이유 (그리고 해결 방법)

요약

초기 RAG 시스템 구축 시 발생하는 환각, 파편화, 관련성 문제를 분석하고 이를 해결하기 위한 기술적 방안을 제시합니다. 부모-자식 청킹과 하이브리드 검색을 통해 문맥 유지와 검색 정확도를 높이는 방법을 다룹니다.

핵심 포인트

  • 단순 청킹은 문맥 파편화와 환각을 유발할 수 있음
  • 부모-자식 청킹으로 검색 효율과 문맥 보존을 동시에 해결
  • 하이브리드 검색을 통해 키워드 매칭과 벡터 유사도 보완

시작은 꽤나 순수했습니다. 우리 팀이 수백 페이지에 달하는 API 레퍼런스, 온보딩 가이드, 컴플라이언스 규칙 등 방대한 내부 문서에 대해 질문할 수 있는 방법이 필요했습니다. ChatGPT는 인상적이었지만, 우리의 비공개 데이터에 대해서는 전혀 알지 못했습니다. 명확한 해답은 바로 검색 증강 생성 (RAG, Retrieval-Augmented Generation)이었습니다.

저는 관련 소문을 들었습니다. 문서를 임베딩 (embedding)하고, 벡터 데이터베이스 (vector database)에 밀어 넣은 다음, 그 위에 LLM을 얹기만 하면 짠—즉각적인 Q&A 봇이 완성된다는 것이죠. 간단해 보였습니다. 하지만 저의 첫 번째 시도는 전혀 그렇지 않았습니다.

거의 성공할 뻔했던 순진한 접근 방식

저는 text-embedding-ada-002를 가져와 문서를 512 토큰 (token) 단위의 청크 (chunk)로 나누고, 이를 Pinecone에 삽입한 뒤, GPT-3.5-turbo와 함께 간단한 LangChain 체인 (chain)을 연결했습니다. 제가 만든 괴물은 다음과 같습니다:

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Pinecone
from langchain.chains import RetrievalQA
...

이론적으로는 작동했습니다. "관리자 비밀번호를 어떻게 재설정하나요?"라고 물으면 일관된 답변이 돌아왔습니다. 하지만 균열은 빠르게 나타났습니다.

세 가지 문제 발생

첫째, 환각 (hallucinations)입니다. 창의적인 종류가 아니라 위험한 종류였습니다. 누군가 "기본 타임아웃은 무엇인가요?"라고 물었을 때, 봇은 실제 값이 5분임에도 불구하고 자신 있게 30초라고 답했습니다. 검색된 청크에 "30"이라는 숫자가 있었지만, 완전히 다른 문맥에 있었던 것입니다.

둘째, 파편화 (fragmentation)입니다. "프로덕션 인스턴스 배포"와 같은 긴 절차들이 여러 청크로 나뉘었습니다. 봇은 "배포 (deploy)"를 언급하는 청크 하나를 선택했지만, 환경 변수에 관한 단계는 놓쳐버렸습니다.

셋째, 관련성 (relevance)입니다. "속도 제한 (rate limits)"에 대한 질의는 "rate"와 "limit"라는 단어가 포함된 청크를 검색했지만, 기술 사양이 아닌 가격표에서 가져온 것이었습니다.

저는 청크 크기를 조정하고, 오버랩 (overlap)을 조절하며, 모델을 교체해 보기도 했습니다. 하지만 핵심 문제는 해결되지 않았습니다. 저의 청크들은 주변 형제 청크들에 대한 인지 없이 텍스트를 멍청하게 잘라놓은 조각들에 불과했습니다.

결국 해결책이 된 것: 부모-자식 청킹 (parent-child chunking)과 하이브리드 검색 (hybrid search)

논문들을 읽고 GitHub 이슈들을 샅샅이 뒤진 끝에, 저는 봇을 실제로 신뢰할 수 있게 만들어준 두 가지 단계의 접근 방식을 결정했습니다.

  1. 부모-자식 청킹 (Parent-child chunking) – 계층 구조를 유지합니다. 작은 "자식 (child)" 청크(예: 256 토큰)를 임베딩(embedding)하고 검색하지만, LLM에는 문맥을 위해 주변의 "부모 (parent)" 청크(예: 섹션 전체)를 전달합니다.
  2. 하이브리드 검색 (Hybrid search) – 밀집 벡터 유사도(dense vector similarity)와 희소 키워드 매칭 (BM25)을 결합하여, "timeout"과 같은 용어가 실제로 올바른 문서를 찾을 수 있도록 합니다.

다음은 제 구현의 핵심 내용입니다 (LangChain과 Weaviate를 사용했지만, 이 패턴은 특정 도구에 종속되지 않습니다):

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 우선, 문서를 더 큰 부모 청크로 분할합니다
...

이제 자식 문서들을 임베딩하되, 각 문서를 부모에 대한 참조와 함께 저장합니다. 검색(retrieval) 과정에서 상위 k개의 자식 청크를 가져온 다음, 그들의 부모 청크를 LLM에 전달합니다. 그렇게 하면 LLM은 잘려 나간 조각이 아니라 전체 섹션을 보게 됩니다.

하이브리드 검색의 경우 Weaviate의 hybrid 파라미터를 사용했지만, 앙상블 리트리버 (ensemble retriever)를 사용하여 수동으로 수행할 수도 있습니다:

from langchain.retrievers import EnsembleRetriever
from langchain.retrievers.bm25 import BM25Retriever

...

이 하이브리드 접근 방식은 환각(hallucinations)을 극적으로 줄였습니다. 키워드 구성 요소는 "admin password"와 같은 정확한 용어가 올바른 문서와 일치하도록 보장했고, 벡터 구성 요소는 의미론적 쿼리(semantic queries)를 이해했습니다.

모든 것을 하나로 통합하기

저는 또한 가장 관련성이 높은 부모 청크만이 LLM에 도달할 수 있도록 Cohere의 rerank 엔드포인트를 사용하여 간단한 재순위화 (reranking) 단계를 추가했습니다. 최종 파이프라인은 다음과 같았습니다:

  1. 사용자가 질문을 합니다.
  2. 하이브리드 검색이 자식 청크를 반환합니다 (벡터 + BM25).
  3. 해당 자식들로부터 고유한 부모 청크들을 수집합니다.
  4. 질문에 대해 부모들을 재순위화(rerank)하고, 상위 3개를 유지합니다.
  5. 이 3개의 부모와 질문을 GPT-4에 입력합니다 (네, 3.5에서 업그레이드했습니다).

그 차이는 밤과 낮처럼 극명했습니다. “기본 배포 시간(default deployment time)은 무엇인가요?”와 같은 질의는 이제 무작위 가격표 대신 “배포 타임아웃(Deployment Timeouts)”이라는 제목의 섹션을 가져왔습니다.

덧붙이자면, ai.interwestinfo.com의 서비스에서 이 과정을 자동으로 수행한다고 주장하는 유사한 접근 방식을 발견했지만, 저는 청킹 (chunking)과 검색 (retrieval) 과정을 완전히 제어하고 싶었습니다. 빠른 프로토타입 (prototype)을 제작 중이라면 이러한 도구들이 시간을 절약해 줄 수 있지만, 프로덕션 (production) 환경을 구축한다면 각 계층을 직접 소유하고 관리해야 합니다.

트레이드오프 (Trade-offs) 및 이 방식을 사용하지 말아야 할 때

부모-자식 청킹 (Parent-child chunking)은 저장 공간과 인덱싱 (indexing) 복잡도를 두 배로 늘립니다. 하이브리드 검색 (Hybrid search)은 지연 시간 (latency)을 추가합니다. 만약 문서가 이미 작고 잘 구조화되어 있다면 (예: 한 페이지 분량의 FAQ), 단순한 벡터 검색 (flat vector search)만으로도 충분할 수 있습니다. 또한, 사용자가 정확한 일치 (exact matches)를 요구하는 사실적 질문만 한다면, 키워드 검색 (keyword search)만 사용하는 것이 더 저렴하고 빠를 수 있습니다.

하지만 지저분하고 긴 형태의 내부 문서라면 어떨까요? 이 접근 방식은 제 봇이 환각 (hallucinations)이라는 엉망진창인 상황에 빠지는 것을 막아주었습니다.

다음에 다시 한다면 다르게 할 점

  • 적절한 평가 세트 (evaluation set)로 시작하기. 개선 사항을 객관적으로 측정할 수 없었기 때문에 며칠 동안 유령을 쫓듯 헤맸습니다.
  • 순위화 (reranking)를 위해 더 저렴한 모델을 사용하기 (또는 앙상블 (ensemble)만으로 충분하다면 생략하기).
  • 프로덕션 환경에서 검색 품질을 모니터링하기 — 검색된 청크 (retrieved chunks)와 실제 답변 간의 로깅 (logging)을 추가하기.

이 모든 여정을 통해 저는 RAG가 단순히 플러그 앤 플레이 (plug-and-play) 방식으로 작동하는 솔루션이 아니라는 것을 배웠습니다. 이것은 시스템 설계 (system design) 문제입니다. 임베딩 모델 (embedding model)은 단지 작은 조각일 뿐이며, 컨텍스트 (context)를 어떻게 자르고, 검색하고, 결합하느냐가 훨씬 더 중요합니다.

여러분도 “안 돼, 내 봇이 거짓말을 하고 있어”와 같은 비슷한 순간을 경험한 적이 있나요? 여러분의 청킹 (chunking) 전략은 어떤 모습인가요?

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0