当前位置: 首页 > news >正文

知识入库:从文档加载到文本拆分

上一篇,我们摊开了 RAG 的全景蓝图。你知道了要让 AI 能够“翻书”回答问题,得先把私有文档变成可检索的知识碎片。从这篇开始,我们就撸起袖子,一铲一铲地挖地基。

整个 RAG 大厦的第一层,就是把散落在各个角落的静态文件——PDF、TXT、Markdown、网页——统一装载、精细切分,变成一堆规格统一、大小适中的“知识砖块”。这一步做好了,后面的嵌入和检索才能稳如磐石。


一、Document 对象:LangChain 世界里的“通用集装箱”

在开始装载之前,你得先认识 LangChain 为所有文本内容设计的统一容器——Document对象。无论原始数据是一篇公众号文章、一份 PDF 手册还是一段网页 HTML,经过 Loader 处理后,都会被装进这个标准集装箱里。

1.1 Document 的简单结构

fromlangchain_core.documentsimportDocument doc=Document(page_content="今天天气真好,适合出门散步。",metadata={"source":"diary.txt","date":"2026-05-12"})print(doc.page_content)# 实际文本内容print(doc.metadata)# 附加信息字典
  • page_content:装载文档的实际文本,这是后续嵌入和检索的主角。
  • metadata:一个字典,用来记录来源文件、页码、作者、创建日期等辅助信息。它在检索时不会被直接搜索,但可以在最终回答中附上出处,实现可追溯性。

这个统一容器,让后续所有处理(拆分、嵌入、存储)都能用一套逻辑应对千差万别的原始数据。


二、文档加载器:把各种格式“翻译”成 Document

LangChain 提供了上百种Document Loader,覆盖了你能想到的几乎所有数据源。它们的使用模式高度一致:实例化一个 Loader,调用.load()方法,返回一个Document列表。

2.1 加载纯文本文件

最常见的场景——本地.txt文件:

fromlangchain_community.document_loadersimportTextLoader# 创建加载器,指向一个文本文件loader=TextLoader("data/公司请假规定.txt",encoding="utf-8")# 加载,返回 Document 列表docs=loader.load()print(f"加载了{len(docs)}个文档")print(f"第一个文档前100字:{docs[0].page_content[:100]}")print(f"来源:{docs[0].metadata['source']}")

TextLoader默认将整个文件作为一个 Document。如果文件很大,后续我们需要对它进行拆分。

2.2 加载 PDF 文件

PDF 是最常见的文档格式之一。LangChain 的PyPDFLoader可以将 PDF 按页加载,每一页成为一个独立的 Document:

# 需要先安装:pip install pypdffromlangchain_community.document_loadersimportPyPDFLoader loader=PyPDFLoader("data/员工手册2026.pdf")docs=loader.load()print(f"PDF 总页数:{len(docs)}")fori,docinenumerate(docs[:3]):print(f"第{i+1}页前50字:{doc.page_content[:50]}...")print(f" 元数据:{doc.metadata}")

每个Documentmetadata中会自动包含source(文件路径)和page(页码),这在需要引用出处时价值连城。

2.3 加载网页内容

如果知识源来自线上,WebBaseLoader可以直接抓取网页正文:

fromlangchain_community.document_loadersimportWebBaseLoader loader=WebBaseLoader("https://docs.python.org/zh-cn/3/glossary.html")docs=loader.load()print(f"网页标题:{docs[0].metadata.get('title','未知')}")print(f"网页正文前200字:{docs[0].page_content[:200]}...")

2.4 批量加载整个目录

当有一个文件夹塞满了需要处理的文件时,DirectoryLoader可以一键加载:

fromlangchain_community.document_loadersimportDirectoryLoader,TextLoader# 加载 ./docs 目录下所有 .txt 文件loader=DirectoryLoader(path="./docs",glob="*.txt",loader_cls=TextLoader,loader_kwargs={"encoding":"utf-8"})docs=loader.load()print(f"共加载{len(docs)}个文档")

glob参数支持通配符,你可以用"*.pdf"配合PyPDFLoader批量处理 PDF,或用"*.md"处理 Markdown。

2.5 Loader 一览表

Loader用途额外依赖
TextLoader纯文本文件
PyPDFLoaderPDF 文件,按页加载pypdf
CSVLoaderCSV 表格,每行一个 Doc
UnstructuredMarkdownLoaderMarkdown 文件unstructured
WebBaseLoader网页正文bs4
NotionDirectoryLoaderNotion 笔记导出目录
DirectoryLoader批量加载整个文件夹

实际项目中,往往需要混合多种 Loader,先用DirectoryLoader把各种格式的文件兜进来,再统一处理。


三、为什么需要拆分:长文的“切蛋糕”艺术

文档加载完成了,但你不能直接把一篇 5000 字的长文抛给向量数据库。原因有三:

  1. 嵌入模型有输入上限:大多数嵌入模型一次只能处理几百到几千个 token,超长文本需要截断。
  2. 检索粒度问题:一个超大的 Document 可能包含多个不同主题,检索时难以精准命中。比如一本员工手册里夹杂了请假流程和报销流程,用户问“请假怎么请”,却把报销内容也返回来,就是噪音。
  3. 生成质量:模型的上下文窗口有限,检索到的块太大,会挤占问题和其他块的空间。

