31.RAG进阶(Advanced RAG)-后检索(PostRetrieval)-上下文压缩

内容参考于:图灵AI大模型全栈

上下文压缩

解决的问题,我们获取的文档有大有小,数据很长的问题,还有通过向量检索出的文档很相似,还有获取的数据很多之前都是通过topk的方式来限制获取多少条数据,这就会有问题比如说获取了8个文档数据,其中两个是与问题相关的,剩余6个都没有用,把没用的文档数据给大模型会导致回答不准确,这样也会导致Token消耗变多,响应速度变慢

上下文压缩,把检索到的文档进行压缩可以压缩单个文档内容和批量压缩,想实现这个功能LangChain给我们提供了基础检索器和文档压缩检索器来实现,先用基础检索器查询文档,查询出来后再使用压缩检索器来进行压缩

如下三个压缩检索器

特性LLMChainExtractorLLMChainFilterEmbeddingsFilter
工作原理使用LLM重写文档内容使用LLM判断文档相关性使用嵌入向量计算相似度
输出结果修改后的精炼文档是/否的文档保留决策文档相关性分数
是否修改内容✅ 修改文档内容❌ 只做过滤❌ 只做过滤
计算开销高 (调用LLM次数多)中 (每文档调用LLM)低 (向量计算)
主要用途提取关键信息过滤不相关文档相似度过滤
精度依赖LLM的理解能力LLM的判断能力嵌入模型质量

LLMChainExtractor压缩

如下图,它就是通过提示词提炼文档中的内容

LLMChainFilter压缩

它是结合问题和上下文做是否相关的判断,如果相关就返回yes,不相关就返回no

EmbeddingsFilter压缩

它是根据向量相似度的阙值来排除文档,在做向量检索的(语意、相似度匹配)时候根据问题查出的文档并不一定查出问题跟文档向量的相似度,我们在存向量数据库时比如存的是1024长度,而问题可能最多就几百个或者说只有几十个字符(可能会二十字符以内),这样的东西对比相似度可能并不是很精确,有一些向量模型超过支持的长度之后效果会更差,也就是获取了8个文档数据,其中两个是与问题相关的,剩余6个都没有用,这就导致召回率变低,我们再次进行向量匹配,在做向量的时候会得到一个分数,我们这次的向量匹配只拿高分的,比如0.6分以上,这样就能提高召回率,如下图获取0.66分以上的文档,EmbeddingsFilter就会先使用基础检索器(下图里的retriever变量)查询出固定topk个,然后获得这topk个的分数,然后根据阙值(0.66)去获取数据,也就是根据分数获取数据并不是根据topk获取

文档的分数,如下图红框通过query_similarity_score就可以看到文档的分数

在使用EmbeddingsFilter之前最好做一下去重,因为如果有重复的也浪废Token,如下图红框去重,这个去重包含了文档去重、文档与文档之间去重(意思是文档和文档之前的相似度可能会高,把高的去重只留一个)

如下图红框去重的阙值默认是0.95

代码

# ========== 导入所需工具包(每一个包的作用都给你讲明白)==========
# TextLoader:专门用来加载纯文本(.txt)文件的工具,能把txt里的内容读取成LangChain能处理的文档格式
from langchain_community.document_loaders import TextLoader
# RecursiveCharacterTextSplitter:递归字符文本分割器,用来把长文本拆成小块
# 它会优先按段落、句子、字符的顺序拆分,尽量保持语义完整,是RAG里最常用的拆分工具
from langchain_text_splitters import RecursiveCharacterTextSplitter
# HuggingFaceEmbeddings:用来加载Embedding(嵌入)模型,把文字转换成计算机能算相似度的一串数字(向量)
from langchain_huggingface import HuggingFaceEmbeddings
# Chroma:轻量级的向量数据库,专门存文本向量,支持快速查找和问题最相似的文本
from langchain_chroma import Chroma
# ChatOpenAI:OpenAI格式的大模型调用类,很多国产大模型都兼容这个格式,可以直接用
from langchain_openai import ChatOpenAI
# ContextualCompressionRetriever:上下文压缩检索器
# 作用是把「基础检索器」和「文档压缩/过滤器」组合起来:先检索出一批文档,再对文档精简/过滤
from langchain_classic.retrievers import ContextualCompressionRetriever
# 导入3种不同的文档处理工具:
# LLMChainExtractor:让大模型从文档里提取和问题相关的内容,相当于「提炼精华」
# EmbeddingsFilter:用向量相似度过滤文档,只留和问题相似度够高的
# LLMChainFilter:让大模型判断文档和问题是否相关,只保留相关的(不修改内容,只做筛选)
from langchain_classic.retrievers.document_compressors import LLMChainExtractor, EmbeddingsFilter, LLMChainFilter
# DocumentCompressorPipeline:文档压缩流水线,可以把多个处理工具按顺序串起来,依次处理文档
from langchain_classic.retrievers.document_compressors import DocumentCompressorPipeline
# EmbeddingsRedundantFilter:文档去重过滤器,去掉检索结果里内容重复、相似度高的文档
from langchain_community.document_transformers import EmbeddingsRedundantFilter
# CharacterTextSplitter:简单的字符分割器,按固定字符数拆分文本,这里导入备用
from langchain_text_splitters import CharacterTextSplitter
# os:Python内置的系统操作库,用来读取环境变量、操作文件路径
import os
# load_dotenv:用来加载.env配置文件的工具
# 把API密钥、地址等敏感信息存在.env里,不用写死在代码里,更安全
from dotenv import load_dotenv

