上下文压缩与相关性过滤
在将检索到的分块输入 LLM 之前应用上下文压缩,去除不相关的句子,从而减少噪声并节省令牌。
上下文压缩与相关性过滤 是 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 反馈 — 无需本地设置。
此课程中的所有课时
- 两阶段检索为何有效
- 使用 Cohere 和 BGE 进行交叉编码器重排
- 上下文压缩与相关性过滤
- 衡量重排的影响