AI Engineering Academy · 课时

上下文压缩与相关性过滤

在将检索到的分块输入 LLM 之前应用上下文压缩,去除不相关的句子,从而减少噪声并节省令牌。

第 3 / 4 课13 个步骤

上下文压缩与相关性过滤 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 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 — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
30
课程
120

常见问题解答

「上下文压缩与相关性过滤」课时是免费的吗?

是的 — 「上下文压缩与相关性过滤」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。

「上下文压缩与相关性过滤」这节课中我会学到什么?

在将检索到的分块输入 LLM 之前应用上下文压缩,去除不相关的句子,从而减少噪声并节省令牌。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Engineering Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「上下文压缩与相关性过滤」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Engineering Academy 课中编写并运行代码吗?

能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 两阶段检索为何有效
  2. 使用 Cohere 和 BGE 进行交叉编码器重排
  3. 上下文压缩与相关性过滤
  4. 衡量重排的影响
← 返回 AI Engineering Academy