资讯动态

LangChain文档加载与递归切分:RAG与Agent应用的关键一步

发布时间:2026/10/20 8:35:42 来源:尧图企业网站定制
最近在做大模型RAG与Agent智能体实战练习一套教程跟到第27节Debug了一整天最后发现不是模型的问题也不是提示词写得不好而是最前面的文档加载和切分环节没做好。这次把LangChain里的TextLoader和RecursiveCharacterTextSplitter单独拎出来聊透。如果你正在做知识库问答、RAG应用或者Agent工具开发大概率会遇到这样的问题文档已经喂进去了检索结果却答非所问明明原文里有答案却因为切块太碎或者太整而找不到。这篇内容就是围绕这类问题展开的从文档加载到递归切分再到与Agent工作流的衔接都会覆盖到。后面我写的代码和参数都是自己实际试过的可以直接拿去改不用再翻文档一头雾水。1. RAG开发的第一关先让文档进入程序1.1 为什么我会单独写文档加载见过不少同学包括我自己刚接触RAG时上来就找向量数据库、研究Embedding模型结果数据源头连加载都没做对。文本加载这件事看起来简单其实坑不少。一个知识库可能同时有TXT、PDF、Word、网页每种格式对应的加载器不同加载结果还会直接影响后续切分质量和检索效果。TextLoader是LangChain里最基础的纯文本加载器专门用来读取TXT、Markdown这类文本文件输出给下游的是标准的Document对象。你只有先把文件变成内存里的文本才能做分割、向量化这些操作。把加载这层弄明白后面换成PDF加载器、网页加载器时处理思路完全一样。我自己后来习惯把所有加载器封装成一个统一的函数不管输入什么格式都返回list[Document]。这样做的好处是下游切分和向量化代码不需要跟着格式变来变去。TextLoader是这套流程的起点也是最好调试的起点。1.2 TextLoader是怎么工作的以及它的坑TextLoader的用法非常直接。先看一段最常见的代码from langchain_community.document_loaders import TextLoader loader TextLoader(notes/langchain_demo.txt, encodingutf-8) docs loader.load() print(type(docs)) print(len(docs)) print(docs[0].page_content[:200]) print(docs[0].metadata)load()会返回一个列表列表里是Document对象。Document有两个核心字段page_content保存文本正文metadata保存来源信息比如source路径。TextLoader默认会把整个文件读成一个Document也就是说metadata里通常只有source没有章节、页码这些信息。这里有一个非常常见的坑如果你的文件是GBK编码而没指定encodingutf-8load()会直接抛异常。还有TextLoader只适合纯文本你拿它加载PDF会得到一堆乱码因为PDF本质上是排版格式不是纯文本。另外大文件场景下load()会把整个文件一次性读进内存文件一大会很吃内存。LangChain也提供了lazy_load()方法可以按迭代器逐条读取。虽然TextLoader一次通常只产生一个Document但对于超大的日志类文件写成这样更稳妥for doc in loader.lazy_load(): print(doc.page_content[:100])我在实际项目里遇到过一个几十MB的文本文件直接用load()时内存涨得很厉害改成lazy_load()之后处理流程就顺了很多。1.3 实际项目里别只用TextLoader常用加载器搭配如果知识库里只有TXT和MarkdownTextLoader就够用了。但真实项目往往是混合格式所以我把常用的加载器整理成了下面这张表文档格式推荐加载器需要注意的问题纯文本 / MarkdownTextLoader无结构解析需要配合分割器PDFPyPDFLoader / PyMuPDFLoader表格和图片可能丢失扫描件需要OCRWordDocx2txtLoader需要安装对应依赖旧版doc不支持HTMLBSHTMLLoader / AsyncHtmlLoader需要先提取正文过滤标签CSVCSVLoader默认一行一个Document方便做表格知识库用PyPDFLoader加载PDF时代码和TextLoader很像from langchain_community.document_loaders import PyPDFLoader loader PyPDFLoader(docs/product_manual.pdf) docs loader.load() print(len(docs)) print(docs[0].page_content[:200])PDF加载器返回的是多个Document通常一页一个。这样metadata里会带上页码后续切分如果跨页需要自己决定是保留页码还是按页切。我的经验是页和页之间的语义常常是连续的所以不要死板地按页切最好先用页切分器把PDF变成多个Document再交给文本分割器做精细切分。Word文档里的表格同样容易丢。如果知识库大量依赖表格数据建议不要依赖通用加载器而是单独把表格抽取出来每行作为一个知识单元。这一步看起来繁琐但对后续检索的准确率影响特别大。2. 文档分割器想清楚切多碎再动手切2.1 不切和狠切的代价很多人觉得切文档就是把大文本拆成小片段越简单越好实际上切分粒度直接决定检索质量。如果你把一个十万字的说明书整体送入向量库单条向量包含的信息太杂用户问其中一个小功能的用法时整篇文档和这句问话的相似度会被大量无关内容稀释检索到的片段太粗模型很难从中抽出准确答案。反过来如果你一股脑把文本按每50个字切语义就会被拦腰打断。拿人的阅读来类比一段话说的是一个完整观点突然在中间断开你读起来也很费劲。模型读检索回来的块也一样它只看到半句话自然无法给出完整回答。所以分割器的目标是在语义完整性和单块尺寸之间做平衡。不是切得越碎越好也不是保留越大越好而是要让每个切块尽量对应一个相对完整的观点、描述或事件。2.2 分割器选型别一上来就盲选LangChain里的文档分割器不少下面几个属于最常用的CharacterTextSplitter按固定字符数硬切参数简单但完全不考虑句子边界很容易切断正文。RecursiveCharacterTextSplitter按分隔符列表递归切分优先保持段落和句子完整是大多数人首选的默认方案。TokenTextSplitter按token数量切分使用BPE tokenizer计数适合对LLM上下文长度有精确要求的场景。MarkdownHeaderTextSplitter / HTMLHeaderTextSplitter按文档标题结构切分适合Markdown文档或带目录的网页。语义分割器基于Embedding计算相邻句子的相似度来分段效果更智能但需要额外计算成本配置也更重。如果你刚开始做RAG不要急着研究语义分割器先用RecursiveCharacterTextSplitter把流程跑通。大部分场景下调好分隔符和重叠窗口后效果已经能满足需求。用语义分割器属于后续优化方向成本高收益不一定值。2.3 RecursiveCharacterTextSplitter的递归逻辑RecursiveCharacterTextSplitter是这类分割器里的代表组件。它接收一个分隔符列表默认是[\n\n, \n, , ]也就是“段落、换行、空格、字符”的优先级顺序。它的工作方式可以理解为先把整个文本按第一个分隔符“\n\n”切成若干段。如果某一段仍然超过chunk_size就继续用第二个分隔符“\n”去切这一段。如果还超再按空格切。最后如果连空格都切不了就干脆按字符硬切。这个设计的巧妙之处在于它尽力尊重原文结构而不是无脑按固定字符数断句。它先把文本拆成自然段落段落太长才拆句子句子太长才拆词汇。这样的递归逻辑让切出来的块在大多数情况下保持语义相对完整。我经常把这个过程比作吃羊腿先沿关节拆关节拆不开再动刀而不是一上来就乱砍。理解了这一层你遇到分隔符调优问题时就能猜到问题出在哪个优先级上了。3. RecursiveCharacterTextSplitter实战从加载到切块的完整流程3.1 直接抄的代码加载、切分、打印下面这段代码是我在项目里的标准起步写法把文本加载和递归切分串在一起from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader TextLoader(docs/knowledge_base.txt, encodingutf-8) docs loader.load() splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap60, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) chunks splitter.split_documents(docs) print(f加载文档数: {len(docs)}) print(f切分后块数: {len(chunks)}) for i, chunk in enumerate(chunks[:5]): print(f### chunk {i}) print(chunk.page_content) print(metadata: , chunk.metadata)这里我用的是split_documents而不是split_text。split_text只接受字符串并返回字符串列表会把Document的metadata丢光。在RAG场景里metadata记录着来源文件、页码、章节这些关键信息如果丢失后面做引用溯源时会非常痛苦。split_documents会保留metadata所以只要输入是Document列表就尽量用这个方法。如果环境里没装langchain_text_splitters也可以从langchain.text_splitter导入RecursiveCharacterTextSplitter效果一样只是不同版本的包路径不同运行时留意一下报错提示就行。3.2 chunk_size和chunk_overlap到底怎么设这是新手问得最多的问题。先说结论没有万能参数只有适合当前语料的参数。我的习惯是这样起步场景chunk_sizechunk_overlap说明普通知识库问答400-60050-100块不太大检索精准长文档技术手册700-1000100-200尽量保留完整段落逻辑对话式Agent工具300-50030-80要求快速返回别追求大块语义连贯性要求高600-800150-200overlap增大减少上下文断层chunk_overlap的作用是让相邻块之间保留一部分重叠内容避免在切分边界处丢失上下文。比如chunk_size400overlap60意思是第1块结尾的60个字会在第2块开头重复出现。这能显著缓解边界语义断裂的问题。但要特别注意overlap不是越大越好。重叠过多会让相邻块内容大量重复向量库里存储噪声增加检索时可能返回多个几乎相同的块浪费上下文窗口。如果你更关心的是token数量而不是字符数可以使用from_tiktoken_encoderfrom langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter.from_tiktoken_encoder( model_namegpt-4, chunk_size500, chunk_overlap50, )这样切分器会用tiktoken计算token数而不是用len函数数多少个字。英文文档效果很好。中文场景下一个汉字可能对应一个或多个token使用token计数能更精确地控制送入大模型的上下文长度。不过也要说清楚token计数会多一步编码计算速度比len慢一点。如果语料不大用len按字符数控制也足够。3.3 中文文档的separators调优这一步很容易被忽略RecursiveCharacterTextSplitter的默认separators是[\n\n, \n, , ]。这套分隔符对英文友好因为英文单词之间天然有空格。但中文文本里没有空格如果直接使用默认参数断句时大概率会从句子中间切或者等段落很长时只能退到空字符串一级把句子从中硬生生切成两半。我处理中文文档时会重新指定separatorsseparators [\n\n, \n, 。, , , , , , ]这个顺序很关键。它表示优先按段落拆段落太长再按换行拆然后按句号、叹号、问号、分号、逗号逐级切分。也就是说切分器会尽量在完整句子的标点处断开语义完整度会明显提升。很多人容易犯的错误是把分割符列表写反比如写成[, 。, \n\n]。空字符串放在第一位会让切分器一开始就直接按字符切割所有块都会变成单个字符完全丧失语义。无论什么时候空字符串都要放在最后它是兜底选项不是优先选项。我自己调中文参数时还会根据文本风格增加顿号、冒号这些标点。只要记住一个大原则分隔符列表的顺序是从“大块到小块”段落优先于句子句子优先于词汇最后一格一定是空字符串。4. 分割器那些坑从报错到效果差的排查实录4.1 区分split_text和split_documents别再搞混这是我在社区答疑时遇到频率最高的误区。split_text输入字符串输出字符串列表split_documents输入Document列表输出Document列表并且会保留metadata。如果你只需要测试切分效果用split_text足够。但一旦进入知识库流程需要记录来源、构建引用链就一定要用split_documents。否则后面生成回答时你想告诉用户“这个结论来自哪份文档的哪一页”会发现根本拿不到信息只能重做一遍。一个简单的自查方法打印chunks[0].metadata如果为空或没有source字段说明你用了错误的方法或者Doc本身没带metadata。4.2 我踩过的几个坑和出路第一个坑metadata丢失。早期我用split_text切分后喂给向量库检索时发现回来的块完全没有来源信息紧急补救了很久。后来统一改成split_documents问题才解决。第二个坑大文件撑爆内存。遇到一个超大文本日志文件loader.load()直接把内存占满程序卡死。解决思路是先用lazy_load()逐条读取或者先按章节拆分成多个小文件再分别加载切分。第三个坑表格被拆得稀烂。知识库里有一份产品参数表用普通文本切分后一个单元格里的内容被拆到多个chunk检索时字段值对不上。后来我把表格单独抽出来每一行做成一个Document问题就消失了。第四个坑分隔符顺序写反。我见过不少人的separators里第一项是空字符串切出来全是乱码级碎片。这类问题其实看分割结果就能发现如果切出的块长度普遍特别小基本就是分隔符顺序出错了。第五个坑overlap设成0。有些问答场景里答案刚好跨越两个相邻块的边界由于没有重叠检索时任何一个块都不够完整模型就没法给出正确答案。适度增加overlap可以缓解这类问题但也不能加太多。4.3 一个可复用的分割质量检查方法切完之后别急着向量化我建议先做一个快速检查for i, chunk in enumerate(chunks[:20]): text chunk.page_content print(fchunk {i}: len{len(text)}) print(text) print(---)重点看三件事第一是否出现了半个句子或半个完整语义第二是否存在大量长度远小于chunk_size的碎片块第三相邻块之间重叠的部分是否自然。如果发现大量碎片块可以把separators里的低频分隔符去掉或者调大chunk_size。如果发现边界处频繁切断关键语义就增加chunk_overlap。做完抽样检查后我再做一轮真实检索测试。选5个有代表性的人工问题每个问题从库里检索Top-K肉眼看一下返回块是否包含答案信息。如果答案内容恰好被切成两块说明overlap太小如果返回块里杂音太多说明chunk_size过大或者文档结构本身需要按标题切。这一步花不了多少时间但对最终RAG效果的影响非常大。很多项目上线后效果不行回头排查往往就卡在这里。4.4 常见问题速查表问题现象可能原因解决方案中文块从句子中间切开默认separators不含中文标点添加“。”等分隔符切出大量超短碎片块分隔符顺序错误或分隔符过碎调整优先级顺序删掉低频分隔符metadata消失用了split_text而不是split_documents改为split_documents大文件加载卡死load()一次性读入内存不足使用lazy_load()或先按章节拆分检索结果答非所问chunk_size过大或overlap过小调小chunk_size适当增加overlap表格数据检索结果错乱表格被文本切分器拆散单独提取表格每行作为Documenttoken超过模型上限用len计字符但没控制token数改用from_tiktoken_encoder或按token切分5. 切好的块会流向哪里文本分割在Agent工作流里的位置5.1 RAG链路中的文本块生命周期完整的RAG链路是加载文档分割文本向量化存储到向量库检索组装提示词给大模型最后生成回答。文本分割处在加载和向量化之间但它影响的是后面所有环节。可以说切好的每一个chunk就是向量化与检索的“最小颗粒度”。向量库的一个条目本质上是一个chunk的向量表示检索引擎比较用户问句和每条向量的相似度返回相似的chunk。在Agent工作流里Agent会调用知识库工具完成检索工具内部执行相似性搜索返回几个chunk作为上下文。这些chunk会被塞进提示词和用户问题一起交给模型。如果chunk本身不完整提示词上下文就是残缺的模型能力再强也补不出原文档里没有的信息。所以提示词工程很重要但它建立在检索质量之上。检索回来的材料本身就是“半句话”提示词设计得再花哨也白搭。很多开发者把精力全部放在提示词上忽略了更前面的文本切分方向就跑偏了。5.2 Agent场景下的分割策略调整Agent和普通RAG有一点不同Agent可能为了完成一个任务多次调用知识库工具比如先查文档目录再查某个章节再查详细参数。这意味着每一次检索都要尽量准确因为Agent的下一步决策依赖于返回的内容。在这种场景下我给每个chunk的metadata里额外写入文档名、章节号、段落序号这样Agent在引用时可以知道自己查的是哪部分内容。如果同一份文档多次命中还可以通过metadata中的段落序号把多个chunk按原文顺序重新拼回一段更完整的上下文再交给模型。另外当Agent面对不同类型的用户问题时可以注册多个检索工具。比如短问题用chunk_size较小的工具快速命中关键词答案长文档综合分析则用chunk_size较大的工具保留更完整上下文。这种动态切换思路比固定参数来得灵活后续调优空间也更大。不过我不建议一上来就搞复杂。先把一个检索工具跑通再逐步拆分成多个工具每加一个工具都要验证它对整体任务是否有真实增益不然Agent的工具调用链路会变得又长又难定位问题。5.3 给新手的几个实在建议不要把教程里的参数当成圣旨。文档结构变语言变甚至用户提问习惯变分割参数都要跟着调整。我自己的习惯是拿到一批新语料后先用默认参数跑一遍打印前20个chunk人工读一遍。这个过程能发现大部分问题。其次把加载和切分操作封装成独立函数。后续换加载器、换切分策略时只改一个函数就行不用动向量化和检索逻辑。我前几个项目就是没有这样做每次换文档格式都要翻半天旧代码后来统一封装后省了很多事。最后做RAG和Agent项目时不要指望一次跑通就没有问题。文本切分没有标准答案只有适合当前数据的最优解。快速迭代、多观察切分结果、用真实检索验证这个流程比背参数可靠得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