# 执行加载.env文件的操作
# 执行完这行,就可以用os.getenv()读取.env文件里配置的环境变量了
load_dotenv()


# 格式化输出内容
# ========== 工具函数详细说明 ==========
# 函数作用:把检索到的文档列表整理成清晰好看的格式打印出来,方便人工查看结果
# 入参说明:
#   docs:必须传入,类型是「文档对象列表」,也就是检索器返回的结果
#         列表里每个元素都是LangChain的Document对象,包含page_content(文本内容)等属性
def pretty_print_docs(docs):
    # 核心逻辑:用列表推导式遍历所有文档,拼接成带编号、带分割线的字符串,最后统一打印
    # 语法逐行拆解:
    # 1. enumerate(docs):遍历文档列表,同时拿到「索引i」和「文档对象d」
    # 2. f"Document {i+1}:\n\n" + d.page_content:给每篇文档加上编号标题,i从0开始所以+1
    # 3. 列表推导式:把所有文档的字符串拼成一个新列表
    # 4. "-"*100:生成100个减号组成的长分割线
    # 5. join(...):用分割线把每篇文档的内容隔开,让打印结果更清晰不挤在一起
    print(
        f"\n{'-' * 100}\n".join(
            [f"Document {i + 1}:\n\n" + d.page_content for i, d in enumerate(docs)]
        )
    )


# ========== 加载本地文本文件(已注释,当前不用执行)==========
# 作用:读取本地的"deepseek介绍.txt"文件,转换成LangChain的Document格式
# 为什么注释掉:向量数据库已经提前建好并保存到本地了,不用每次运行都重新加载、拆分、入库
# 参数说明:
#   第一个参数:文本文件的路径,这里是相对路径,代表和代码同目录下的deepseek介绍.txt
#   encoding="utf-8":指定用utf-8编码读取,防止中文乱码
#   .load():执行加载操作,返回包含文档内容的列表
# documents = TextLoader("deepseek介绍.txt", encoding="utf-8").load()

# ========== 初始化文本分割器 ==========
# 为什么要拆分文本:向量数据库对超长文本的检索效果很差,拆成小块检索更精准,也更省资源
# 入参说明:
#   chunk_size=1024:每个文本块的最大字符数是1024,可根据需求调整,一般256~2048比较常用
#   chunk_overlap=100:相邻两个文本块之间重叠的字符数是100
#                     作用是防止拆分把一句话切断,丢失上下文语义
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1024,
    chunk_overlap=100
)
# 执行文本拆分:把加载好的长文档拆分成多个小文本块,返回拆分后的文档列表
# 为什么注释掉:和上面的加载代码配套,数据库已建好,不需要重复拆分
# texts = text_splitter.split_documents(documents)

# ========== 配置Embedding(嵌入)模型路径 ==========
# 变量作用:保存本地Embedding模型的文件夹路径
# 语法说明:路径前面加r,表示「原始字符串」
#          Windows路径里的\会被Python当成转义字符,加r之后就不会转义了
# 可以修改的地方:把路径改成你自己电脑上的Embedding模型文件夹路径就行
# 本地embedding模型地址
embedding_model_path = r'D:\huanjing\ai模型\BAAI\bge-large-zh-v1___5'

