AI Engineering Academy · Lektion

Kontekstuel komprimering og relevansfiltrering

Anvend kontekstuel komprimering til at fjerne irrelevante sætninger fra fundne chunks, før De sender dem til LLM'en, så støj reduceres, og der spares tokens.

Lektion 3 af 413 trin

Kontekstuel komprimering og relevansfiltrering er en gratis AI Engineering Academy-lektion på CoddyKit. Dette er lektion 3 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i AI Engineering Academy, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. AI Engineering Academy-kurset indeholder 4 lektioner i alt.

Problemet med støjende hentede tekststykker

Hentede tekststykker indeholder ofte indhold med blandet relevans. Et tekststykke på 500 tokens om databaseindeksering kan besvare de første to sætninger i forespørgslen, men samtidig indeholde seks sætninger med uvedkommende materiale om sikkerhedskopiering. Hvis du sender hele tekststykket til LLM'en, spilder du tokens, reducerer signal-støj-forholdet og kan få modellen til at generere et svar, der bygger på den irrelevante del i stedet for de relevante sætninger.

Hvad er kontekstuel komprimering?

Kontekstuel komprimering er et trin efter søgningen, der tager hvert hentet tekststykke og udtrækker kun de sætninger, der er relevante for forespørgslen, før tekststykket sendes til LLM'en. Det oprindelige tekststykke komprimeres til de mest relevante dele, hvilket reducerer tokenforbruget og forbedrer kvaliteten af svaret. LangChains ContextualCompressionRetriever indpakker enhver retriever med en kompressorkomponent, der udfører denne udtrækning.

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: Relevansfiltrering

I stedet for at udtrække sætninger fra tekststykker træffer LLMChainFilter en binær afgørelse: Er dette tekststykke relevant for forespørgslen eller ej? Irrelevante tekststykker fjernes helt, før de når frem til LLM'en. Det er billigere end udtrækning (et kortere LLM-kald) og nyttigt, når tekststykkerne er korte og sammenhængende nok til, at delvis udtrækning ikke hjælper. Typisk filtreres 20-40 procent af de oprindeligt hentede tekststykker fra.

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

Relevansfiltrering baseret på embeddings

Brug af en LLM til filtrering medfører ekstra latenstid og omkostninger. Et billigere alternativ er filtrering baseret på embeddings, hvor cosinusligheden mellem forespørgslens embedding og hvert tekststykkes embedding beregnes, og tekststykker under en lighedstærskel fjernes. Det er deterministisk, hurtigt og gratis — det genbruger embeddings, der allerede blev beregnet under søgningen, uden et ekstra LLM-kald.

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

Kædning af flere kompressorer

Du kan kæde flere kompressorer sekventielt ved hjælp af DocumentCompressorPipeline. Et almindeligt mønster er først at anvende et hurtigt embeddingsbaseret filter for at fjerne tydeligt irrelevante tekststykker, derefter anvende sætningsopdeling for at dele lange tekststykker op i sætninger og til sidst anvende LLM-udtrækning for at hente de mest relevante sætninger. Denne trinvise tilgang afvejer omkostninger og præcision.

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

Fjernelse af overflødige tekststykker

Når flere hentede tekststykker grundlæggende siger det samme, spilder du tokens ved at sende dem alle til LLM'en uden at tilføje information. EmbeddingsRedundantFilter fjerner næsten identiske tekststykker ved at beregne parvise cosinusligheder og fjerne tekststykker, der ligner allerede valgte tekststykker for meget. Det er især værdifuldt, når dit korpus har mange overlappende tekststykker på grund af høje overlapindstillinger under indlæsningen.

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

Relevansudtræk på sætningsniveau

For lange dokumenter, hvor relevant indhold er spredt ud, giver udtræk på sætningsniveau med en cross-encoder den højeste præcision. Beregn en score for hver sætning i det hentede tekststykke mod forespørgslen, behold kun sætninger over en tærskel, og opbyg et komprimeret dokument igen. Denne tilgang reducerer typisk kontekststørrelsen med 40-70 procent, samtidig med at alle relevante sætninger bevares.

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

