内容参考于:图灵AI大模型全栈
上下文压缩
解决的问题,我们获取的文档有大有小,数据很长的问题,还有通过向量检索出的文档很相似,还有获取的数据很多之前都是通过topk的方式来限制获取多少条数据,这就会有问题比如说获取了8个文档数据,其中两个是与问题相关的,剩余6个都没有用,把没用的文档数据给大模型会导致回答不准确,这样也会导致Token消耗变多,响应速度变慢
上下文压缩,把检索到的文档进行压缩可以压缩单个文档内容和批量压缩,想实现这个功能LangChain给我们提供了基础检索器和文档压缩检索器来实现,先用基础检索器查询文档,查询出来后再使用压缩检索器来进行压缩
如下三个压缩检索器
特性 LLMChainExtractor LLMChainFilter EmbeddingsFilter 工作原理 使用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)


-上下文压缩&spm=1001.2101.3001.5002&articleId=162140332&d=1&t=3&u=7dbbe18445974ee1a7bc54d1d301193a)
4532

被折叠的 条评论
为什么被折叠?



