RAG 파이프라인에서 검색(Retrieval) 단계가 아무리 좋은 문서를 가져와도, 그 문서 안에 불필요한 정보가 가득하다면 최종 답변의 품질은 떨어진다. 반대로, 사용자의 질문 표현이 문서와 다른 스타일이라면 애초에 검색 자체가 실패할 수도 있다.
이번 글에서는 이 두 가지 문제를 해결하는 기법을 다룬다. 검색 이후 결과를 정제하는 Contextual Compression과, 검색 이전 문서를 미리 풍부하게 만드는 Document Augmentation이다.
Contextual Compression: 검색 결과에서 핵심만 추출하기
문제 상황
벡터 검색으로 관련 문서 청크를 가져왔다고 하자. 하지만 청크 하나가 500토큰이라면, 그 안에서 실제로 질문과 관련된 부분은 한두 문장에 불과한 경우가 많다. 나머지는 배경 설명이거나 다른 주제의 내용이다.
이런 잡음(noise) 이 포함된 컨텍스트를 그대로 LLM에게 넘기면 두 가지 문제가 생긴다.
- LLM이 불필요한 정보에 혼동되어 부정확한 답변을 생성할 수 있다.
- 토큰 수가 늘어나면서 비용과 응답 시간이 증가한다.
Contextual Compression은 검색된 청크에서 질문과 직접 관련 없는 정보를 제거하고, 핵심 내용만 압축하여 LLM에게 전달하는 후처리 기법이다.
작동 방식
택배에 비유하면 이해가 쉽다. 물건(정보)이 큰 박스(청크)에 담겨 도착했는데, 실제로 필요한 건 박스 안의 작은 부품 하나뿐이다. Contextual Compression은 박스를 열어서 필요한 부품만 꺼내주는 역할을 한다.
구체적인 처리 과정은 다음과 같다.
- 일반 검색 (Initial Retrieval): 벡터 검색을 통해 사용자 질문과 관련성 높은 전체 문서 청크들을 가져온다.
- 내용 압축 (Content Compression): 검색된 청크를 최종 답변 LLM에게 바로 보내지 않고, 압축기(Compressor) 역할을 하는 별도의 LLM에게 전달한다.
- 핵심 정보 추출 (Key Information Extraction): 압축기 LLM은 원본 질문을 참고하여, 각 청크에서 질문과 직접적으로 관련된 문장이나 구절만 추출한다. 나머지 불필요한 정보는 버린다.
- 압축된 컨텍스트 전달: 잡음이 제거되고 핵심만 남은 압축된 컨텍스트를 최종 답변 생성 LLM에게 전달하여 더 정확하고 간결한 답변을 생성한다.
장점
- 정확도 향상: 최종 LLM이 불필요한 정보에 의해 혼동될 가능성이 줄어든다.
- 효율성 증대: 처리해야 할 컨텍스트의 양이 줄어들어 LLM의 처리 속도가 빨라지고 비용이 절감된다.
- 집중된 컨텍스트: 질문과 관련된 핵심 정보만 제공하므로, 더 초점이 명확한 답변을 유도할 수 있다.
구현: LangChain의 ContextualCompressionRetriever
LangChain에서는 이 패턴을 ContextualCompressionRetriever로 제공한다. 기본 retriever와 압축기를 결합하여 새로운 retriever를 구성하는 방식이다.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
# 1. 기본 retriever 생성 (벡터 검색)
base_retriever = vectorstore.as_retriever()
# 2. 압축기 생성 (LLM 기반 핵심 추출)
compressor = LLMChainExtractor.from_llm(llm)
# 3. 압축 retriever 구성
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever,
)
# 4. QA 체인에서 사용
result = compression_retriever.get_relevant_documents(query)
LLMChainExtractor가 압축기 역할을 담당한다. 검색된 각 문서에 대해 LLM을 호출하여 질문 관련 내용만 추출하므로, LLM 호출 비용이 추가로 발생한다는 점은 고려해야 한다.
Document Augmentation: 검색 전에 문서를 풍부하게 만들기
문제 상황
Contextual Compression이 검색 이후의 문제를 해결한다면, Document Augmentation은 검색 이전의 문제를 다룬다.
사용자의 질문과 문서의 표현 방식이 다를 때 검색이 실패하는 경우가 있다. 예를 들어, 문서에 "TCP 3-way handshake의 동작 원리"가 설명되어 있는데 사용자가 "서버와 클라이언트가 연결되는 과정"이라고 질문하면, 의미적으로는 같은 내용이지만 벡터 유사도가 낮게 나올 수 있다.
Document Augmentation은 문서의 각 내용에 대해 LLM으로 다양한 예상 질문(anticipated questions) 을 미리 생성하고, 이를 원본 텍스트와 함께 벡터 DB에 저장하여 검색 대상을 증강하는 기법이다.
작동 방식
도서관 사서에 비유하면 이해하기 쉽다. 책(문서)을 서가에 꽂기 전에, 사서가 "이 책에 대해 독자들이 어떤 질문을 할까?"를 미리 생각하고, 그 질문들을 카드에 적어 함께 색인해 두는 것이다.
- 문서 분할 및 질문 생성: 문서를 적당한 크기의 상위 문서(parent document) 로 나눈다. 각 상위 문서에 대해 LLM을 이용하여 사용자가 할 법한 여러 개의 예상 질문을 생성한다.
- 통합 인덱싱 (Unified Indexing): 원본 문서 조각과 생성된 모든 예상 질문들을 하나의 벡터 데이터베이스에 함께 저장한다. 이때 각 예상 질문은 자신이 어떤 원본 문서 조각에서 나왔는지 출처(메타데이터) 를 기억한다.
- 질문 기반 검색 (Question-to-Question Search): 사용자가 실제 질문을 하면, 벡터 DB에서 의미적으로 가장 유사한 대상을 찾는다. 핵심은 여기에 있다. 사용자의 질문은 원본 텍스트보다 미리 생성해 둔 예상 질문과 일치할 확률이 훨씬 높다. 질문과 질문 사이의 유사도 매칭이기 때문이다.
- 원본 문맥 참조 및 답변 생성: 가장 유사한 예상 질문을 찾으면, 그 질문이 가리키는 원본 상위 문서 전체를 최종 컨텍스트로 가져온다. 이 풍부한 컨텍스트를 바탕으로 LLM이 최종 답변을 생성한다.
장점
- 검색 강건성(Robustness) 향상: 사용자가 어떤 표현으로 질문하더라도, DB에 저장된 다양한 스타일의 예상 질문과 매칭될 확률이 높아져 검색 실패율이 크게 줄어든다.
- 고품질 컨텍스트: 최종적으로는 작은 조각이 아닌, 더 크고 풍부한 상위 문서를 컨텍스트로 사용하므로 답변의 질이 향상된다.
- HyPE와의 관계: HyPE(Hypothetical Prompt Embedding) 와 유사한 접근이지만, 검색된 대상의 부모 문맥(parent context) 을 활용한다는 점에서 한 단계 더 나아간 기법이다.
구현: FAISS + 메타데이터 활용
from langchain.schema import Document
from langchain.vectorstores import FAISS
# 1. 각 문서 단위로 LLM을 이용해 예상 질문 생성
def generate_questions(doc_text, llm):
prompt = f"다음 문서에 대해 사용자가 할 법한 질문 5개를 생성하라:\n{doc_text}"
return llm.invoke(prompt)
# 2. 원본 문서와 생성된 질문을 통합 리스트로 구성
all_documents = []
for parent_doc in parent_documents:
# 원본 문서 추가
all_documents.append(Document(
page_content=parent_doc.text,
metadata={"text": parent_doc.text}
))
# 예상 질문 추가 (메타데이터에 원본 문서 텍스트 저장)
questions = generate_questions(parent_doc.text, llm)
for q in questions:
all_documents.append(Document(
page_content=q,
metadata={"text": parent_doc.text}
))
# 3. FAISS 벡터 저장소에 통합 인덱싱
vectorstore = FAISS.from_documents(all_documents, embedding_model)
# 4. 검색 후 메타데이터에서 원본 문서 참조
results = vectorstore.similarity_search(user_query)
final_context = results[0].metadata["text"] # 원본 상위 문서
핵심은 metadata["text"]에 원본 상위 문서의 전체 텍스트를 저장하는 부분이다. 예상 질문으로 검색이 되더라도, 최종 컨텍스트로는 항상 원본의 풍부한 내용을 사용할 수 있다.
두 기법의 비교
두 기법은 RAG 파이프라인에서 서로 다른 시점에 개입한다는 점에서 근본적으로 다르다.
| 구분 | Contextual Compression | Document Augmentation |
|---|---|---|
| 적용 시점 | 검색 이후 (후처리) | 인덱싱 시점 (전처리) |
| 목적 | 불필요한 정보 제거 | 검색 대상 확장 |
| LLM 호출 시점 | 매 쿼리마다 (런타임) | 인덱싱 시 1회 (오프라인) |
| 해결하는 문제 | 컨텍스트에 잡음이 많음 | 질문-문서 간 표현 불일치 |
| 비용 특성 | 쿼리당 추가 LLM 호출 비용 | 인덱싱 시 1회 비용 |
Contextual Compression은 매 검색마다 LLM을 호출하므로 런타임 비용이 증가하지만, 항상 질문에 최적화된 컨텍스트를 제공한다. Document Augmentation은 인덱싱 시 한 번만 비용이 발생하지만, 검색 자체의 강건성을 근본적으로 개선한다.
두 기법은 상호 배타적이지 않다. Document Augmentation으로 검색 품질을 높인 뒤, Contextual Compression으로 검색 결과를 한 번 더 정제하면 파이프라인 전체의 품질을 극대화할 수 있다.
정리
RAG의 품질을 높이는 방법은 검색 알고리즘을 바꾸는 것만이 아니다. 검색 결과를 어떻게 후처리하느냐(Contextual Compression), 검색 대상을 어떻게 사전에 준비하느냐(Document Augmentation)도 큰 차이를 만든다.
- Contextual Compression: 검색된 청크에서 LLM으로 핵심만 추출하여 잡음을 제거하는 후처리 기법
- Document Augmentation: LLM으로 예상 질문을 미리 생성하여 검색 대상을 풍부하게 만드는 전처리 기법
두 기법 모두 LLM을 단순한 답변 생성 도구가 아니라, RAG 파이프라인의 품질 개선 도구로 활용한다는 공통점이 있다. 용도와 비용 특성을 고려하여 적절히 조합하면, 보다 정확하고 강건한 RAG 시스템을 구축할 수 있다.