AI Engineering Academy · Lekcja

Kompresja kontekstowa i filtrowanie trafności

Zastosuj kompresję kontekstową, aby usuwać nieistotne zdania z wyszukanych fragmentów przed przekazaniem ich do LLM, ograniczając szum i oszczędzając tokeny.

Lekcja 3 z 413 kroki

Kompresja kontekstowa i filtrowanie trafności to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Problem z zaszumionymi pobranymi fragmentami

Pobrane fragmenty często zawierają treści o mieszanej trafności. Fragment liczący 500 tokenów, dotyczący indeksowania bazy danych, może odpowiadać na dwa pierwsze zdania zapytania, ale zawierać sześć zdań niezwiązanych z tematem i dotyczących procedur tworzenia kopii zapasowych. Przesłanie całego fragmentu do LLM marnuje tokeny, obniża stosunek sygnału do szumu i może spowodować wygenerowanie odpowiedzi opartej na nieistotnej części zamiast na właściwych zdaniach.

Czym jest kompresja kontekstowa?

Kompresja kontekstowa to etap po wyszukiwaniu, który pobiera każdy znaleziony fragment i wyodrębnia tylko zdania istotne dla zapytania przed przekazaniem fragmentu do LLM. Oryginalny fragment zostaje skrócony do najbardziej istotnych części, co zmniejsza zużycie tokenów i poprawia jakość odpowiedzi. ContextualCompressionRetriever w LangChain opakowuje dowolny retriever komponentem kompresora wykonującym to wyodrębnianie.

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: filtrowanie trafności

Zamiast wyodrębniać zdania z fragmentów, LLMChainFilter podejmuje decyzję binarną: czy dany fragment jest istotny dla zapytania, czy nie? Nieistotne fragmenty są całkowicie odrzucane, zanim dotrą do LLM. Jest to tańsze niż wyodrębnianie (krótsze wywołanie LLM) i przydatne, gdy fragmenty są krótkie oraz wystarczająco spójne, aby częściowe wyodrębnianie nie przynosiło korzyści. Zazwyczaj odrzucanych jest 20–40 procent początkowo pobranych fragmentów.

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')

Filtrowanie trafności na podstawie embeddingów

Użycie LLM do filtrowania zwiększa opóźnienie i koszty. Tańszą alternatywą jest filtrowanie na podstawie embeddingów, które oblicza podobieństwo cosinusowe między embeddingiem zapytania a embeddingiem każdego fragmentu i odrzuca fragmenty poniżej określonego progu podobieństwa. Metoda ta jest deterministyczna, szybka i bezpłatna — ponownie wykorzystuje embeddingi obliczone już podczas wyszukiwania, bez dodatkowego wywołania 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')

Łączenie wielu kompresorów

Można łączyć wiele kompresorów sekwencyjnie za pomocą DocumentCompressorPipeline. Typowy schemat polega na tym, aby najpierw zastosować szybki filtr oparty na embeddingach i usunąć wyraźnie nieistotne fragmenty, następnie podzielić długie fragmenty na zdania, a na końcu zastosować wyodrębnianie przez LLM w celu pobrania najbardziej istotnych zdań. Takie warstwowe podejście zapewnia równowagę między kosztami a dokładnością.

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,
)

Usuwanie nadmiarowych fragmentów

Gdy wiele pobranych fragmentów mówi zasadniczo to samo, przesłanie ich wszystkich do LLM marnuje tokeny, nie dodając żadnych informacji. EmbeddingsRedundantFilter usuwa niemal zduplikowane fragmenty, obliczając podobieństwo cosinusowe parami i odrzucając fragmenty zbyt podobne do już wybranych. Jest to szczególnie cenne, gdy korpus zawiera wiele nakładających się fragmentów z powodu dużych wartości overlapu podczas pozyskiwania danych.

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')

Wyodrębnianie trafności na poziomie zdań

W przypadku długich dokumentów, w których istotne treści są rozproszone, wyodrębnianie na poziomie zdań za pomocą cross-encodera zapewnia najwyższą precyzję. Oceń każde zdanie w pobranym fragmencie względem zapytania, zachowaj tylko zdania powyżej określonego progu i utwórz z nich ponownie skompresowany dokument. Takie podejście zazwyczaj zmniejsza rozmiar kontekstu o 40–70 procent, zachowując wszystkie istotne zdania.

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