所以,我们需要把每个长文档智能地切成若干小块(Chunks)。这个操作叫文本拆分,块的大小和重叠量是两个关键参数。

3.1 块大小与重叠

想象你在切一条长面包:

  • 块大小(chunk_size):每刀切多宽。一般以 token 数为单位(一个中文字约 1~2 个 token)。例如chunk_size=500表示每块包含约 500 个 token。
  • 重叠(chunk_overlap):相邻两块之间的尾部/头部重合部分。例如chunk_overlap=50会让每块的开头包含前一块结尾的 50 个 token。这像做木工时的榫卯——避免把一个完整意思拦腰截断在两块的边界上。

设置合理的重叠量,能大大提高检索的召回率,防止答案刚好卡在两块中间而搜不到。


四、LangChain 的文本拆分器:精准“下刀”

LangChain 提供了多种Text Splitter,用途各不相同。最通用的全能选手是RecursiveCharacterTextSplitter

4.1RecursiveCharacterTextSplitter:按自然边界逐级切

它会按照“段落 → 句子 → 词语 → 字符”的优先级逐级尝试拆分。这意味着它会尽量在段落换行处下刀,如果不满足 chunk_size,再尝试在句子结尾(句号、问号)切,以此类推。这样就最大程度保证了语义完整性。

fromlangchain_text_splittersimportRecursiveCharacterTextSplitter# 创建拆分器text_splitter=RecursiveCharacterTextSplitter(chunk_size=500,# 每块最多 500 个字符(可按需要改成 token 数)chunk_overlap=50,# 块之间重叠 50 个字符length_function=len,# 用 len() 计算字符数(也可换成 token 计数函数)separators=["\n\n","\n","。","!","?",";",","," ",""]# 拆分优先级)# 假设 docs 是之前加载的 Document 列表split_docs=text_splitter.split_documents(docs)print(f"拆分前:{len(docs)}个文档")print(f"拆分后:{len(split_docs)}个片段")fori,dinenumerate(split_docs[:3]):print(f"片段{i+1}{len(d.page_content)}字):{d.page_content[:80]}...")

separators是优先级列表。拆分器会先尝试用"\n\n"(两个换行,表示段落)切分。如果切出来的某个块仍超过chunk_size,则降级用"\n"(单换行)再切,以此类推,直到用空字符串""强制按字符切。这保证了在满足大小限制的同时,尽量保持语义单元完整。

4.2TokenTextSplitter:按 Token 数精准切割

当需要严格控制发送给模型的 token 量时,用 token 级别的拆分器更准确:

fromlangchain_text_splittersimportTokenTextSplitter token_splitter=TokenTextSplitter(chunk_size=300,# 每块最多 300 个 tokenchunk_overlap=30,# 重叠 30 个 tokenencoding_name="cl100k_base"# 与 DeepSeek 模型的 tokenizer 近似)token_docs=token_splitter.split_documents(docs)

它会用模型对应的 tokenizer 来计数,因此比字符数更贴近实际消耗。对于需要精细化控制成本的场景(如生产环境),优先推荐。

4.3 Markdown 标题拆分器

如果你的知识库是结构化的 Markdown 文档,MarkdownHeaderTextSplitter能按标题层级拆分,并自动将标题信息写入 metadata:

fromlangchain_text_splittersimportMarkdownHeaderTextSplitter headers_to_split_on=[("#","h1"),("##","h2"),("###","h3"),]markdown_splitter=MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)md_docs=markdown_splitter.split_text("# 请假流程\n## 申请\n...")# 每一个 split 的 metadata 里会带有 h1, h2, h3 字段

这样,检索时不仅返回文本块,还能知道它属于哪个章节,方便溯源和结构化呈现。

4.4 拆分器选择速查表

拆分器适用场景
RecursiveCharacterTextSplitter通用场景,纯文本、PDF、网页均适用,首选
TokenTextSplitter需要精准控制 token 消耗的生产环境
MarkdownHeaderTextSplitter结构清晰的 Markdown 知识库
PythonCodeTextSplitterPython 代码库,按函数/类边界切分

现在你手上已经有了完整的“切蛋糕”工具箱。


五、从加载到拆分:一条最小化预处理链

把这两步串起来,就是一个实际可用的文档预处理流程。假设我们有一个docs文件夹,里面躺着.txt.pdf.md文件,要为 RAG 做数据准备:

