문맥 압축 검색기(ContextualCompressionRetriever)

검색기

문서 검색을 위한 다양한 Retriever 패턴과 최적화 기법을 다룬다.

AI
RAG
LangChain
Agent
저자

Kwangmin Kim

공개

2024년 05월 02일

1 문제 상황: 무관한 문서의 과다 검색

RAG 시스템에서 리트리버가 과도한 수의 문서를 반환하면 할루시네이션 가능성이 증가한다. 예를 들어, 실제 질의와 관련된 부분이 5곳(k=5)에만 있는데 k=20으로 설정하면, LLM이 15곳의 무관한 내용까지 검토하게 되어 마치 함정을 설치하는 것과 같다. 이상적으로는 LLM에게 정확히 5곳만 제공해야 하지만, 질의를 미리 알 수 없으므로 이를 구현하기는 어렵다.

2 해결책: 문맥 압축 (Contextual Compression)

ContextualCompressionRetriever는 이 문제를 해결하기 위해 설계되었다. 다량의 문서(k=10~20)를 검색한 후 문맥 압축을 통해 관련성 높은 문서만 선별(k=5)하여 반환한다.

압축 방식의 종류:

  1. LLM 기반 압축: LLM을 활용하여 관련 내용만 추출
    • 장점: 할루시네이션 감소, 의미 기반 판단
    • 단점: 비용 증가, 응답 시간 증가
  2. Embedding 기반 압축: 유사도 임계값으로 문서 필터링
    • 장점: 저비용, 빠른 처리, 중복 제거 가능
    • 단점: 의미적 이해 제한

3 작동 원리

ContextualCompressionRetriever는 두 단계로 작동한다:

  1. 검색 단계: 질의를 base retriever에 전달하여 초기 문서 검색 (비효율적이지만 폭넓은 범위)
  2. 압축 단계: Document Compressor를 통해 관련성 낮은 문서 제거 또는 내용 축약

“압축”의 의미: - 개별 문서의 내용을 축약 (관련 내용만 추출) - 무관한 문서를 완전히 필터링 제거

이를 통해 LLM에 전달되는 토큰 수를 줄이면서도 관련성을 유지한다.

출처: https://drive.google.com/uc?id=1CtNgWODXZudxAWSRiWgSGEoTNrUFT98v

4 환경 설정

# API 키를 환경변수로 관리
from dotenv import load_dotenv
load_dotenv()
# LangSmith 추적 설정 (선택사항: 디버깅 및 모니터링용)
from langchain_teddynote import logging
logging.langsmith("CH10-Retriever")
# 패키지 설치 (테디노트 커스텀 압축기)
!pip install -qU langchain-teddynote

5 헬퍼 함수: 문서 출력

# 문서를 시각적으로 보기 좋게 출력하는 함수
def pretty_print_docs(docs):
    print(
        f"\n{'-' * 100}\n".join(
            [f"문서 {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]
        )
    )

6 기본 Retriever 설정

벡터 스토어 retriever를 초기화하고 문서를 검색합니다. 기본 retriever는 관련 문서와 무관한 문서를 함께 반환합니다.

from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter

# 문서 로드 및 청크 분할
loader = TextLoader("./data/appendix-keywords.txt")
text_splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=0)
texts = loader.load_and_split(text_splitter)

# 벡터 저장소 생성 및 검색기 설정
retriever = FAISS.from_documents(texts, OpenAIEmbeddings()).as_retriever()

# 검색 실행
docs = retriever.invoke("Semantic Search 에 대해서 알려줘.")
pretty_print_docs(docs)
문서 1:

Semantic Search

정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝

Embedding
----------------------------------------------------------------------------------------------------
문서 2:

정의: 키워드 검색은 사용자가 입력한 키워드를 기반으로 정보를 찾는 과정입니다. 이는 대부분의 검색 엔진과 데이터베이스 시스템에서 기본적인 검색 방식으로 사용됩니다.
예시: 사용자가 "커피숍 서울"이라고 검색하면, 관련된 커피숍 목록을 반환합니다.
연관키워드: 검색 엔진, 데이터 검색, 정보 검색

Page Rank
----------------------------------------------------------------------------------------------------
문서 3:

정의: 크롤링은 자동화된 방식으로 웹 페이지를 방문하여 데이터를 수집하는 과정입니다. 이는 검색 엔진 최적화나 데이터 분석에 자주 사용됩니다.
예시: 구글 검색 엔진이 인터넷 상의 웹사이트를 방문하여 콘텐츠를 수집하고 인덱싱하는 것이 크롤링입니다.
연관키워드: 데이터 수집, 웹 스크래핑, 검색 엔진