Zarządzanie budżetem tokenów za pomocą kompresji

Kompresja kontekstowa jest również skutecznym narzędziem zarządzania budżetem tokenów. Jeśli chcesz przekazać 5 dokumentów do modelu z ograniczonym oknem kontekstu, skompresowanie każdego z 500 tokenów do 100 istotnych tokenów pozwala zmieścić 5 razy więcej informacji. Jest to szczególnie cenne podczas pracy z mniejszymi i szybszymi modelami, takimi jak GPT-4o-mini, które mają krótsze okna kontekstu i bardziej rygorystyczne wymagania dotyczące opóźnienia.

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

Pomiar jakości kompresji

Kompresja wiąże się z ryzykiem: można przypadkowo usunąć zdanie kluczowe dla odpowiedzi na zapytanie. Jakość kompresji należy mierzyć, porównując wyniki zgodności z treścią źródłową przed kompresją i po niej, korzystając z RAGAS lub niestandardowego ewaluatora LLM, który sprawdza, czy skompresowany kontekst nadal uzasadnia poprawną odpowiedź. Jeśli po kompresji zgodność z treścią źródłową spada, obniż próg lub zamiast filtrowania wybierz wyodrębnianie.

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

Kiedy pominąć kompresję

Kompresja kontekstowa nie zawsze przynosi korzyści. W przypadku krótkich, spójnych fragmentów (poniżej 200 tokenów) zwykle cały fragment jest istotny, a kompresja jedynie zwiększa opóźnienie. W przypadku dokumentacji technicznej, w której odpowiedź zależy od wszystkich części procedury (np. sekwencji ponumerowanych kroków), filtrowanie poszczególnych zdań może usunąć kluczowe kroki. Stosuj kompresję rozważnie i zawsze sprawdzaj, czy poprawia ona jakość odpowiedzi w konkretnym przypadku użycia.

Gotowy do produkcji potok kompresji

Solidny produkcyjny potok kompresji łączy filtrowanie na podstawie embeddingów (szybkie i tanie), usuwanie nadmiarowości (aby uniknąć powtarzania tych samych informacji) oraz opcjonalne wyodrębnianie zdań za pomocą cross-encodera (dokładne, ale wolniejsze). Uruchamiaj szybkie etapy jako pierwsze, a kosztowny etap oparty na LLM stosuj tylko w przypadku cennych zapytań lub wtedy, gdy po szybkich etapach pozostaje zbyt wiele fragmentów. Takie adaptacyjne podejście optymalizuje zarówno koszty, jak i jakość.

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

Szybkie sprawdzenie

Sprawdź, czy rozumiesz kompresję kontekstową.

Podsumowanie lekcji

W tej lekcji nauczyli się Państwo, że kompresja kontekstowa redukuje szum, wyodrębniając lub filtrując nieistotne treści z pobranych fragmentów, zanim trafią one do LLM, DocumentCompressorPipeline łączy szereg kompresorów (filtrujących, usuwających duplikaty i wyodrębniających) wykonywanych sekwencyjnie, a filtrowanie oparte na embeddingach zapewnia w większości przypadków szybką i tanią alternatywę dla kompresji opartej na LLM. Kompresja pomaga zarządzać budżetem tokenów i poprawia jakość odpowiedzi. Następnie zmierzymy rzeczywisty wpływ ponownego szeregowania na jakość wyszukiwania.

Bezpłatny start

Ucz się Python dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
30
Lekcje
120

Często zadawane pytania

Czy lekcja „Kompresja kontekstowa i filtrowanie trafności” jest bezpłatna?

Tak — pełny tekst „Kompresja kontekstowa i filtrowanie trafności” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Kompresja kontekstowa i filtrowanie trafności”?

Zastosuj kompresję kontekstową, aby usuwać nieistotne zdania z wyszukanych fragmentów przed przekazaniem ich do LLM, ograniczając szum i oszczędzając tokeny. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Kompresja kontekstowa i filtrowanie trafności”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Dlaczego wyszukiwanie dwuetapowe działa
  2. Re-ranking za pomocą cross-encodera z Cohere i BGE
  3. Kompresja kontekstowa i filtrowanie trafności
  4. Pomiar wpływu re-rankingu
← Powrót do AI Engineering Academy