Håndtering af tokenbudget via komprimering

Kontekstuel komprimering er også et effektivt værktøj til håndtering af tokenbudgettet. Hvis du vil sende 5 dokumenter til en model med et begrænset kontekstvindue, kan du få plads til 5 gange mere information ved at komprimere hvert dokument fra 500 tokens til 100 relevante tokens. Det er især værdifuldt, når du arbejder med mindre og hurtigere modeller som GPT-4o-mini, der har kortere kontekstvinduer og strengere krav til latenstid.

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

Måling af komprimeringskvalitet

Komprimering medfører en risiko: Du kan ved et uheld fjerne en sætning, der er afgørende for at besvare forespørgslen. Mål komprimeringskvaliteten ved at sammenligne troværdighedsscorer før og efter komprimering ved hjælp af RAGAS eller en tilpasset LLM-bedømmer, der kontrollerer, om den komprimerede kontekst stadig understøtter det korrekte svar. Hvis troværdigheden falder efter komprimering, skal du sænke tærsklen eller skifte til udtrækning i stedet for filtrering.

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

Hvornår skal komprimering springes over?

Kontekstuel komprimering er ikke altid en fordel. For korte, sammenhængende tekststykker (under 200 tokens) er hele tekststykket normalt relevant, og komprimering tilføjer kun latenstid. I teknisk dokumentation, hvor svaret afhænger af alle dele af en procedure (f.eks. en nummereret rækkefølge af trin), kan filtrering af enkelte sætninger fjerne afgørende trin. Anvend komprimering med omtanke, og kontrollér altid, at den forbedrer kvaliteten af svaret i netop dit anvendelsestilfælde.

Komprimeringspipeline klar til produktion

En robust komprimeringspipeline til produktion kombinerer filtrering baseret på embeddings (hurtig og billig), fjernelse af redundans (undgå gentagelse af den samme information) og valgfri sætningsudtrækning med en cross-encoder (præcis, men langsommere). Kør de hurtige trin først, og anvend kun det dyre LLM-baserede trin på forespørgsler med høj værdi, eller når de hurtige trin efterlader for mange tekststykker. Denne adaptive tilgang optimerer både omkostninger og kvalitet.

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

Hurtigt tjek

Test din forståelse af kontekstuel komprimering ud fra denne lektion.

Opsummering af lektionen

I denne lektion lærte du: kontekstuel komprimering reducerer støj ved at udtrække eller filtrere irrelevant indhold fra de hentede tekststykker, før de når LLM'en, DocumentCompressorPipeline kæder flere komprimeringsmoduler (filtrering, deduplikering, udtræk) sammen i rækkefølge, og filtrering baseret på embeddings er et hurtigt og billigt alternativ til LLM-baseret komprimering i de fleste scenarier. Komprimering hjælper med at styre tokenbudgetter og forbedrer kvaliteten af svarene. Nu måler vi den faktiske effekt af omrangering på informationshentningens kvalitet.

Gratis at komme i gang

Lær Python med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
30
Lektioner
120

Ofte stillede spørgsmål

Er lektionen “Kontekstuel komprimering og relevansfiltrering” gratis?

Ja — hele teksten til “Kontekstuel komprimering og relevansfiltrering” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af AI Engineering Academy-kurset, skal du opgradere til CoddyKit PRO. AI Engineering Academy-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Kontekstuel komprimering og relevansfiltrering”?

Anvend kontekstuel komprimering til at fjerne irrelevante sætninger fra fundne chunks, før De sender dem til LLM'en, så støj reduceres, og der spares tokens. Du øver dig i AI Engineering Academy med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på AI Engineering Academy?

Der kræves ingen tidligere erfaring. AI Engineering Academy på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 3 af 4.

Hvor lang tid tager lektionen “Kontekstuel komprimering og relevansfiltrering”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne AI Engineering Academy-lektion?

Ja. Alle AI Engineering Academy-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Hvorfor søgning i to trin virker
  2. Cross-encoder-reranking med Cohere og BGE
  3. Kontekstuel komprimering og relevansfiltrering
  4. Måling af effekten af reranking
← Tilbage til AI Engineering Academy