문맥 압축과 관련성 필터링
검색된 청크에서 관련 없는 문장을 제거하는 문맥 압축을 적용한 뒤 LLM에 전달하여 잡음을 줄이고 토큰을 절약합니다.
문맥 압축과 관련성 필터링은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
잡음이 있는 검색 청크의 문제
검색된 청크에는 흔히 관련성이 섞인 콘텐츠가 포함됩니다. 예를 들어 데이터베이스 인덱싱에 관한 500토큰 청크가 질의의 처음 두 문장에는 답할 수 있지만, 백업 절차와 관련 없는 여섯 문장을 포함할 수 있습니다. 이 전체 청크를 LLM에 보내면 토큰이 낭비되고 신호 대 잡음비가 낮아지며, 모델이 관련 문장이 아니라 관련 없는 부분을 근거로 답변을 생성할 수 있습니다.
문맥 압축이란 무엇인가요?
문맥 압축은 검색 후 단계에서 검색된 각 청크를 가져와 LLM에 전달하기 전에 질의와 관련된 문장만 추출하는 과정입니다. 원본 청크를 가장 관련성 높은 부분으로 압축하면 토큰 사용량을 줄이고 답변 품질을 높일 수 있습니다. LangChain의 ContextualCompressionRetriever는 추출을 수행하는 압축기 구성 요소로 모든 검색기를 감쌉니다.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI
# LLMChainExtractor uses an LLM to extract the relevant portion
llm = ChatOpenAI(model='gpt-4o-mini', temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever,
)
results = compression_retriever.invoke('how does HNSW indexing work?')
for doc in results:
print(len(doc.page_content), 'chars:', doc.page_content[:150])LLMChainFilter: 관련성 필터링
청크에서 문장을 추출하는 대신 LLMChainFilter는 이 청크가 질의와 관련이 있는지 여부를 이진으로 판단합니다. 관련 없는 청크는 LLM에 전달되기 전에 완전히 제거됩니다. 이는 추출보다 비용이 적게 들고(LLM 호출이 더 짧음), 청크가 짧고 일관되어 부분 추출의 효과가 크지 않을 때 유용합니다. 일반적으로 처음 검색된 청크의 20~40%가 필터링됩니다.
from langchain.retrievers.document_compressors import LLMChainFilter
filter_compressor = LLMChainFilter.from_llm(
llm=ChatOpenAI(model='gpt-4o-mini', temperature=0)
)
filtering_retriever = ContextualCompressionRetriever(
base_compressor=filter_compressor,
base_retriever=base_retriever,
)
# Fetch 10 docs, filter drops irrelevant ones
results = filtering_retriever.invoke('what are the pgvector distance operators?')
print(f'{len(results)} chunks passed the relevance filter')임베딩 기반 관련성 필터링
필터링에 LLM을 사용하면 지연 시간과 비용이 추가됩니다. 더 저렴한 대안은 임베딩 기반 필터링입니다. 이는 질의 임베딩과 각 청크의 임베딩 사이의 코사인 유사도를 계산하고, 유사도 임계값보다 낮은 청크를 제거합니다. 이 방식은 결정적이고 빠르며 무료입니다. 추가 LLM 호출 없이 검색 중 이미 계산한 임베딩을 재사용하기 때문입니다.
from langchain.retrievers.document_compressors import EmbeddingsFilter
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
embeddings_filter = EmbeddingsFilter(
embeddings=embeddings,
similarity_threshold=0.76, # cosine similarity threshold
)
embedding_retriever = ContextualCompressionRetriever(
base_compressor=embeddings_filter,
base_retriever=base_retriever,
)
results = embedding_retriever.invoke('BM25 hyperparameter tuning')
print(f'Filtered to {len(results)} relevant chunks')여러 압축기 연결하기
DocumentCompressorPipeline을 사용하면 여러 압축기를 순서대로 연결할 수 있습니다. 일반적인 방식은 먼저 빠른 임베딩 기반 필터를 적용해 명백히 관련 없는 청크를 제거하고, 긴 청크를 문장으로 나누기 위해 문장 분할을 적용한 다음, 마지막으로 LLM 추출을 적용해 가장 관련성 높은 문장을 가져오는 것입니다. 이 계층적 접근 방식은 비용과 정확도의 균형을 맞춥니다.
from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain_community.document_transformers import EmbeddingsRedundantFilter
from langchain_text_splitters import CharacterTextSplitter
# Step 1: split chunks into individual sentences
sentence_splitter = CharacterTextSplitter(
chunk_size=200,
chunk_overlap=0,
separator='. ',
)
# Step 2: remove redundant sentences via embeddings
redundant_filter = EmbeddingsRedundantFilter(embeddings=OpenAIEmbeddings())
# Step 3: keep only relevant sentences
relevance_filter = EmbeddingsFilter(embeddings=OpenAIEmbeddings(), similarity_threshold=0.76)
pipeline = DocumentCompressorPipeline(
transformers=[sentence_splitter, redundant_filter, relevance_filter]
)
piped_retriever = ContextualCompressionRetriever(
base_compressor=pipeline,
base_retriever=base_retriever,
)중복 청크 제거
검색된 여러 청크가 사실상 같은 내용을 담고 있다면, 모든 청크를 LLM에 보내도 정보가 늘지 않고 토큰만 낭비됩니다. EmbeddingsRedundantFilter는 쌍별 코사인 유사도를 계산하고 이미 선택된 청크와 지나치게 유사한 청크를 제거하여 거의 중복된 청크를 삭제합니다. 수집 과정에서 높은 중첩 설정을 사용해 겹치는 청크가 많이 생긴 말뭉치에서 특히 유용합니다.
from langchain_community.document_transformers import EmbeddingsRedundantFilter
from langchain_core.documents import Document
redundant_filter = EmbeddingsRedundantFilter(
embeddings=OpenAIEmbeddings(),
similarity_threshold=0.95, # treat docs with >95% cosine sim as duplicates
)
# Simulate near-duplicate documents
docs = [
Document(page_content='pgvector is a PostgreSQL extension for vector similarity search'),
Document(page_content='pgvector extends PostgreSQL to support vector similarity search'), # near duplicate
Document(page_content='HNSW and IVFFlat are the two index types in pgvector'),
]
filtered = redundant_filter.transform_documents(docs, query='')
print(f'Kept {len(filtered)} of {len(docs)} documents after deduplication')문장 수준 관련성 추출
관련 콘텐츠가 긴 문서 전체에 흩어져 있다면 교차 인코더를 사용한 문장 수준 추출이 가장 높은 정밀도를 제공합니다. 검색된 청크의 모든 문장에 질의와의 관련성 점수를 매기고, 임계값을 넘는 문장만 남긴 뒤 압축된 문서를 다시 구성합니다. 이 방식은 일반적으로 모든 관련 문장을 유지하면서 문맥 크기를 40~70% 줄입니다.
from sentence_transformers import CrossEncoder
import re
sentence_reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
def compress_with_cross_encoder(
query: str, chunk: str, min_score: float = 0.0
) -> str:
sentences = re.split(r'(?<=[.!?]) +', chunk)
if len(sentences) <= 1:
return chunk
pairs = [[query, s] for s in sentences]
scores = sentence_reranker.predict(pairs)
relevant = [
sentence
for sentence, score in zip(sentences, scores)
if float(score) >= min_score
]
return ' '.join(relevant) if relevant else chunk압축을 통한 토큰 예산 관리
문맥 압축은 효과적인 토큰 예산 관리 도구이기도 합니다. 제한된 문맥 창을 가진 모델에 문서 5개를 전달하려 할 때 각 문서를 500토큰에서 관련 토큰 100개로 압축하면 5배 더 많은 정보를 담을 수 있습니다. 이는 문맥 창이 더 짧고 지연 시간 요구 사항이 더 엄격한 GPT-4o-mini와 같은 작고 빠른 모델을 사용할 때 특히 유용합니다.
def compress_to_budget(
query: str,
docs: list[str],
total_token_budget: int = 2000,
tokens_per_char: float = 0.25,
) -> list[str]:
compressed = []
used_tokens = 0
for doc in docs:
compressed_doc = compress_with_cross_encoder(query, doc)
doc_tokens = int(len(compressed_doc) * tokens_per_char)
if used_tokens + doc_tokens <= total_token_budget:
compressed.append(compressed_doc)
used_tokens += doc_tokens
else:
break # stop when budget is exhausted
print(f'Using {used_tokens} tokens across {len(compressed)} docs')
return compressed압축 품질 측정
압축에는 위험이 따릅니다. 질의에 답하는 데 중요한 문장을 실수로 제거할 수 있기 때문입니다. 압축 전후의 충실도 점수를 RAGAS 또는 압축된 문맥이 여전히 올바른 답변을 뒷받침하는지 확인하는 사용자 지정 LLM 평가자를 사용해 비교하여 압축 품질을 측정하세요. 압축 후 충실도가 낮아진다면 임계값을 낮추거나 필터링 대신 추출을 사용하세요.
def measure_compression_faithfulness(query, original_docs, compressed_docs, llm):
# Use LLM to check if answer from compressed context matches
# answer from full context
def generate_answer(docs, q):
context = '\n'.join(docs)
resp = llm.invoke(f'Answer from context:\n{context}\nQ: {q}')
return resp.content
full_answer = generate_answer([d.page_content for d in original_docs], query)
comp_answer = generate_answer(compressed_docs, query)
# Check semantic similarity between answers
vecs = [embed(full_answer), embed(comp_answer)]
sim = cosine_similarity(vecs[0], vecs[1])
print(f'Answer similarity after compression: {sim:.3f}')
return sim압축을 생략해야 하는 경우
문맥 압축이 항상 유익한 것은 아닙니다. 짧고 일관된 청크(200토큰 미만)는 일반적으로 전체가 관련성이 있으므로 압축하면 지연 시간만 늘어납니다. 기술 문서에서는 절차의 모든 부분(예: 번호가 매겨진 단계 순서)에 답이 의존할 수 있으므로 개별 문장을 필터링하면 중요한 단계를 제거할 수 있습니다. 압축은 신중하게 적용하고, 항상 특정 사용 사례에서 답변 품질이 향상되는지 검증하세요.
운영 환경을 위한 압축 파이프라인
안정적인 운영 환경용 압축 파이프라인은 임베딩 기반 필터링(빠르고 저렴함), 중복 제거(같은 정보의 반복 방지), 선택적인 교차 인코더 문장 추출(정확하지만 느림)을 결합합니다. 빠른 단계를 먼저 실행하고, 가치가 높은 질의에만 또는 빠른 단계 후에도 청크가 너무 많이 남는 경우에만 비용이 큰 LLM 기반 단계를 적용하세요. 이 적응형 접근 방식은 비용과 품질을 모두 최적화합니다.
class AdaptiveCompressor:
def __init__(self, embeddings, cross_encoder=None, threshold=0.76):
self.emb_filter = EmbeddingsFilter(embeddings=embeddings,
similarity_threshold=threshold)
self.dedup = EmbeddingsRedundantFilter(embeddings=embeddings)
self.cross_encoder = cross_encoder
def compress(self, query: str, docs, use_cross_encoder: bool = False):
# Stage 1: embedding filter
docs = self.emb_filter.compress_documents(docs, query=query)
# Stage 2: deduplication
docs = self.dedup.transform_documents(docs, query=query)
# Stage 3: optional sentence-level extraction
if use_cross_encoder and self.cross_encoder:
docs = [
type(d)(page_content=compress_with_cross_encoder(
query, d.page_content
), metadata=d.metadata)
for d in docs
]
return docs빠른 확인
이 레슨에서 배운 문맥 압축을 제대로 이해했는지 확인해 보세요.
레슨 요약
이 레슨에서는 다음을 배웠습니다. 컨텍스트 압축은 검색된 청크가 LLM에 전달되기 전에 관련 없는 내용을 추출하거나 필터링하여 노이즈를 줄이고, DocumentCompressorPipeline은 여러 압축기(필터링, 중복 제거, 추출)를 순서대로 연결하며, 임베딩 기반 필터링은 대부분의 상황에서 LLM 기반 압축을 빠르고 저렴하게 대체합니다. 압축은 토큰 예산을 관리하고 답변 품질을 향상하는 데 도움이 됩니다. 다음으로 재순위 지정이 검색 품질에 미치는 실제 영향을 측정해 보겠습니다.
AI 튜터와 함께 Python을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 30
- 레슨
- 120
자주 묻는 질문
“문맥 압축과 관련성 필터링” 강의는 무료인가요?
네 — “문맥 압축과 관련성 필터링” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“문맥 압축과 관련성 필터링”에서 뭘 배우나요?
검색된 청크에서 관련 없는 문장을 제거하는 문맥 압축을 적용한 뒤 LLM에 전달하여 잡음을 줄이고 토큰을 절약합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“문맥 압축과 관련성 필터링” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.