# 初始化嵌入模型对象
# 核心作用:把文本转换成计算机能计算相似度的「向量(一串数字)」
# 注意事项:文本入库和检索必须用同一个嵌入模型,不然向量标准不一样,相似度计算会出错
# 入参说明:
#   model_name:嵌入模型的名称/路径,这里传本地模型的路径
#              也可以传HuggingFace的模型名,程序会自动下载模型
# 初始化嵌入模型(用于文本向量化)
embeddings_model = HuggingFaceEmbeddings(
    model_name=embedding_model_path
)

# 变量作用:保存Chroma向量数据库的本地存储文件夹路径
# 「持久化」的意思:把向量数据存在电脑磁盘上,程序关闭后数据不会消失,下次可以直接加载用
persist_directory="./chroma_db"

# ========== 创建向量数据库(已注释,仅第一次建库时用)==========
# Chroma.from_documents:一步完成「文本转向量 + 存入向量数据库 + 保存到本地」
# 入参说明:
#   第一个参数texts:要入库的拆分后的文档列表
#   第二个参数embeddings_model:用来转向量的嵌入模型
#   persist_directory:数据库保存的本地路径
# 为什么注释掉:数据库已经创建过了,重复执行会把文档重复入库,导致检索结果重复
# chroma = Chroma.from_documents(texts, embeddings_model, persist_directory=persist_directory)

# ========== 加载已有的本地向量数据库 ==========
# 直接从本地文件夹加载已经建好的向量数据库,不用重新生成向量
# 入参说明:
#   persist_directory:本地数据库文件夹的路径,必须和建库时的路径一致
#   embedding_function:嵌入模型,必须和建库时用的是同一个模型
chroma = Chroma(
    persist_directory=persist_directory,
    embedding_function=embeddings_model
)

# 把向量数据库转换成「检索器」对象
# 检索器的作用:提供统一的查询接口,输入问题就能自动从数据库里找最相关的文档
retriever = chroma.as_retriever()

# 打印分割线,标记下面输出的是「没有经过压缩过滤的原始检索结果」
print("-------------------压缩前--------------------------")
# 调用检索器,查询和"deepseek的发展历程"相关的文档
# .invoke()是LangChain里通用的执行方法,传入查询内容,返回相关文档列表
docs = retriever.invoke("deepseek的发展历程")
# 调用格式化打印函数,把原始检索结果打印出来
pretty_print_docs(docs)

# ========== 初始化大语言模型(LLM)==========
# 这里用ChatOpenAI类调用通义千问模型,因为通义千问兼容OpenAI的API格式
# 入参说明:
#   model="qwen-plus":要调用的模型名称,这里是阿里的通义千问plus,可换成其他兼容模型
#   api_key:调用模型的API密钥,从环境变量里读取,对应.env文件里的DASHSCOPE_API_KEY
#   base_url:API接口的地址,因为不是OpenAI官方接口,所以要指定国产模型的接口地址
#           对应.env文件里的DASHSCOPE_BASE_URL
llm = ChatOpenAI(
    model="qwen-plus",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL")
)


# ========== 压缩方案1:LLMChainExtractor 内容提炼压缩(已注释,供对比测试)==========
# 作用:让大模型逐篇阅读检索到的文档,只提取和问题相关的内容,相当于给文档做摘要提炼
# 特点:会修改文档内容,只保留精华,语义精度高;但要调用LLM,速度慢、会消耗API额度
# .from_llm(llm):类的快捷创建方法,直接传入LLM对象就能生成压缩器,不用自己写提示词
# LLMChainExtractor 具体执行文档内容精炼的压缩器, 通过 LLM 对文档进行精炼
# compressor = LLMChainExtractor.from_llm(llm)
# 把「基础检索器」和「内容提炼压缩器」组合成带压缩功能的检索器
# 执行流程:用户提问 → 基础检索器召回文档 → 压缩器提炼文档内容 → 返回精简后的结果
# ContextualCompressionRetriever 将基础检索器和 压缩器结合
# compression_retriever = ContextualCompressionRetriever(
#     base_compressor=compressor, base_retriever=retriever
# )
# 打印分割线,标记是LLMChainExtractor的结果
# print("-------------------LLMChainExtractor压缩后--------------------------")
# 执行带压缩的检索查询
# compressed_docs = compression_retriever.invoke("deepseek的发展历程")
# 格式化打印结果
# pretty_print_docs(compressed_docs)



