Document 切割:RAG 知识库数据预处理实战

📅 2026/7/18 3:21:12
Document 切割:RAG 知识库数据预处理实战
文章目录一、知识从哪来二、Document知识的最小标准单元三、Loader知识接入的第一步四、爬虫实战从零实现网页内容抓取爬虫手写版的局限五、LangChain 方案CheerioWebBaseLoader六、文本切割让知识可以被精细检索6.1 为什么要切割6.2 三个核心参数详解1separators分隔符语义排第一2chunkSize块大小硬性上限3chunkOverlap重叠弥补语义断裂七、AI 时代程序员的价值八、完整实战从 URL 到可检索的知识块九、全文总结十、核心知识点复盘十一、常见问题 / 避坑指南一、知识从哪来对于一个 RAG检索增强生成知识库系统知识可以来自各种各样的地方一篇 CSDN / 掘金 / 知乎上的技术文章URL一个 Word 文档.docx一个 PDF 文件.pdf一段 B 站视频的字幕甚至一条靠谱的 Twitter 推文这些不同格式的文件最终都需要变成向量数据库能处理的标准化格式——这就是本文要解决的问题。我们会从零开始先手写爬虫理解底层原理再引入 LangChain 的 Loader TextSplitter 方案逐层递进地把整个 Document 切割流程讲透。二、Document知识的最小标准单元LangChain 定义了一个标准的数据结构叫Document它是所有知识在 RAG 系统中的统一载体// Document 对象结构示意{pageContent:这篇文章主要介绍了蒸馏技术的原理...,// 文档正文内容metadata:{// 元数据source:https://example.com/article,title:蒸馏技术详解,author:张三}}一个 Document 包含两个核心字段字段含义用途pageContent文本的实际内容后续会被向量化Embedding用于语义检索metadata元信息来源、作者、时间等用于追溯原文、按条件过滤“只看张三写的文章”关键点我们不能直接手动创建 Document 对象而是需要通过Loader加载器将各种格式的文件自动转换。这保证了 Document 的结构统一和数据完整性。三、Loader知识接入的第一步Loader 做的事情很简单输入是各种文件输出是标准化 Document 数组。LangChain 生态提供了 180 多种 Loader来自两个主要来源来源包名特点社区langchain/community社区贡献覆盖面广我们也可以贡献 loader官方langchain/core官方维护核心功能稳定面对一个知识库项目你要做的只有两件事选择合适的 Loader根据文件类型选对输出的 Document 进行分块文件太大需要切成有语义的小块才能做向量检索下面我们先从最基础的手写爬虫开始理解 Loader 底层到底做了什么。四、爬虫实战从零实现网页内容抓取在引入 LangChain 之前我们先手写一个爬虫理解底层原理。搞懂了底层再用 Loader 就会明白它帮你省了多少事。目标从指定 URL 爬取网页用 CSS 选择器提取需要的文本。技术选型工具作用axios发送 HTTP 请求拿到 HTML 字符串cheerio在内存中把 HTML 字符串解析为 DOM 树支持 CSS 选择器——让前端开发者用熟悉的 jQuery 风格提取内容// crawl.mjs —— 手写爬虫实现importaxiosfromaxios;// 发送 HTTP 请求import*ascheeriofromcheerio;// 服务端 DOM 解析consttargetUrlhttps://leetcode.cn/problems/spiral-matrix/;asyncfunctioncrawlPage(){try{// 第一步发送 GET 请求获取 HTML 字符串const{data:html}awaitaxios.get(targetUrl);console.log(html);// 原始 HTML 字符串可以在控制台看到完整的网页源码// 第二步cheerio.load() 在内存中把 HTML 字符串虚拟化为一个 DOM 对象// 这是 cheerio 的核心能力让 JS 开发者用前端思维操作后端 HTMLconst$cheerio.load(html);// 第三步用 CSS 选择器提取指定区域的内容// 就像在浏览器里写 $(.main-area p).text() 一样自然constpageContent$(.main-area p).text();console.log(pageContent);}catch(e){console.error(爬取失败:,e.message);}}crawlPage();代码执行流程解析axios.get(url) → 拿到 HTML 字符串整个网页的源码 ↓ cheerio.load(html) → 在内存中构建 DOM 树类似浏览器解析 HTML ↓ $(css selector).text() → CSS 选择器遍历 DOM 树精准提取文本cheerio 为什么好用因为它让后端开发者用前端思维解决问题。你不用写复杂的正则表达式去匹配 HTML 标签直接用熟悉的 CSS 选择器语法#content、.article p、h1.title等就能精确提取页面中的任意内容。这是会用 jQuery 就会写爬虫的开发体验。爬虫手写版的局限手写爬虫虽然灵活但也有一些重复性工作每次都要处理 axios 请求、异常捕获每次都要手动 cheerio.load()输出的只是字符串不是标准化的 Document 对象没有 metadata来源 URL、抓取时间等元信息丢失了LangChain 的 Loader 就是帮你把这些重复工作封装好了。五、LangChain 方案CheerioWebBaseLoader手写爬虫理解原理后我们升级到专业方案。LangChain 社区封装了CheerioWebBaseLoader一行配置就能完成爬取 转换为标准 Document// index.mjs —— 使用 LangChain Loader TextSplitterimportdotenv/config;import{CheerioWebBaseLoader}fromlangchain/community/document_loaders/web/cheerio;import{RecursiveCharacterTextSplitter}fromlangchain/textsplitters;// 第一步用 Loader 爬取网页自动转换为 DocumentconstcheerioLoadernewCheerioWebBaseLoader(// 目标 URLhttps://blog.csdn.net/xxx/article/details/162911131,{selector:#content_views p// CSS 选择器只提取文章正文段落});// load() 返回 Document[]每个元素的 pageContent 是段落文本metadata 自动填充constdocumentsawaitcheerioLoader.load();console.log(加载到的文档数量:,documents.length);console.log(第一篇文档内容:,documents[0].pageContent.slice(0,200));console.log(来源:,documents[0].metadata.source);CheerioWebBaseLoader 帮我们做了什么它将前面的 axios cheerio Document 构造三步合而为一URL → axios 请求 HTML → cheerio 解析 CSS 选择器提取 → 构造标准 Document 对象我们拿到手的直接就是带pageContentmetadata的标准 Document 数组可以直接进入下一步——切块。六、文本切割让知识可以被精细检索6.1 为什么要切割一整篇文章比如 5000 字的技术博客直接塞进向量数据库检索效果会很差。比如你问什么是蒸馏系统可能把整篇文章都返回给你里面夹杂了大量无关段落。切割的核心矛盾块太大语义太宽泛检索不准返回一堆无关内容块太小语义被切断信息不完整关键上下文丢失目标每个块都是一个语义完整的最小单元LangChain 提供了RecursiveCharacterTextSplitter递归字符切割器来解决这个问题// 第二步将大文档切割成语义完整的小块ChunkconsttextSplitternewRecursiveCharacterTextSplitter({chunkSize:400,// 每个 chunk 最多 400 个字符separators:[。,,],// 优先在句号、感叹号、问号处切割chunkOverlap:100,// 相邻 chunk 重叠 100 个字符});constsplitDocumentsawaittextSplitter.splitDocuments(documents);console.log(切割后的文档块数量:,splitDocuments.length);// 查看前两个 chunk 的内容和重叠区域splitDocuments.slice(0,2).forEach((doc,i){console.log(\n--- Chunk${i1}(${doc.pageContent.length}字) ---);console.log(doc.pageContent.slice(-150));// 看末尾 150 字符});6.2 三个核心参数详解1separators分隔符语义排第一separators:[。,,]切割的第一优先级是语义而不是大小。分隔符告诉 TextSplitter“优先在句号。、感叹号、问号这些自然断句点切分”。为什么是这三个标点因为它们标记了一个完整语义的结束。每一句话以它们结尾时表达了一个完整的、可独立理解的意思。而逗号只表示语气停顿语义还没讲完不适合作为切割点。2chunkSize块大小硬性上限chunkSize:400当一段文本按分隔符切完后如果仍然超过 400 字符TextSplitter 会递归尝试更细粒度的分隔符比如从。降级到再到空格直到每个块都不超过 400 字符。这个过程叫递归因为尝试按 。 切割 → 还是太大 → 尝试按 切割 → 还是太大 → 尝试按 切割 → 还是太大 → 尝试按 切割 → 还是太大 → 尝试按空格切割 → 还是太大 → 硬切到 400 字符递归的核心价值是逐级找到最优的切割方案能在句号处切就绝不切逗号能在逗号处切就绝不切空格实在不行才硬切。3chunkOverlap重叠弥补语义断裂chunkOverlap:100这是最容易被忽视但最重要的参数。考虑下面这个被切割的场景Chunk 1: ...光光每天训练足球他非常努力希望参加比赛。 Chunk 2: 希望参加比赛。比赛那天东东帮助他获得胜利。Chunk 1 的最后一句话和 Chunk 2 的第一句话语义相关性是最强的——它们属于同一个故事上下文。但因为chunkSize400的限制它们被硬生生切开了。如果检索时只命中了 Chunk 2你就丢失了光光努力训练的前置信息。chunkOverlap 的解决方案相邻两个 chunk 之间保留 100 个字符的重叠内容。┌─────────────────────────────┐ │ Chunk 1 (400字) │ │ ...他非常努力希望参加比赛。│ └─────────────────────────────┘ │←── 100字 overlap ──→│ ┌─────────────────────────────┐ │ Chunk 2 (400字) │ │ 希望参加比赛。比赛那天... │ └─────────────────────────────┘这样希望参加比赛这句话会同时出现在 Chunk 1 的末尾和 Chunk 2 的开头确保跨越切割边界的上下文不会丢失。一句话总结切割策略语义排第一separators大小做约束chunkSize重叠来兜底chunkOverlap。七、AI 时代程序员的价值你可能会有一个疑问上面这些代码 AI 都能写那程序员还有什么价值价值不再是coding而是驾驭 AI 构建产品。在 AI 时代以下几个方面变得比写代码本身更重要能力维度含义提出好问题向 AI 描述清楚需求给出精确的上下文Context而不是模糊一句帮我写个功能。问题质量决定 AI 产出质量驾驭 Agent设计 Agent 产品架构串联多个工具和模型部署Deploy稳定运行的 AI 应用设计 LoopAgent 的本质是一个长期运行的自主循环Loop设计这个循环让它稳定不出错是架构师的核心能力快速迭代把精力花在定义什么是正确的事而非一行行写代码用 AI 加速从想法到产品你可以快速成长为一名AI 架构师——不一定是代码写得最多的那个人但一定是最懂怎么让 AI 干好活的那个人。八、完整实战从 URL 到可检索的知识块把整个流程串联起来一个完整的 RAG 数据预处理流水线importdotenv/config;importcheerio;// // 第一步引入 Loader — 负责从 URL 爬取网页转为标准 Document// import{// CheerioWebBaseLoader内部封装了 axios cheerio// 发送 HTTP 请求拿到 HTML → 用 CSS 选择器提取内容 → 构造 Document 对象CheerioWebBaseLoader}fromlangchain/community/document_loaders/web/cheerio;// // 第二步引入 TextSplitter — 负责把大文档切成语义完整的小块// import{// RecursiveCharacterTextSplitter递归字符切割器// 会逐级尝试不同的分隔符。→ → → → 空格找到最优切割点RecursiveCharacterTextSplitter}fromlangchain/textsplitters// ----------------------------------------------------------// 阶段一加载 — 从目标 URL 爬取并提取文章正文// ----------------------------------------------------------// selector 使用 CSS 选择器 #content_views p精准提取 CSDN 文章正文的每个段落constcheerioLoadernewCheerioWebBaseLoader(https://blog.csdn.net/suger__salt/article/details/162911131?fromshareblogdetailsharetypeblogdetailsharerId162911131sharereferPCsharesourcesuger__saltsharefromfrom_link,{selector:#content_views p});// load() 发起请求并返回标准 Document 数组每个元素包含 pageContent metadataconstdocumentsawaitcheerioLoader.load();// ----------------------------------------------------------// 阶段二切割 — 把大文档递归切成语义完整的小块Chunk// ----------------------------------------------------------// 切割策略// 1. 语义排第一 — 优先在句号/感叹号/问号处断句保证每个 chunk 表达完整意思// 2. 大小做约束 — 每个 chunk 不超过 chunkSize 个字符// 3. 重叠来兜底 — 相邻 chunk 有 overlap 个字符的重复防止关键上下文被切断//// 为什么不用逗号作为分隔符// 逗号只表示语气停顿语义还没讲完不适合作为切割点consttextSplitternewRecursiveCharacterTextSplitter({chunkSize:400,// 每个知识块最多 400 个字符separators:[。,,],// 优先在句末标点处切割chunkOverlap:100,// 相邻块之间保留 100 个字符重叠})// 下面用两段示意文本直观展示 chunkOverlap 的作用//// Chunk 1: ...光光每天训练足球他非常努力希望参加比赛。// ↑───────────────── 重叠区域 ─────────────────↑// Chunk 2: 希望参加比赛。比赛那天东东帮助他获得胜利。//// 希望参加比赛。 同时出现在两个 chunk 中避免了因切割导致的语义断裂。constsplitDocumentsawaittextSplitter.splitDocuments(documents);// 输出切割后的所有文档块可以看到每个 chunk 的 pageContent 和 metadataconsole.log(splitDocuments);直观看到上述代码的执行结果如果不做切割切割之后上面代码执行完整数据流向图┌──────────────┐ 各种来源 │ Loader │ 标准化 ──────────────────►│ │────────────► URL / PDF / Word │ 180 种加载器│ Document[] └──────────────┘ │ ▼ ┌──────────────┐ │ TextSplitter│ 切块 │ │────────────► │ 递归分块策略 │ Chunk[] └──────────────┘ │ ▼ ┌──────────────┐ │ 向量数据库 │ 存储检索 │ │────────────► │ Embedding │ 知识库就绪 └──────────────┘九、全文总结本文从零到一完成了 RAG 知识库的 Document 切割全流程底层原理手写 axios cheerio 爬虫理解 HTML 字符串 → DOM 树 → CSS 选择器提取的完整链路。工程化方案使用 LangChain 的 CheerioWebBaseLoader一行配置完成 URL → 标准 Document 的转换。核心技巧RecursiveCharacterTextSplitter 的三大参数——separators 语义优先、chunkSize 硬性约束、chunkOverlap 弥补语义断裂。AI 时代定位代码能力不再是唯一竞争力驾驭 AI 构建产品的架构思维才是核心价值。十、核心知识点复盘知识点关键内容Document 结构{ pageContent, metadata }RAG 系统中知识的标准化载体Loader 作用输入任意格式文件 → 输出标准化 Document 数组cheeiro服务端 DOM 解析库让后端开发者用前端 CSS 选择器思维操作 HTMLCheerioWebBaseLoader封装 axios cheerio Document 构造URL → Document 一步完成RecursiveCharacterTextSplitter递归尝试不同分隔符找到每个 chunk 的最优切割方案chunkSize每个知识块的最大字符数硬性大小上限separators优先切割点列表语义排第一——能在句号切就绝不切逗号chunkOverlap相邻 chunk 的重叠字符数防止跨切割边界的语义断裂递归切割逐级尝试更细粒度的分隔符找到最优切割点十一、常见问题 / 避坑指南1. import 路径报错ERR_PACKAGE_PATH_NOT_EXPORTED问题引入CheerioWebBaseLoader时写成// ❌ 错误路径缺少 /web/import{CheerioWebBaseLoader}fromlangchain/community/document_loaders/cheerio;langchain/community的package.json中exports字段只声明了./document_loaders/web/cheerio路径直接写cheerio会报错。解决// ✅ 正确路径注意多了 /web/import{CheerioWebBaseLoader}fromlangchain/community/document_loaders/web/cheerio;排错技巧安装langchain/community后可以到node_modules/langchain/community/package.json的exports字段中搜cheerio确认正确的导入路径。2.RecursiveCharacterTextSplitter导入报错问题写成了RecursiveC不完整或直接拼错名字。// ❌ 不完整的命名模块中不存在此导出import{RecursiveC}fromlangchain/textsplitters;解决// ✅ 完整类名import{RecursiveCharacterTextSplitter}fromlangchain/textsplitters;同时确保已安装依赖pnpmi langchain/textsplitters3. 爬虫拿不到内容常见原因和解决方法原因排查方法解决方案CSS 选择器不匹配F12 打开浏览器开发者工具检查实际 DOM 结构根据实际 HTML 结构调整选择器目标网站有反爬机制观察返回内容是否包含验证码/重定向添加 User-Agent 等请求头伪装浏览器页面动态渲染SPA查看 HTML 源码中是否没有正文内容cheerio 只能解析静态 HTMLSPA 页面需要用 Puppeteer/Playwright4. chunkOverlap 设多大合适经验值chunkOverlap一般设为chunkSize的10% ~ 20%比较合理。chunkSize推荐 overlap说明40040 ~ 80中文场景约 2-3 句话1000100 ~ 200英文或中英混合场景2000200 ~ 400长文档场景太小了纠不住语义断裂太大了重复太多浪费存储空间和向量计算成本。5..mjs文件执行没有输出也不报错如果你写的.mjs文件运行后什么都没输出也不报错逐项检查import 路径是否正确见避坑 1、2——路径错误时 Node.js 会在模块解析阶段报错是否有顶层await——Node.js ≥ 14.8 在.mjs中支持顶层 await确认版本检查空 catch有没有catch(err){}把错误偷偷吞掉加上console.error(err)试试package.json中type: commonjs不影响.mjs——.mjs文件强制按 ESM 处理但项目混用 CJS 和 ESM 时要注意兼容性本文是 RAG 核心技术系列的第二篇实战篇基于 LangChain.js 生态langchain/communitylangchain/textsplitters所有代码均可在 Node.js 18 环境中直接运行。