fromlangchain_community.document_loadersimportDirectoryLoader,TextLoader,PyPDFLoaderfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain_core.documentsimportDocument# 第一步:加载(为简单演示,此处只加载 txt,实际可多轮加载合并)loader=DirectoryLoader("./docs",glob="*.txt",loader_cls=TextLoader,loader_kwargs={"encoding":"utf-8"})raw_docs=loader.load()# 也可追加加载 PDFpdf_loader=DirectoryLoader("./docs",glob="*.pdf",loader_cls=PyPDFLoader)raw_docs.extend(pdf_loader.load())print(f"加载完成,共{len(raw_docs)}个原始文档")# 第二步:拆分text_splitter=RecursiveCharacterTextSplitter(chunk_size=800,chunk_overlap=80,separators=["\n\n","\n","。","!","?",";",","," ",""])chunks=text_splitter.split_documents(raw_docs)print(f"拆分完成,共{len(chunks)}个知识片段")# 看一眼前两个片段fori,chunkinenumerate(chunks[:2]):print(f"\n--- 片段{i+1}---")print(f"内容:{chunk.page_content[:100]}...")print(f"元数据:{chunk.metadata}")

这个脚本运行后,你就拥有了一堆规格统一的“知识砖块”,随时可以送入下一道工序——嵌入和向量存储。


六、最佳实践:切得聪明,搜得精准

在实际项目中,拆分策略直接决定 RAG 系统的成败。几个经过检验的建议:

  1. 先测后定chunk_sizechunk_overlap没有万能值。建议根据你的文档类型,先用几个典型问题和较小的文档做实验,调整到检索结果最相关为止。
  2. 重叠不要太小:一般设置为 chunk_size 的 10%~20%。过于吝啬的重叠会让边界信息彻底丢失。
  3. 保留源码数据:原始 document 的 metadata 务必随 chunk 一路传递,不要中途丢失。这样最终回答可以附上“参考来源”,用户会更信任。
  4. 不要切分得太碎:块太小会丧失上下文。如果一个回答通常需要半页纸的素材,那就让块大小覆盖半页纸的内容。
  5. 清洗文本:在拆分前可以对文本做轻度清洗(去掉无意义空行、统一标点符号),但不要过度,以免丢失自然语言的上下文信号。

七、今日收获与下篇预告

今天,我们完成了 RAG 大厦的奠基工程:

  • 你认识了Document对象——LangChain 中所有文本的统一集装箱。
  • 你用TextLoaderPyPDFLoaderWebBaseLoaderDirectoryLoader把不同格式的文档装进了 LangChain。
  • 你理解了为什么必须拆分文本,以及块大小与重叠量的意义。
  • 你掌握了RecursiveCharacterTextSplitter等拆分器,能把长文档切成规格统一、语义友好的小块。

现在,一堆整齐的知识砖块已经码好。下一篇《知识向量化:嵌入模型与向量数据库》,我们将把这些砖块送入“魔法烤箱”——用嵌入模型将每一块转成向量,再存入向量数据库。届时,你对知识库的任何提问,都将能在毫秒间定位到最相关的碎片。检索的魔法,即将开始。

下一篇见!

http://www.jsqmd.com/news/819366/

相关文章:

  • 运维系列【仅供参考】:彻底清除TortoiseSVN:从基础卸载到深度清理全指南
  • 杰理之先开广播再切换SPDIF光纤输入,会打印‘a’,无法播放和广播【篇】
  • 【权威实测报告】:对比12种生成场景下的真实Cost/Img,Midjourney API性价比跌破临界点?
  • AI驱动代码库优化:基于Claude Code的上下文工程与自动化重构实践
  • Copaw:专为算法竞赛设计的本地自动化测试与调试工具
  • CircuitPython库管理实战:从零构建嵌入式开发环境
  • 2026年AI学习指南:收藏这份靠谱进阶路径,轻松拉开差距!
  • 【shell编程知识点汇总】第九章 HTML 清洗、多行合并与条件替换
  • 说说Markdown为什么不会被HTML取代
  • KMS_VL_ALL_AIO:智能激活解决方案完全解析
  • 第6章:C++ Sanitizer全家桶实战
  • day22_深度学习入门与pytorch
  • 程序员的副业天花板:靠接私活实现年入百万的秘诀
  • AI智能体技能库开发指南:从原理到实战构建高效Agent应用
  • 苍穹外卖开发日记-微信登录
  • 2026年5月更新:美甲产业升级,甲片专用机定制厂家遴选全攻略 - 2026年企业推荐榜
  • PKSM终极指南:从菜鸟到宝可梦存档管理大师的完整路径
  • Dify插件重打包工具:标准化分发与一键部署实践
  • SPI长距离通信的时钟同步与信号完整性优化
  • 从零上手VibeCoding(ClaudeCode+DeepSeek V4.Pro)
  • 0. 深度学习课程大纲:
  • Redis 身份迷失
  • 从“边缘人”到香饽饽:35岁程序员的开源逆袭路
  • 《我的世界》Java版客户端模组开发:基于freedom-for-steve框架的底层定制实践
  • 【ElevenLabs有声书制作黄金法则】:20年音频工程师亲授,零基础7天交付商用级有声书
  • Node 版本升级后 Electron 原生模块编译失败怎么解决
  • AI工程化实战:从模型到服务的全链路部署与优化指南
  • 手摸手教你用Claude多智能体,零代码构建专属“超级办公助理”全过程
  • Claw-ED:基于Python的配置驱动Web爬虫框架实战指南
  • Gemini CLI提示词库:AI辅助开发提效的工程化实践