# ========== 压缩方案2:LLMChainFilter 相关性过滤(已注释,供对比测试)==========
# 作用:让大模型判断每篇文档和问题有没有关系,有关系就留下,没关系就直接删掉
# 特点:不修改文档内容,只做「留/删」的筛选;比Extractor快,但还是要调用LLM
# 和Extractor的核心区别:Extractor是从文档里抠精华,Filter是整篇文档要么留要么扔
# LLMChainFilter 具体执行文档内容过滤的压缩器, 通过 LLM 对文档进行过滤
# _filter = LLMChainFilter.from_llm(llm)
# 同样组合成带过滤功能的压缩检索器
# compression_retriever = ContextualCompressionRetriever(
#     base_compressor=_filter, base_retriever=retriever
# )
# 打印分割线
# print("-------------------LLMChainFilter压缩后--------------------------")
# 执行查询
# compressed_docs = compression_retriever.invoke("deepseek的发展历程")
# 打印结果
# pretty_print_docs(compressed_docs)


# ========== 压缩方案3:EmbeddingsFilter 向量相似度过滤(已注释,供对比测试)==========
# 作用:不用大模型,直接用向量计算问题和文档的相似度,把相似度低于阈值的文档删掉
# 特点:速度极快,不用调用LLM不花钱;但只能算字面相似度,语义理解能力不如LLM
# 入参说明:
#   embeddings=embeddings_model:指定用哪个嵌入模型来计算相似度,要和数据库的一致
#   similarity_threshold=0.71:相似度阈值,范围0~1,数值越高要求越严,留下的文档越少
# EmbeddingsFilter 具体执行文档内容过滤的压缩器, 通过检索到的文档相识度进行过滤
# embeddings_filter = EmbeddingsFilter(embeddings=embeddings_model, similarity_threshold=0.71)
# 组合成压缩检索器
# compression_retriever = ContextualCompressionRetriever(
#     base_compressor=embeddings_filter, base_retriever=retriever
# )
# 执行查询
# compressed_docs = compression_retriever.invoke("deepseek的发展历程")
# 打印分割线
# print("-------------------EmbeddingsFilter压缩后--------------------------")
# 这里原代码注释了格式化打印,直接打印原始对象,方便查看文档对象的结构
# # pretty_print_docs(compressed_docs)
# print(compressed_docs)






# ========== 压缩方案4:多步骤流水线压缩(当前正在生效的代码)==========
# 思路:把多个过滤器组合起来,按顺序处理文档,同时实现「去重 + 相关性过滤」的效果

# 1. 文档去重过滤器:EmbeddingsRedundantFilter
# 作用:对比多篇检索结果之间的相似度,把内容重复、太相似的文档删掉,只留一份
# 使用场景:检索经常会返回好几段内容差不多的文档,去重后结果更精简不冗余
# 入参embeddings:指定用哪个嵌入模型计算文档之间的相似度
# EmbeddingsRedundantFilter是文档和文档之间进行过滤的压缩器(去重)
redundant_filter = EmbeddingsRedundantFilter(embeddings=embeddings_model)

# 2. 相关性过滤器:EmbeddingsFilter
# 作用:计算用户问题和每篇文档的相似度,删掉相似度不够的文档,只留相关的
# 和上面去重的核心区别:去重是「文档和文档比」,这个是「问题和文档比」
# 入参similarity_threshold=0.66:相似度阈值,低于0.66的文档会被过滤掉
# EmbeddingsFilter是查询问题和文档的相似度进行过滤
relevant_filter = EmbeddingsFilter(embeddings=embeddings_model, similarity_threshold=0.66)

# 3. 构建处理流水线:DocumentCompressorPipeline
# 作用:把多个处理步骤按顺序串成一条流水线,文档会依次经过每个步骤处理
# 执行顺序:先经过redundant_filter去重,再经过relevant_filter做相关性过滤
# 为什么用这个顺序:先去重减少文档数量,后面过滤处理更快,整体效率更高
# 入参transformers:处理步骤的列表,列表的顺序就是执行的顺序
# DocumentCompressorPipeline 是一个用于串联多个文档处理步骤的组件,其核心作用是通过组合不同的文档转换/过滤工具,构建一个多阶段的文档压缩流水线
pipeline_compressor = DocumentCompressorPipeline(
    transformers=[redundant_filter, relevant_filter]
)

# 4. 把流水线压缩器和基础检索器组合,形成最终的压缩检索器
compression_retriever = ContextualCompressionRetriever(
    base_compressor=pipeline_compressor, base_retriever=retriever
)

# 打印分割线,标记是流水线压缩后的结果
print("-------------------DocumentCompressorPipeline压缩后--------------------------")
# 执行带流水线压缩的检索查询,传入问题
compressed_docs = compression_retriever.invoke("deepseek的发展历程")
# 格式化打印最终结果
pretty_print_docs(compressed_docs)


img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值