1. 什么是 RAG为什么第一站是读取文本数据1.1 RAG 的核心思路给大模型配一个外挂资料库RAGRetrieval-Augmented Generation检索增强生成这两年已经成了大模型落地场景里默认出镜率最高的手法。无论是企业内部的知识库问答、客服助手还是个人搭的本地 AI 助理背后基本都离不开 RAG 这条链路。它的核心想法可以用一句话概括先检索再生成。也就是说当用户提出问题时系统不直接让大模型凭空回答而是先从你自己的资料库里检索出相关的片段把片段拼到 Prompt 里再交给大模型生成答案。这样一来大模型的回答就“有据可依”了。打个比方RAG 就像是给一个见多识广但“记忆不可靠”的专家配了一个他可以随时翻阅的专属档案柜。专家本身很聪明能讲道理、能梳理逻辑但档案柜里存放的才是你真正想让他引用的内部资料。他回答之前先去档案柜翻一通找到相关文件再结合自己的表达功底输出内容。这个“档案柜”就是知识库而“翻文件”就是检索过程。RAG 解决的核心问题是让大模型不再局限于训练时那一刀切的知识截止时间也不需要在每次新资料出现时重新训练模型。你只需要往知识库里丢新文档再问问题大模型就能引用到最新、最内部的信息。相比微调Fine-tuningRAG 的成本低得多、迭代快得多而且答案能够给出出处方便人工核对。这也是为什么几乎所有团队上大模型应用时第一个想到的架构就是 RAG。1.2 LangChain 在 RAG 里的位置把零散环节串起来的脚手架RAG 是一条完整流水线加载文档 → 拆分文本 → 向量化 → 存入向量库 → 检索 → 构造 Prompt → 调用大模型 → 输出答案。这中间任何一步你都可以自己写代码实现也能借助 LangChain 这类框架把零散环节串起来。LangChain 在 RAG 体系里扮演的是“脚手架”角色。它本身不实现大模型也不实现向量数据库但它提供了一套统一接口——不管你是用 OpenAI、通义千问还是本地跑 Ollama不管是往 Faiss、Milvus 还是 Chroma 里存向量LangChain 都能用差不多的代码把流程组织起来。对初学者来说LangChain 真正的价值不是性能更优而是把“从文本到答案”的工程链路标准化了。我在多个项目里体会最深的一点是RAG 的完整链路其实并不复杂真正让新手下不去手的反而是“入口”。一堆文档在那里怎么让程序读进来是纯文本、Word、PDF 还是网页读取之后是乱码怎么办不同格式怎么统一处理这些看似基础的问题恰恰是 RAG 落地时第一个坑。所以这篇就从“读取文本数据”这一步开始把 LangChain 处理文本的几种常用姿势讲清楚全程给可复现的代码和踩坑记录。2. 文本读取在 RAG 流水线中的地位与常见误区2.1 读取阶段的质量直接决定 RAG 的效果上限很多人上手 RAG 时习惯把注意力放在模型选择、向量库调优或者 Prompt 工程上对“读取文档”这一步往往不太重视。但实际做过项目的人都有体会乱码、空段落、错位内容、格式残留这些读取阶段的脏数据后期再努力清洗也难以完全补救。你后续做的拆分、向量化、检索全都是在读取结果的基础之上进行的。如果读取这一层就是歪的后面的流程再花哨也是白搭。读取阶段常见的问题我归纳为三类第一类是格式问题。文档里可能包含标题、表格、页眉页脚、超链接等结构信息简单粗暴地按纯文本读取会把很多“非正文内容”也混进来导致向量化时产生大量噪声。第二类是编码问题。中文文档最典型的就是 GBK、GB2312 与 UTF-8 之间的互转读取时如果编码不对出来的内容就成了“锟斤拷”这类乱码。第三类是语义割裂问题。如果直接读文本不做任何分段处理长文档会把语义句段打散后续检索时经常搜出来文不对题的内容。2.2 LangChain 读取能力全景文本加载器的分类体系LangChain 把文档读取统一抽象成了Document Loader文档加载器这个概念。它做的事情其实很直白输入是文件路径或文本内容输出是一个Document对象。这个对象里包含两个关键部分page_content是文档正文文本metadata是相关的元信息比如来源路径、页数、标题等。读到这里可能有人会觉得LangChain 不过是在文件读取外面包了一层皮。但这一层皮的真正价值在于它让后续所有环节可以统一对接。无论你的文档是 PDF、网页、Markdown 还是数据库表格加载器输出的对象结构一致后续拆分文本的组件就不需要关心文档来源了。这种统一的接口设计正是 LangChain 被广泛使用的原因之一。LangChain 的文档加载器按来源和格式可以分成几大类类别典型加载器适用场景纯文本类TextLoader、DirectoryLoader本地 txt、日志、配置文件办公文档类PyPDFLoader、Docx2txtLoaderPDF、Word 文档网页类WebBaseLoader网址内容抓取结构化数据类CSVLoader、DataFrameLoader表格类数据富格式类UnstructuredFileLoader多种格式统一解析对初学者而言最先掌握的应该是TextLoader和DirectoryLoader它们最简单、最容易排查问题而且已经能覆盖大多数 RAG 知识库初版需求。2.3 为什么先学纯文本读取而不是直接上 PDFPDF 解析一直是 RAG 项目里比较头疼的问题因为它本质上是“排版格式”而非“文本格式”。同一个 PDF 文件有的页是文字层可以提取有的是扫描件必须走 OCR有的格式复杂到直接提取会乱序。PDF 水很深一上来就啃它容易打击积极性。我建议先掌握纯文本读取原因有三第一文本文件没有格式干扰可以把注意力集中到 RAG 本身的概念理解上第二很多实际业务数据经过前置处理后最终都是以 txt、日志、导出的 Markdown 形式存在的读取纯文本是使用频率最高的能力第三纯文本读取涉及的编码、路径、批量处理问题与 PDF 场景高度重合先解决这部分后面再处理复杂格式会顺手很多。2.4 中文场景下的读取特殊性中文文本读取有一个绕不开的话题编码。同样是 txt 文件Windows 记事本默认保存可能是 ANSIGBKLinux 下创建的文件通常是 UTF-8macOS 上有些文本文件又是 UTF-16。如果程序默认按 UTF-8 读取 GBK 文件就会得到一堆乱码。LangChain 的TextLoader提供了一个encoding参数但更稳妥的做法是读取前先判断文件编码再交给加载器处理。此外中文分词的语义切分思路也与英文不同——英文按空格切词已经比较合理中文则需要专门的分词器。不过这一步属于“文本拆分”环节不在这篇的讨论范围内。我们先把读取阶段做好不要让中文编码把第一批数据搞坏就成功了一半。3. 环境准备与基础依赖安装3.1 版本选择不要盲目追新开始写代码之前先把环境整理好。我的建议是Python 3.9 以上LangChain 用带langchain-community的近期稳定版本不要用 0.0.x 的老版本。LangChain 在 0.1 时代做了大规模模块化重构把很多东西拆到了langchain-community、langchain-core等独立包里。如果教程里写的是旧版写法照抄了之后动不动就报ModuleNotFoundError原因多半就是版本错位。我当前环境用的是python --version # Python 3.10.14安装依赖的方式很简单直接 pip 装即可pip install langchain langchain-community langchain-core如果只是做文本读取其实langchain-core里的Document对象定义和langchain-community里的加载器是核心依赖langchain主包负责把它们组织起来。装完之后可以快速验证一下from langchain_community.document_loaders import TextLoader print(TextLoader)如果这行不报错说明环境基本就位。3.2 准备一份练习用例规格明确的样例数据为了把每个环节都跑通建议准备一份规格明确的样例数据。我这边在本地建了一个docs目录放了一篇产品说明文档的 txt 版本内容大概三五段主题是“智能温控器使用说明”。选这个题材没别的原因就是方便演示——有标题、有列表、有分步说明比较符合真实知识库文档的样子。mkdir -p ~/rag_demo/docs cd ~/rag_demo # 在 docs 目录下放入 sample.txt如果手头没有现成的文本直接用cat创建一份也行。关键是这份文件要够“干净”方便对照后续每一步的加载结果。4. 实操第一步用 TextLoader 读取单个文本文件4.1 TextLoader 的基本用法与参数细节TextLoader是最基础的加载器它的参数设计得很简洁。最常用的是file_path传入本地文件路径。LangChain 在读取时会把文件内容加载为字符串然后封装为Document。from langchain_community.document_loaders import TextLoader loader TextLoader(docs/sample.txt) docs loader.load() print(docs)运行结果是一串Document对象需要特别留意的是它在内部做了哪些事。默认情况下TextLoader会尝试用UTF-8编码去读文件。如果你的文件是 UTF-8 编码这一步没有任何问题如果是其他编码此时就会报错或乱码。看几个核心属性doc docs[0] print(doc.page_content) print(doc.metadata)page_content是文档正文的字符串metadata里默认包含source字段也就是文件路径。别看 metadata 就这一个字段它在后续溯源时作用很大——每一条检索出来的文本都能知道它来自哪个文件这对知识库场景几乎是刚需。4.2 实战中必须处理的编码问题如果直接按上面代码读取 GBK 编码的文件通常会出现两种情况一种是报UnicodeDecodeError另一种是加载成功但内容显示为乱码。前者反而是好事至少你知道出问题了后者比较坑因为数据已经错了但程序不报错等到检索阶段才发现数据完全不可用那时候排查成本就高了。TextLoader有一个encoding参数可以指定编码loader TextLoader(docs/sample_gbk.txt, encodinggbk)但编码这个东西靠手动猜不是长久之计。我在项目里常用的做法是先用工具检测编码再决定用什么参数加载。Python 的chardet库可以胜任这个任务import chardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(10000) return chardet.detect(raw)[encoding] file_path docs/sample_gbk.txt enc detect_encoding(file_path) print(enc) # 输出类似 GB2312 或 utf-8拿到编码后再传给TextLoader的encoding参数。这样能最大限度避免乱码。实际项目中我更推荐一个稳妥的工作流先把所有源文件统一转码为 UTF-8 文本再交给加载器。这个动作虽然多了一步但后续所有处理环节都默认 UTF-8能减少大量不确定性。TextLoader还有一个容易被忽略的autodetect_encoding参数它可以让加载器自动检测编码。老实说这个功能我在少数版本里遇到过识别不准确的情况所以我的态度是作为兜底可以但依赖它不如自己做一轮预处理。4.3 从 load 到 lazy_load大文件的内存问题TextLoader.load()会把整个文件读进内存然后一次性返回列表。如果文件只有几十 KB、几百 KB这没有任何问题。但如果文件达到几百 MB 级别一次性加载会非常占用内存甚至可能把机器卡死。LangChain 针对这个痛点提供了lazy_load方法它返回的是生成器只有在迭代到某一项时才真正加载数据。loader TextLoader(docs/large_file.txt) for doc in loader.lazy_load(): # 每次只处理一个 Document print(doc.page_content[:50])lazy_load在读取超大日志文件、历史数据导出文件时特别有用。RAG 前期把资料库做起来时文件往往成千上万如果每个文件都一次性 load内存会爆炸。我后来的项目基本都采用 lazy 方式遍历目录逐文件处理边加载边拆分边入库。这个习惯建议现在就开始养成。4.4 实操心得读取后立即做内容自检读取不是拿了Document就结束了我每次写完加载代码都会花十几秒做一圈自检先打印page_content前 200 个字符确认没有乱码再看一下总长度是否合理最后确认metadata里的路径字段是否正确。这十几秒的时间花得非常值——很多问题在加载完成的那一刻就暴露了等到入库后再发现整个链路都要重新跑一遍。5. 批量场景用 DirectoryLoader 读取整个目录5.1 为什么单个文件读取不够用真实的知识库很少只有一个文件一般是一整个目录甚至多级目录里面堆着各类文档。如果靠手动实例化多个TextLoader代码会变得冗长且难以维护。DirectoryLoader的作用就是批量化处理目录下的所有文本文件。看一个最直接的例子from langchain_community.document_loaders import DirectoryLoader loader DirectoryLoader( pathdocs, glob**/*.txt, loader_clsTextLoader, ) docs loader.load() print(f共加载了 {len(docs)} 个文档)这里几个参数拆开看。path是根目录路径glob是文件匹配模式**/*.txt表示递归匹配所有层级下的 txt 文件loader_cls指定用哪个加载器类去处理匹配到的文件。DirectoryLoader的巧妙之处在于它是“可插拔”的。只要指定不同的loader_cls就能用同一套目录遍历代码加载 PDF、Word、Markdown 等不同格式这让混合格式知识库的处理变得非常可行。5.2 给 DirectoryLoader 指定编码与参数如果你需要批量加载中文文件且源文件不是统一编码直接在DirectoryLoader里指定loader_kwargs就行loader DirectoryLoader( pathdocs, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, )这段代码的意思是所有被匹配到的 txt 文件实例化TextLoader时统一传入encodingutf-8。如果你的所有文件都已经在预处理阶段转成 UTF-8这里就比较省心如果还没有统一转码建议参照前文说的方案先做一轮转码再加载。5.3 DirectoryLoader 不能完全替代手工控制有一点值得说明DirectoryLoader虽然方便但它在一种场景下会显得不够灵活——就是当你需要按文件类型、按子目录分别配置不同加载器或不同参数时。比如你的知识库里既有 GBK 编码的旧文档又有 UTF-8 的新文档统一传encodingutf-8会在部分文件上报错。这种“混合编码”场景下我倾向于不用DirectoryLoader的默认批量方式而是自己写一个简单的遍历循环逐文件检测编码后再加载from pathlib import Path base_dir Path(docs) all_files base_dir.rglob(*.txt) docs [] for file_path in all_files: enc detect_encoding(str(file_path)) loader TextLoader(str(file_path), encodingenc) docs.extend(loader.load())代码量没增加多少但灵活性和容错能力明显提升。DirectoryLoader适合“环境整齐”的情况自定义遍历适合“实际情况复杂”的情况。先想清楚你的数据长什么样再决定用哪条路而不是看到目录加载器就直接套上去。5.4 静默失败与异常日志批量场景下的安全阀批量读取时最怕的其实是“静默失败”——某个文件读不出来但它被跳过了你浑然不知最后知识库里少了关键内容问题答案质量受损。DirectoryLoader在遇到单个文件报错时默认行为是抛出异常中断整个加载流程这种方式虽然粗暴但至少不会静默丢数据。我真正踩过的坑反而是自己写遍历循环时只写了try...except...却没有打出日志结果某个目录下的文件全部加载失败整个知识库瘫痪了大半天。后来养成的习惯是任何批量处理动作都要记录成功加载数和失败文件列表。哪怕是简单打印几行日志也能保证异常发生时第一时间知道问题出在哪。6. 读取之后为什么要立刻做文本拆分6.1 不拆分直接入库会怎样把文件读取成Document之后很多人会急着做向量化、入库。如果文档很短比如只有几句话那倒也无所谓。但只要文本一长问题立刻显现一整个文档抛给嵌入模型转成一个向量检索的时候这个大向量代表的是整篇文档的“平均语义”用户问的问题往往只涉及文档中某个局部知识点。比如产品手册里有安装步骤、有保养说明、有故障码表用户问“故障码 E5 是什么意思”如果用整篇文档的向量去召回匹配度会非常差。所以读取之后紧跟着的一步是把长文本拆成有独立语义的短段落。LangChain 提供了TextSplitter体系其中RecursiveCharacterTextSplitter是最常用的选择。6.2 RecursiveCharacterTextSplitter 的原理与参数RecursiveCharacterTextSplitter的思路通俗地讲就是先用大分隔符比如段落空行切切出来的块还是太长就降级用更小的分隔符比如句号、换行继续切直到每个 chunk 满足长度要求。它是“递归”地寻找更细粒度分割点所以对普通叙事文本的效果通常都还过得去。from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , ], ) chunks splitter.split_documents(docs) print(f拆分后得到 {len(chunks)} 个块)这里两个参数值得说道说道。chunk_size500指的是每个块的目标字符数具体取多少要根据你的嵌入模型和检索策略来定。太小了语义不完整太大了检索效果变差一般经验值在 300 到 800 之间。chunk_overlap50表示相邻块之间保留 50 个字符的重叠目的在于避免文本在切割点被“拦腰斩断”导致上下文信息丢失。举个例子如果一个关键句恰好被切断前一块末尾缺了半句、后一块开头只有半句两者单独检索时都语义残缺有了重叠至少有一块能保留相对完整的表达。6.3 按结构拆分 vs 按长度拆分RecursiveCharacterTextSplitter是按字符长度来拆分适合大多数场景。但有些文档有天然的结构比如 Markdown 的#标题、代码里的函数块这时用MarkdownHeaderTextSplitter按标题层级拆效果会比纯按长度拆好很多——因为每个块天然对应一个小节语义完整性更强。初学阶段不必急着上这些先从RecursiveCharacterTextSplitter入手摸清参数手感后再根据实际文档结构调整拆分策略。文本拆分是 RAG 链路中最需要“调参手感”的一环。不同语料、不同模型、不同检索策略合适的参数都不一样。我的建议是做一个简单的对比实验用不同chunk_size跑一遍检索把召回结果拿出来人工翻一翻哪种尺寸下回答质量好就先用哪个参数。这个流程虽然土但在项目前期远比盲目套参数靠谱。6.4 读取、拆分、入库与检索的衔接顺序严格来说LangChain 的读取load和拆分split是两个独立步骤。实际项目里我建议把它们打包成一个“预处理函数”输入文件路径输出拆分后的块列表。这样后面对接向量库时只需要循环遍历块列表做嵌入即可。到了 OpenAI或本地模型向量化、Faiss 入库等环节时使用的是统一的 Document 块对象格式的一致性会让所有后续逻辑非常顺畅。这里要重点提醒的是拆分产生的 chunk 必须保留metadata并且最好把来源路径透传下去。这样每条 chunk 在检索时都能追溯到原始文档后续做“答案附出处”的功能也好做数据排查也好都离不开这个源头信息。7. 常见问题与排查技巧实录7.1 问题一加载文件时报 UnicodeDecodeError这是新手问得最多的问题。报错核心是编码不匹配文件本身是 GBK 或其他编码但TextLoader默认用 UTF-8 解析。解决办法有两类一类是给加载器手动指定encodinggbk另一类是先用chardet检测出正确的编码再传入。注意不推荐把errorsignore之类的容错参数无脑加进去它会让文件被强行读取但乱码问题会被掩盖后期数据已经是坏的了程序却一点不报错排查起来更痛苦。为了能快速定位是哪个文件出了问题建议在批量处理时打印文件路径。7.2 问题二中文内容加载后出现乱码这种情况通常不是加载器的问题而是源文件本身的编码格式和终端或脚本环境不一致。排查思路不难先用文本编辑器或chardet确认文件实际编码再检查写入读取过程中的编码参数。另外注意在 Windows 终端直接打印中文时有时显示乱码但实际数据没问题这是终端编码的干扰与加载无关。这个状况常见又容易误判可以先存到文件里再打开确认不要盲目改代码。7.3 问题三目录加载时某些文件总是失败如果DirectoryLoader在批量加载时反复在某几个文件上报错多半是因为这几个文件的编码或格式与默认参数不匹配。解决方案是不要硬套一个全局的编码参数改为自定义遍历逻辑对单个文件做“检测编码—加载—异常捕获”三步处理。日志要尽量记录到文件级别比如“2025-01-01 10:00:00 WARN 文件 docs/old_report.txt 加载失败原因GBK 编码无法用 utf-8 解码”。这种日志对后期维护特别关键因为你不大可能记得几个月前到底哪批数据是被遗漏的。7.4 问题四匹配规则写错加载到 0 个文档glob**/*.txt写错了会静默匹配不到文件。常见错误包括忘了写**只查当前目录、扩展名写错、路径大小写问题。排查方法就是在加载之前先自己遍历一遍文件核对匹配模式。同时留意DirectoryLoader的一些参数比如use_multithreading可以在文件较多时开启多线程能显著提速。文件数量过千时多线程和懒加载的收益会非常明显。7.5 问题五文档很长时 load 卡死这其实是懒加载使用不及时导致的。load()会把全部内容载入内存超大文件下机器容易卡死。这个场景更适合lazy_load()它能按需加载。处理大量文件时我的习惯是改写成“遍历目录 懒加载 边读边拆分边的处理流程”这样能运行得很轻巧。7.6 问题六文本读取成功但检索答案质量差这个问题比较综合但经常能在读取环节找到因。一种可能是读取时混入了大量无关文本页眉页脚、广告、重复声明导致向量召回时被噪声干扰另一种可能是拆分参数不合适比如 chunk 太小让同一句话被截断、chunk 太大让一个块里包含多个主题。此时建议回到读取结果检查page_content的前后内容是否符合预期。通常很轻微的文件读取问题在 RAG 后期会被放大成严重的答案偏移所以做一次“原始数据质量检查”是所有排查步骤里优先级最高的。8. 实操总结完整的最小 RAG 读取 拆分管线把前面的内容串起来一个短文档场景下的完整代码大概长这样from pathlib import Path import chardet from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter def detect_encoding(file_path): with open(file_path, rb) as f: return chardet.detect(f.read(10000))[encoding] def load_and_split(file_path, chunk_size500, chunk_overlap50): enc detect_encoding(file_path) loader TextLoader(file_path, encodingenc) docs loader.load() splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, separators[\n\n, \n, 。, , , ], ) chunks splitter.split_documents(docs) return chunks if __name__ __main__: chunks load_and_split(docs/sample.txt) print(f拆分得到 {len(chunks)} 个块) for i, chunk in enumerate(chunks[:3]): print(f\n--- chunk {i1} ---) print(chunk.page_content[:100]) print(来源:, chunk.metadata.get(source))在这个函数基础上后续只要再补一层向量化入库就能跑起一个知识库的雏形了。这个流程本身我已经在多个试验项目里复用过最大的感受是把输入规范化之后整个后续链路的复杂度会降一个数量级。9. 我自己的实践经验与建议这一段写给即将开始做 RAG 的读者。建议不要一开始就急着把所有组件都上齐先用少量文本文件把“读取 → 拆分 → 打印结果”这一步跑顺看着输出去理解 RAG 的数据形态。数据长什么样、切出来的块是什么样只有亲眼看到才有感觉。等这一步有了判断力再到向量化、检索、最终回答的环节会顺手很多。还有一个被反复验证有效的经验给每个版本的数据集做一份“数据健康报告”统计文件数、总字符数、拆分后块数、编码分布。知识库变大的时候没有这份台账排查问题会变得非常难受。这份台账不需要复杂导出成记事本文件就行关键是持之以恒地做记录。读取文本数据这个动作看起来很简单但它是整条 RAG 链路的“地基”。地基没打好上面无论用多强的模型、多快的向量库最终效果都会打折扣。把这个基础步骤吃透后续的检索优化、Prompt 调优、知识库迭代才可能真正落地见效。第一步稳了后面就顺了。