Word2Vec
----------------------------------------------------------------------------------------------------
문서 4:

정의: 페이지 랭크는 웹 페이지의 중요도를 평가하는 알고리즘으로, 주로 검색 엔진 결과의 순위를 결정하는 데 사용됩니다. 이는 웹 페이지 간의 링크 구조를 분석하여 평가합니다.
예시: 구글 검색 엔진은 페이지 랭크 알고리즘을 사용하여 검색 결과의 순위를 정합니다.
연관키워드: 검색 엔진 최적화, 웹 분석, 링크 분석

데이터 마이닝

결과 해석: 기본 리트리버는 쿼리와 무관한 문서(2, 3, 4번)까지 함께 반환됨. 이로 인해 LLM에 불필요한 컨텍스트가 전달되어 비용과 할루시네이션 위험 증가.

7 맥락적 압축(ContextualCompression)

LLMChainExtractor는 LLM을 사용하여 문서에서 관련 내용만 추출합니다.

from langchain_teddynote.document_compressors import LLMChainExtractor # deprecated lang chain module을 테디가 다시 커스터마이징함
from langchain.retrievers import ContextualCompressionRetriever
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(temperature=0, model="gpt-4o-mini")

# LLM을 사용하여 문서 압축기 생성
compressor = LLMChainExtractor.from_llm(llm)
# compressor = LLMChainExtractor.from_llm(llm, prompt= PromptTemplate.from_template("""주어진 문서에서 entity 들을 추출해 주세요.""")) # 이건 원본 문서를 paraphrasing하는 역할을 한다.
compression_retriever = ContextualCompressionRetriever(
    # 문서 압축기와 리트리버를 사용하여 컨텍스트 압축 리트리버 생성
    base_compressor=compressor,
    base_retriever=retriever,
)

# 일반 리트리버 vs 압축 리트리버 성능 비교

## 일반 리트리버
pretty_print_docs(retriever.invoke("Semantic Search 에 대해서 알려줘."))

print("=========================================================")
print("============== LLMChainExtractor 적용 후 ==================")

## 압축 리트리버
compressed_docs = (
    compression_retriever.invoke(  # 컨텍스트 압축 리트리버를 사용하여 관련 문서 검색
        "Semantic Search 에 대해서 알려줘."
    )
)
pretty_print_docs(compressed_docs)  # 검색된 문서를 예쁘게 출력
문서 1:

Semantic Search

정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝

Embedding
----------------------------------------------------------------------------------------------------
문서 2:

정의: 키워드 검색은 사용자가 입력한 키워드를 기반으로 정보를 찾는 과정입니다. 이는 대부분의 검색 엔진과 데이터베이스 시스템에서 기본적인 검색 방식으로 사용됩니다.
예시: 사용자가 "커피숍 서울"이라고 검색하면, 관련된 커피숍 목록을 반환합니다.
연관키워드: 검색 엔진, 데이터 검색, 정보 검색

Page Rank
----------------------------------------------------------------------------------------------------
문서 3:

정의: 크롤링은 자동화된 방식으로 웹 페이지를 방문하여 데이터를 수집하는 과정입니다. 이는 검색 엔진 최적화나 데이터 분석에 자주 사용됩니다.
예시: 구글 검색 엔진이 인터넷 상의 웹사이트를 방문하여 콘텐츠를 수집하고 인덱싱하는 것이 크롤링입니다.
연관키워드: 데이터 수집, 웹 스크래핑, 검색 엔진

Word2Vec
----------------------------------------------------------------------------------------------------
문서 4:

정의: 페이지 랭크는 웹 페이지의 중요도를 평가하는 알고리즘으로, 주로 검색 엔진 결과의 순위를 결정하는 데 사용됩니다. 이는 웹 페이지 간의 링크 구조를 분석하여 평가합니다.
예시: 구글 검색 엔진은 페이지 랭크 알고리즘을 사용하여 검색 결과의 순위를 정합니다.
연관키워드: 검색 엔진 최적화, 웹 분석, 링크 분석

데이터 마이닝
=========================================================
============== LLMChainExtractor 적용 후 ==================
문서 1:

Semantic Search

정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.

