LangChain框架-数据检索
经或多或少知道了大模型存在的缺陷数据不实时缺少垂直领域数据和私域数据等。解决这些缺陷的主要方法是通过检索增强生成RAG。首先检索外部数据然后在执行生成步骤时将其传递给LLM。LangChain为RAG应用程序提供了从简单到复杂的所有构建块数据检索Retrieval模块包括与检索步骤相关的所有内容例如数据的获取、切分、向量化、向量存储、向量检索等模块。1.Document loaders 文档加载模块在上面的图中已经清晰的知道第一步是必须有数据源第二步就是读取数据源LangChain为开发者封装了很多文档加载模块例如 pdf、csv、json、word、md格式文件我们先看一下如何加载1.加载本地pdf、word文件LangChain中加载pdf使用的是pypdf需要先安装一下我们在前面其实也读取过pdf不过用的是原生的方式如果要做那种可以更精细化的处理例如读取pdf中的表格行用原生的比较好pip install pypdfpdf加载from langchain_community.document_loaders import PyPDFLoader loader PyPDFLoader(物理知识点.pdf) print(loader) page loader.load() print(page[0].page_content)word加载from langchain_community.document_loaders import UnstructuredWordDocumentLoader # 指定要加载的Word文档路径 loader UnstructuredWordDocumentLoader(语文.docx) print(loader) # 加载文档并分割成段落或元素 documents loader.load() print(documents) # 输出加载的内容 for doc in documents: print(doc.page_content)2.文档切分模块上一步我们已经对已有的数据源进行加载当然也不局限于本地文件其实数据源可以来源于很多途径数据库查询接口获取甚至用于直接通过接口调用写入的都归属于数据加载这一环节那么文档切分模块就是对已经加载的数据源进行切分然后向量化那么为什么要切分呢简单说分割就是为了检索精度和信息完整性之间找一个平衡点如果不进行分割直接把所有数据作为检索单元会存在一些问题1.把整个源数据作为转换为向量那么整个向量的语义就会非常宽泛2.大语言模型的上下文窗口是有限的处理太长的文本会增加计算成本3.将整个数据作为上下文相当于把答案藏在大海模型需要从里面筛选信息无异于大海捞针而且容易被一些不相关的噪声干扰导致质量下降当然分割也不是越细越好如果仅仅按照固定字符强行切断很容易破坏数据完整性例如:1.上下文丢失例如原文是“我是小余。我喜欢吃西瓜”如果被切分成“我是小余”和 ”我喜欢吃西瓜“2个块当用户问“小余喜欢吃什么“的时候或者包含”我喜欢吃西瓜”的时候可能就检索不到。2.破坏结构对于表格、列表、操作步骤这一些结构化的的内容分割不好会将他们拆散导致查出来的信息缺胳膊少腿用都没法用。所以一些做的比较好的检索会采用一些更智能的分割方法例如按语义分割在段落、章节这些自然的边界上进行切分保证从语义上相对完整。什么是语义简单来说就是归类例如当一辆无人驾驶的汽车通过摄像头看到前方的景像有汽车道路、行人、建筑语义分割就是所有道路的像素都被标记为灰色。所有汽车的像素都被标记为蓝色。所有行人的像素都被标记为红色。所有建筑的像素都被标记为紫色这样汽车就不再是看一张复杂的图片而是得到了一张清晰的指令图灰色区域可以行驶红色区域必须避让同理文档也是一样核心就是分类。重叠分割在相邻的块之间保留一部分重叠来让上下文衔接增加连贯性这也是用的比较多的一种直接上图。在LangChain中框架也提供了许多不同类型的文本切分器分割器类型核心逻辑适用场景爽点坑点CharacterTextSplitter数着字数切不管内容啥意思凑够指定字数就咔嚓一刀。简单的纯文本或者对语义没啥要求的场景。快不需要加载任何模型简单直接。傻很容易把一句完整的话从中间切断导致上下文不连贯。RecursiveCharacterTextSplitter层层递进地切先试着按段落切切不开再按换行切再切不开按句号切...直到切小为止。最推荐。适合绝大多数长文档、文章、书籍。聪明尽量保证不把句子切断保留了语义完整性。需要稍微花点心思调整分隔符的优先级。TokenTextSplitter按模型“词汇量”切按 Token 数量切而不是按字数。因为大模型是按 Token 收费和计数的。需要严格控制成本或者怕超出模型上下文限制比如 4K/8K 限制时。精准能完美匹配大模型的输入限制不会超支。计算稍微复杂一点点其实也就是一瞬间的事。Language 专属分割器按代码语法切懂编程语言的逻辑按函数、类来切而不是瞎切。处理源代码Python, JS, Java 等。专业能把完整的函数或类打包在一起不会把代码切碎。挑食只支持特定的编程语言普通文本用不了。SemanticChunker按意思切利用 AI 模型判断两句话“像不像”意思变了就切一刀。对检索质量要求极高的场景比如复杂的知识库问答。高质量切出来的每一段都是一个完整的意思检索效果最好。贵且慢因为要调用 AI 模型去理解全文费钱又费时。MarkdownHeaderTextSplitter按标题切专门识别 Markdown 的#号把每个章节单独切出来。结构化文档比如 GitHub 的 README 文件、技术文档。有结构能保留文档的层级关系知道这段话属于哪个标题。挑食只认 Markdown 格式普通文本用不了。首选万金油方案就是使用RecursiveCharacterTextSplitter是LangChain默认推荐的因为它在速度和语义之间取得了平衡。建议配置chunk_size1000, chunk_overlap200 不过具体数值视模型上下文而定。如果还有比较特殊格式处理像代码库问答那必须用 Language作为分割器.例如我用cursor写c#他就是基于Tree-sitter来进行语法分块然后向量化当用户询问某个函数的逻辑他会把匹配的分块结果自行交给大模型推导语义无需 Roslyn 这类重型的编译器介入不过这里不要误解Tree-sitter号称支持50语言的语法分析。这里再扩展一下语义和语法的区别这个之前学习roslyn时里面提到过会解析语法树好多api的意思之前我也有点懵逼现在搞清白了语法就是长什么样而语义就是代表“代码实际在做什么”下面来用RecursiveCharacterTextSplitter来进行分档分割他有几个重点参数1.chunk_size每个切块的token数量2.chunk_overlap相邻2个块重复的token数就是重叠分割的意思重叠几个字符看上面的图就能明白假设一个数字一个token 。# 导入 PDF 加载器模块 from langchain_community.document_loaders import PyPDFLoader # 导入递归字符文本分割器模块 from langchain_text_splitters import RecursiveCharacterTextSplitter # 1. 加载 PDF 文档 # 使用 PyPDFLoader 初始化加载器指定文件路径为 财务管理文档.pdf loader PyPDFLoader(葵花宝典完整版.pdf) # 调用 load_and_split() 方法加载文档并按页进行初步分割返回一个包含 Document 对象的列表 pages loader.load_and_split() # 2. 初始化文本分割器 # 创建 RecursiveCharacterTextSplitter 实例用于将长文本切分成小块 text_splitter RecursiveCharacterTextSplitter( chunk_size200, # 每个文本块的最大字符数设置为 200 chunk_overlap100, # 文本块之间的重叠字符数设置为 100以保持上下文连贯 length_functionlen # 使用内置的 len 函数来计算文本长度按字符数计算 ) # 3. 数据清洗与提取 # 遍历 pages 列表提取每页的 page_content 内容并使用 replace 方法去除所有的换行符(\n)和空格 # 如果 pages 不为空则执行列表推导式否则 data 被赋值为空列表 [] data [page.page_content.replace(\n,).replace( ,) for page in pages ] if pages else [] # 4. 执行文本切分 # 使用 text_splitter 将清洗后的纯文本列表 (data) 转换为 Document 对象列表 result text_splitter.create_documents(data) # 5. 打印结果 print(result) # 打印整个 result 列表查看对象概览 # 6. 遍历并输出每个文本块的详细信息 for page in result: # 打印每个文本块的具体内容和其对应的字符长度 print(page.page_content, len(page.page_content))这个网站可以可视化展示文本如何分割3.文本向量化模型封装我们已经介绍了对源数据进行切割那么这一环就是对我们已经切割的文档进行向量化LangChain对文本向量化模型的接口做了封装OpenAI, Cohere, Hugging Face等。向量化模型的封装提供了两种接口一种针对文档的向量化embed_documents一种针对句子的向量化embed_query。我们还是用听得懂的来理解抽象了一个接口2个方法多态OpenAI, Cohere, Hugging Face至于使用哪一种就需要自己来选型了学习演示就使用百炼的和Huggingface如果不熟悉Huggingface的这里说一下你就理解为大模型界的github就可以了。百炼向量模型和本地bge-large-zh-v1.5import os from dotenv import load_dotenv from langchain_community.embeddings import DashScopeEmbeddings from langchain_huggingface import HuggingFaceEmbeddings data [ 元旦1月1日周四至3日周六放假调休共3天。1月4日周日上班。, 春节2月15日农历腊月二十八、周日至23日农历正月初七、周一放假调休共9天。2月14日周六、2月28日周六上班, 清明节4月4日周六至6日周一放假共3天。, 劳动节5月1日周五至5日周二放假调休共5天。5月9日周六上班。, 端午节6月19日周五至21日周日放假共3天。 ] # 加载 .env 文件中的环境变量到系统环境中从环境变量中获取名为 QW_KEY 的 API 密钥 load_dotenv() api_key os.getenv(QW_KEY) # 初始化 DashScope 的 Embeddings 模型传入 API 密钥并指定模型为 text-embedding-v3 bl_embeddings_model DashScopeEmbeddings(dashscope_api_keyapi_key, modeltext-embedding-v3) # 调用模型的 embed_documents 方法将一组文档字符串列表批量转换为向量Embeddings bl_embeddings bl_.embed_documents(data) # 打印向量列表的总长度 print(len(bl_embeddings ), len(bl_embeddings [0]), len(bl_embeddings [1])) # 将用户的提问Query也转换为相同维度的向量以便进行相似度匹配。 bl_query_text 劳动节放假几天? bl_query_embedding bl_embeddings_model .embed_query(query_text) #------------------------------------------------------- #本地bge-large-zh-v1.5 #------------------------------------------------------- 指定本地 HuggingFace 模型的路径,配置编码参数将生成的嵌入向量进行归一化处理归一化后的向量在计算余弦相似度时会更准确、更高效 model_name /Users/yuxl/3.Resources/Demo/llm/embdding/bge-large-zhv15/BAAI/bge-large-zh-v1___5 encode_kwargs {normalize_embeddings: True} hf_embeddings_model HuggingFaceEmbeddings( model_namemodel_name, encode_kwargsencode_kwargs ) hf_embeddings hf_embeddings_model .embed_documents(data) # 将用户的提问Query也转换为相同维度的向量以便进行相似度匹配。 hf_query_text 劳动节放假几天? hf_query_embedding hf_embeddings_model .embed_query(query_text)4.向量存储文档加载、切割、向量化已经完成那么接下来就需要把向量存起来了如何存储呢使用向量数据库常用的向量数据库有Chroma、Milvus、FAISS、Qdrant在本地练习使用Chroma甚至如果你是中小规模 RAG 应用、个人项目都可以使用它因为Chroma 的设计非常符合 Python的开发习惯支持内存模式和持久化模式几行代码即可完成向量存储与检索参考下面的例子import os from dotenv import load_dotenv from langchain_community.vectorstores import Chroma from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # 1.加载 .env 文件中的环境变量 load_dotenv() # 1. 加载并拆分 PDF 文档 loader PyPDFLoader(葵花宝典.pdf) pages loader.load_and_split() # 2. 初始化文本分割器设置块大小与重叠字符数 text_splitter RecursiveCharacterTextSplitter( chunk_size200, chunk_overlap100, length_functionlen, add_start_indexTrue ) # 清洗文本数据去除换行符和空格并分割为独立的段落文档 data [page.page_content.replace(\n, ).replace( , ) for page in pages] if pages else [] paragraphs text_splitter.create_documents(data) # 3. 初始化阿里云 DashScope 向量嵌入模型 embeddings DashScopeEmbeddings(dashscope_api_keyos.getenv(QW_KEY), modeltext-embedding-v2) # 将处理好的段落转化为向量并存入 Chroma 本地向量数据库 db Chroma.from_documents(paragraphs, embeddings) # 4. 定义查询问题并在向量库中进行相似度检索 query 欲练此功的下一句是什么 docs db.similarity_search(query,k 2) # 打印检索到的相关文档内容 for doc in docs: print(doc)5.Retrievers 检索器先看下面这一段代码上面的就是直接在向量数据库查询但是需要把问题向量化再去搜索但是用检索器这里语法上就不一样然后也不需要写代码把问题向量化# 定义查询问题并在向量库中进行相似度检索 query 欲练此功的下一句是什么 docs db.similarity_search(query,k 2) #实例化一个检索器 retriever db.as_retriever(search_kwargs{k: 1}) docs retriever.get_relevant_documents(欲练此功的下一句是什么)其实乍一看还是很懵的什么是检索器他和直接向量查询有什么关系后面看了些资料用自己理解的大白话来搞明白检索器Retriever和直接查询向量数据库Vector Store到底有什么区别1.向量数据库Vector Store就像一个巨大的仓库。里面堆满了各种零件 数据向量它只负责把零件存好而且能精准地告诉你某个零件在哪个货架上哪个柜子里头。2.检索器Retriever就像这个仓库里的管理员。我们不需要知道零件在哪个货架只需要把的需求我想组装一台电脑告诉他他就会跑进仓库帮你把最合适的零件挑出来打包好直接交出来。可以看出核心区别就是职责不同一个是仓库一个是仓库管理员向量数据库它的核心能力就是存向量和做相似度计算。它只能搜自己库里有的东西。但是检索器非常通用不仅仅限于向量数据库。除了到向量库里头捞数据还可以去维基百科搜、去谷歌搜、甚至去公司的 SQL 数据库里查表格。还有就是数据库通常是给多少拿多少。你设了找5条它就很死板地把最像的5条给你但是检索器如果加上一些策略他会帮你筛选掉重复的挑出比较代表性内容多样的文档。生产中的主流检索器类型1.向量检索器 (Vector Retriever / Semantic Search)基于语义相似度。它不纠结字面是否完全一样而是理解意思。比如你搜么样修电脑它能找到计算机故障排除说明书2.关键词检索器 (Keyword / Sparse Retriever)基于传统的文本匹配。严格匹配你输入的词是否在文档中出现过。特点就是非常精准特别适合查找专有名词、产品型号、订单号或特定的术语但理解不了语义。