网站怎么做域名,微信公众号h5商城网站开发,重庆建筑有限公司,网站织梦模板欢迎来到啾啾的博客#x1f431;。 记录学习点滴。分享工作思考和实用技巧#xff0c;偶尔也分享一些杂谈#x1f4ac;。 有很多很多不足的地方#xff0c;欢迎评论交流#xff0c;感谢您的阅读和评论#x1f604;。 目录 1 引言2 分块#xff1a;RAG流程反映的技术限制… 欢迎来到啾啾的博客。 记录学习点滴。分享工作思考和实用技巧偶尔也分享一些杂谈。 有很多很多不足的地方欢迎评论交流感谢您的阅读和评论。 目录 1 引言2 分块RAG流程反映的技术限制2.1 为什么我们不能直接将500页的PDF文档一次性喂给LLM2.2 块分成多大合适2.3 怎么判断分块的好坏2.3.1 评估流程 3 LangChain的文本分块处理3.1 RecursiveCharacterTextSplitter3.1.1 RecursiveCharacterTextSplitter分割文本3.1.2 RecursiveCharacterTextSplitter块重叠问题 3.2 分块中的英文处理 1 引言
在LangChain快速筑基系列中我们已经了解了RAG基本流程。
RAG的精髓就是“先查资料再回答”的流程。它把LLM从一个“凭记忆回答者”变成了一个“有能力查阅资料的专家”。 而从本篇开始我们将适当深入了解RAG具体细节。
2 分块RAG流程反映的技术限制
2.1 为什么我们不能直接将500页的PDF文档一次性喂给LLM
A最核心的原因是LLM的上下文窗口Context Window大小限制。
上下文窗口是LLM的“短期记忆”或“工作台”的大小。它限制了你 一次性 可以输入给模型作为Prompt和从模型获取作为输出的总文本量。这个量通常用 Token 来计算一个Token约等于0.75个英文单词或半个汉字。 DeepSeek文档显示其上下文限制为输出64k。 所以RAG流程需要将文本进行分块处理。 其他原因还有 提高检索精度 如果我们把一整本书作为一个“块”然后问一个关于其中某个具体细节的问题。那么“整本书”这个块的向量代表的是全书的“平均”语义。用一个非常具体的问题向量去匹配一个非常概括的“全书”向量效果会很差就像用“拿破仑在哪场战役中失败了”去匹配《欧洲通史》这本书一样匹配度很低。而如果我们把书切分成章节那么问题就更容易精确地匹配到“滑铁卢战役”那一章。 降低噪声 即使我们能把整本书都塞进上下文但书中99%的内容都与用户的问题无关。把这些无关的“噪声”一起喂给LLM会干扰它的注意力可能导致它给出不相关或质量更差的答案。只给它最相关的几段文本能让它更聚焦。
2.2 块分成多大合适
这是一个典型的工程权衡。
A太大会噪声太多降低检索精度 如果块设置的过大比如5000字符。那么块的信息将不精确、难以比较、充满“噪声”。 且在RAG的最后一步我们会把检索到的块和问题一起发给LLM。发送的文本越长消耗的Token就越多API调用费用就越高同时LLM生成答案的速度也越慢。
A太小会上下文丢失信息不全。 如果设置的过小比如50字符。那么会带来上下文丢失 (Loss of Context)的问题。 因为一个独立的句子或短语往往无法完整地表达一个意思。 例如“它主要通过分代假设来优化性能”这句话被切成了一个独立的块。 当系统检索到这个块时它本身是有意义的但LLM看到它时会非常困惑“它”是谁是JVM是G1回收器还是某个算法因为失去了上一句话的铺垫这个块的信息变得残缺不全。
通用的做法是切小块然后进行“块重叠Chunk Overlap”。 块重叠在切分时让后一个块包含前一个块末尾的一部分内容。
举个例子 假设块大小是100字符重叠大小是20字符。
块1: 从字符1到字符100。块2: 不再从字符101开始而是从字符81开始到字符180结束。块3: 从字符161开始到字符260结束。
这极大地保留了局部的上下文信息提高了RAG系统的回答质量。
2.3 怎么判断分块的好坏
Q如何判定分块所带来的影响呢是不是如果知识库中存在相关内容但 RAG 却没检索到就意味着分块没分好呢 A不一定但分块是主要因素剩下的因素还有Embedding模型与top_k参数。
我们通过两个核心指标来衡量分块的好坏
召回率 (Recall) 在所有相关的文本块中我们的检索系统成功找回来了多少精确率 (Precision) 在我们找回来的所有文本块中有多少是真正相关的
2.3.1 评估流程
我们不能凭感觉判断指标。 第一步创建测试集 我们需要创建一个小型的、高质量的评测集。 这个集合包含成对的“问题”和“期望答案原文”。 做法 打开您的一个源文档比如一个PDF选中一段话然后思考“如果我想让RAG返回这段话我应该问什么问题”。把这个问题和这段原文记录下来。重复这个过程创建几十个这样的问答对。 第二步执行“仅检索”测试 对于测试集里的每一个问题执行RAG的前半部分——检索。拿到返回的文本块列表。 第三步人工或自动评估 检查返回的文本块列表里是否包含了您期望的“答案原文”。 如果包含了 - 这次测试的 召回成功。 如果没包含 - 召回失败。 第四步诊断失败案例 诊断流程如下
找到“期望的答案原文”在分块后到底属于哪个或哪几个文本块。分析这个/这些文本块它是不是太小/太大了关键信息是不是正好被边界切断了如果分块看起来没问题那就去比较“问题向量”和“答案块向量”的相似度得分。如果得分真的很低那可能是Embedding模型的问题。检查这个“答案块”在所有块的相似度排名中到底排第几。如果排在第5而你只召回前3那就是top_k的问题。
3 LangChain的文本分块处理
3.1 RecursiveCharacterTextSplitter
RecursiveCharacterTextSplitterLangChain库提供的递归字符文本分割器。 它的工作逻辑是
它会拿到一个“分隔符”列表默认是 [“\n\n”, “\n”, , “”] (两个换行符、一个换行符、空格、空字符串)。它首先尝试用第一个分隔符 \n\n (通常代表段落) 来分割文本。分割完后它会检查每个分割出的块如果哪个块的长度仍然超过了我们设定的 chunk_size它就会对 那个过长的块使用 下一个 分隔符 \n (通常代表句子) 来进行二次分割。这个过程会 递归 地进行下去直到所有块的长度都小于 chunk_size。
这种方法的绝妙之处在于它会 优先尝试在最自然的语义边界段落、句子上进行分割只有在万不得已时才会粗暴地按空格或字符来切分。这最大限度地保留了文本的原始结构和语义完整性。
3.1.1 RecursiveCharacterTextSplitter分割文本
假设我们有以下这段关于RAG的介绍文本
text_to_split
RAG的核心思想是开卷考试。您可以把传统的LLM想象成一个“闭卷考试”它只能根据自己脑海里预先训练好的知识来回答问题。如果问它一个最新的、或者私有的信息它就会说“我不知道”。而RAG (Retrieval-Augmented Generation)就是把这个过程变成了一场“开卷考试”。在回答你的问题之前它会先去一个我们指定的“书架”也就是你的知识库上快速“检索”到最相关的几页“书”也就是文本片段然后把这些内容和你的问题一起作为参考资料交给LLM让它“阅读并总结”出答案。使用RecursiveCharacterTextSplitter进行分割
# 1. 从langchain库中导入我们需要的分割器
from langchain.text_splitter import RecursiveCharacterTextSplittertext_to_split
RAG的核心思想是开卷考试。您可以把传统的LLM想象成一个“闭卷考试”它只能根据自己脑海里预先训练好的知识来回答问题。如果问它一个最新的、或者私有的信息它就会说“我不知道”。而RAG (Retrieval-Augmented Generation)就是把这个过程变成了一场“开卷考试”。在回答你的问题之前它会先去一个我们指定的“书架”也就是你的知识库上快速“检索”到最相关的几页“书”也就是文本片段然后把这些内容和你的问题一起作为参考资料交给LLM让它“阅读并总结”出答案。
# 2. 创建一个分割器实例
# chunk_size: 每个块的最大长度字符数。这是一个核心参数。
# chunk_overlap: 块之间的重叠长度。这是防止上下文断裂的关键。
text_splitter RecursiveCharacterTextSplitter(chunk_size100, # 我们故意设置一个较小的值以便清晰地看到分割效果chunk_overlap20,length_functionlen, # 使用Python内置的len函数来计算长度is_separator_regexFalse, # 分隔符不是正则表达式
)# 3. 使用分割器来创建文本块
chunks text_splitter.create_documents([text_to_split])# 4. 打印结果看看发生了什么
for i, chunk in enumerate(chunks):print(f--- 块 {i1} ---)print(chunk.page_content) # .page_content 属性包含了块的文本内容print(f长度: {len(chunk.page_content)}\n)输出如下
--- 块 1 ---
RAG的核心思想是开卷考试。您可以把传统的LLM想象成一个“闭卷考试”它只能根据自己脑海里预先训练好的知识来回答问题。如果问它一个最新的、或者私有的信息它就会说“我不知道”。
长度: 89--- 块 2 ---
而RAG (Retrieval-Augmented
长度: 25--- 块 3 ---
Generation)就是把这个过程变成了一场“开卷考试”。在回答你的问题之前它会先去一个我们指定的“书架”也 就是你的知识库上快速“检索”到最相关的几页“书”也就是文本片段然后把这些
长度: 99--- 块 4 ---
几页“书”也就是文本片段然后把这些内容和你的问题一起作为参考资料交给LLM让它“阅读并总结”出答案 。
长度: 553.1.2 RecursiveCharacterTextSplitter块重叠问题
RecursiveCharacterTextSplitter 的默认行为是尊重语义边界这里我们设置了chunk_overlap20但是输出的结果并没有重叠部分。 因为chunk_overlap 参数只在 一个连续的文本块因为过长而被“切开”时才会在切开的两个新块之间生效。 在我们的文本中块1和块2是被两个\n\n分开段落因此他们之间不应用重叠。
如何让他们块重叠来增强上下文的联系呢 有两种主流方法
预处理文本移除天然隔离符 最简单直接的方法就是在分割之前先把那些它优先使用的分隔符主要是换行符替换成普通字符比如空格。这样在分割器看来整段文本就是一块连续的“铁板”它不得不进行“硬切”从而触发重叠逻辑。
from langchain.text_splitter import RecursiveCharacterTextSplittertext_to_split
RAG的核心思想是开卷考试。您可以把传统的LLM想象成一个“闭卷考试”它只能根据自己脑海里预先训练好的知识来回答问题。如果问它一个最新的、或者私有的信息它就会说“我不知道”。而RAG (Retrieval-Augmented Generation)就是把这个过程变成了一场“开卷考试”。在回答你的问题之前它会先去一个我们指定的“书架”也就是你的知识库上快速“检索”到最相关的几页“书”也就是文本片段然后把这些内容和你的问题一起作为参考资料交给LLM让它“阅读并总结”出答案。
# --- 核心修改在这里 ---
# 1. 预处理文本将所有换行符替换为空格
processed_text text_to_split.replace(\n, )# 2. 创建分割器实例 (参数不变)
text_splitter RecursiveCharacterTextSplitter(chunk_size100,chunk_overlap20,length_functionlen,is_separator_regexFalse,
)# 3. 使用预处理后的文本进行分割
# 注意create_documents需要一个列表所以我们传入 [processed_text]
chunks text_splitter.create_documents([processed_text])# 4. 打印结果
for i, chunk in enumerate(chunks):print(f--- 块 {i1} ---)print(chunk.page_content)print(f长度: {len(chunk.page_content)}\n)自定义分隔符列表 另一种更“高级”的方法是在创建分割器时直接告诉它不要使用默认的 [“\n\n”, “\n”, , “”] 分隔符列表而是使用一个我们自己定义的、在文本中几乎不存在的分隔符。
# --- 核心修改在这里 ---
# 1. 创建分割器时传入一个几乎不可能存在的分隔符列表
text_splitter_custom RecursiveCharacterTextSplitter(chunk_size100,chunk_overlap20,separators[|IMPOSSIBLE_SEPARATOR|] # 提供一个自定义的分隔符列表
)# 2. 直接对原始文本进行分割
chunks_custom text_splitter_custom.create_documents([text_to_split])3.2 分块中的英文处理
RecursiveCharacterTextSplitter原理上是依靠分隔符按分块大小分割而中英文书写习惯不一样英文有大量空格而中文没有因此对于中英混合的文本处理RecursiveCharacterTextSplitter有着天然的局限性。
为此我们需要在更有意义的边界上进行分割。而最自然、最有效的语义边界就是 句子。
使用基于句子边界的分割器。 利用成熟的自然语言处理NLP库如 NLTK (Natural Language Toolkit)来准确地识别句子边界然后再根据句子列表来组织我们的文本块。