결과 해석: - 기존 리트리버: 4개 문서 모두 반환 (약 800토큰) - 압축 리트리버: 관련 문서만 추출 (약 200토큰, 75% 감소) - 효과: 토큰 사용량 감소로 비용 절감, 할루시네이션 위험 감소

8 LLMChainFilter: 문서 선택 기반 필터링

LLMChainFilter는 문서 내용을 수정하지 않고 관련 문서만 선택하여 반환합니다.

from langchain_teddynote.document_compressors import LLMChainFilter # 입력쿼리와 관련된 내용만 필터링, 문서 내용 변경은 하지않음

# LLM을 사용하여 LLMChainFilter 객체를 생성
_filter = LLMChainFilter.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(
    # LLMChainFilter와 retriever를 사용하여 ContextualCompressionRetriever 객체를 생성
    base_compressor=_filter,
    base_retriever=retriever,
)

compressed_docs = compression_retriever.invoke("Semantic Search 에 대해서 알려줘.")
pretty_print_docs(compressed_docs)
문서 1:

Semantic Search

정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝

Embedding

8.1 EmbeddingsFilter

  • 각각의 검색된 문서에 대해 추가적인 LLM 호출을 수행하는 것은 비용이 많이 들고 속도가 느리다
  • EmbeddingsFilter는 문서와 쿼리를 임베딩하고 쿼리와 충분히 유사한 임베딩을 가진 문서만 반환함으로써 더 저렴하고 빠른 옵션을 제공
  • 이를 통해 검색 결과의 관련성을 유지하면서도 계산 비용과 시간을 절약
  • EmbeddingsFilterContextualCompressionRetriever 를 사용하여 관련 문서를 압축하고 검색하는 과정
    • EmbeddingsFilter 를 사용하여 지정된 유사도 임계값(0.86) 이상인 문서를 필터링 합니다.
from langchain.retrievers.document_compressors import EmbeddingsFilter
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

embeddings_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.86)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=embeddings_filter, base_retriever=retriever
)

compressed_docs = compression_retriever.invoke("Semantic Search 에 대해서 알려줘.")
pretty_print_docs(compressed_docs)
문서 1:

Semantic Search

정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝

Embedding

9 DocumentCompressorPipeline: 다단계 압축

여러 compressor를 순차적으로 결합하여 최적화된 결과를 얻는다

파이프라인 단계: 1. TextSplitter: 문서를 작은 청크로 분할 2. EmbeddingsRedundantFilter: 중복 문서 제거 (유사도 0.95 이상) 3. EmbeddingsFilter: 관련성 필터링 (유사도 0.86 이상) 4. LLMChainExtractor: 최종 내용 추출

from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain_community.document_transformers import EmbeddingsRedundantFilter
from langchain_text_splitters import CharacterTextSplitter

# 문자 기반 텍스트 분할기를 생성하고, 청크 크기를 300으로, 청크 간 중복을 0으로 설정합니다.
splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=0)

# 임베딩을 사용하여 중복 필터를 생성: 검색된 문서 사이의 유사도 검색을 수행, 0.95도 이상의 문서들은 중복 문서라 판단하고 드랍시켜줌
redundant_filter = EmbeddingsRedundantFilter(embeddings=embeddings)

# 임베딩을 사용하여 관련성 필터를 생성하고, 유사도 임계값을 0.86으로 설정합니다.
relevant_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.86)

pipeline_compressor = DocumentCompressorPipeline(
    # 문서 압축 파이프라인을 생성하고, 분할기, 중복 필터, 관련성 필터, LLMChainExtractor를 변환기를 순서대로 설정합니다.
    # 반드시, 이 기능들을 순서대로 쓰는게 아니라 필요시 파이프라인을 추가할 수 있다는 것에 집중
    transformers=[
        splitter,
        redundant_filter,
        relevant_filter,
        LLMChainExtractor.from_llm(llm),
    ]
)

ContextualCompressionRetriever를 초기화하며, base_compressorpipeline_compressor를, base_retrieverretriever를 사용합니다.

compression_retriever = ContextualCompressionRetriever(
    # 기본 압축기로 pipeline_compressor를 사용하고, 기본 검색기로 retriever를 사용하여 ContextualCompressionRetriever를 초기화
    base_compressor=pipeline_compressor,
    base_retriever=retriever,
)

compressed_docs = compression_retriever.invoke("Semantic Search 에 대해서 알려줘.")
pretty_print_docs(compressed_docs)
문서 1:

Semantic Search

정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.

Subscribe

Enjoy this blog? Get notified of new posts by email: