从零理解 RAG 文档切割:爬虫、Loader 与语义分块

4 阅读5分钟

从零理解 RAG 文档切割:爬虫、Loader 与语义分块

前言

在构建 RAG(Retrieval-Augmented Generation,检索增强生成)应用时,文档切割是一个绕不开的核心环节。本文将结合一个实际的代码项目,从爬虫抓取到文档分块,手把手带你理解整个流程。


一、知识从哪来?—— 知识库的源头

知识库里的知识来源五花八门:

  • 📄 一个 Word 文档
  • 📑 一个 PDF 文件
  • 🎬 一个 Bilibili 视频
  • 🔗 一个 URL 链接
  • 🐦 一条靠谱的 Twitter

这些不同格式的文件,最终都要变成向量数据库中的向量。但在此之前,它们需要先被统一成一种标准格式 —— Document

LangChain 提供了标准的 Document 格式:pageContent(页面内容)+ metadata(元数据)。

这个将原始文件转换为 Document 对象的过程,就叫做 Loader(加载器)


二、Loader:从文件到 Document 的桥梁

LangChain 生态提供了 180 多种 Loader,覆盖了几乎所有常见的文件格式。它们分为两类:

来源说明
@langchain/community社区维护,覆盖面广,我们也可以贡献 Loader
@langchain/core官方维护,核心功能

每种文件后缀都对应着不同的 Loader,但它们的职责一致:输入是文件,输出是 Document


三、爬虫实战:用 Cheerio 抓取网页内容

来看 crawl.mjs 中的代码,它演示了如何手动爬取一个网页:

import axios from 'axios';
import * as cheerio from 'cheerio';

const targetUrl = 'https://juejin.cn/post/7660707431753678854';

async function crawlPage() {
  const { data: html } = await axios.get(targetUrl);
  const $ = cheerio.load(html);        // 在内存中构建 DOM 树
  const pageContent = $('.main-area p').text(); // CSS 选择器提取内容
  console.log(pageContent);
}

爬虫的两个核心步骤

第一步:发送 HTTP 请求,拿到 HTML 字符串

axios 发送 GET 请求,服务端返回 text/html 类型的 HTML 文档。这一步很简单。

第二步:解析 HTML,提取需要的文本内容

这是真正的难点。传统做法是用正则表达式,但 HTML 结构复杂,正则很难精准匹配。

Cheerio 另辟蹊径——它提供了一种前端思维:

  1. cheerio.load(html) 将 HTML 字符串在内存中虚拟化为一个 DOM 对象(树状结构)
  2. 然后你就可以像写前端代码一样,用 CSS 选择器(如 .main-area p)来定位和提取内容
  3. Cheerio 负责遍历 DOM 树,返回匹配节点的文本

💡 核心价值:Cheerio 让 JS 开发者能以前端思维,简单高效地完成指定 URL、指定部分的爬取工作,完全不需要跟正则表达式较劲。


四、LangChain 的 CheerioWebBaseLoader

手动爬虫虽然能跑,但在 RAG 体系中,我们更希望一步到位得到标准的 Document 对象。index.mjs 展示了优雅的做法:

import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';

const cheerioLoader = new CheerioWebBaseLoader(
  'https://juejin.cn/post/7660707431753678854',
  {
    selector: '.main-area p'  // CSS 选择器,精准定位文章段落
  }
);

const documents = await cheerioLoader.load();

CheerioWebBaseLoader 集成了爬虫 + Cheerio 解析的能力,同时还能传入 CSS 选择器来缩小提取范围。返回的结果已经是 LangChain 标准的 Document 对象,可以直接进入下一步:分块(Split)


五、分块的意义:保持语义完整性

为什么要分块?

一个完整的文章可能有几千甚至上万字,直接把它作为一个整体塞给 LLM 是不现实的(上下文窗口有限,检索精度也差)。我们需要把它切分成一定大小、具有独立语义的 Chunk(块) ,这样才能精准检索。

怎么切才不伤语义?

切割的核心矛盾在于:

ChunkSize 会切断相邻 Chunk 之间的语义联系——上一个 Chunk 的最后一句和下一个 Chunk 的第一句,它们的语义相关性往往是最大的,但因为 ChunkSize 的限制被硬生生切开了。

为了解决这个"语义遗憾",LangChain 的 RecursiveCharacterTextSplitter 做了两件事:

第一层:智能选择分隔符(Separators)

不是随便找个位置就切,而是按照语义的基本构成符号来切:

  • (句号)✅ —— 一句话结束,语义完整
  • (感叹号)✅ —— 语气结束,语义完整
  • (问号)✅ —— 问题结束,语义完整
  • (逗号)❌ —— 话还没说完,切开会破坏语义

代码中这样配置:

separators: ["。", "!", "?"]

递归的策略是:先尝试用最优先的分隔符切,如果 Chunk 还是太大,就尝试下一个分隔符,直到找到最优的切分点,使每个 Chunk 都尽量保持语义完整,同时接近 ChunkSize。

第二层:Overlap 重叠补救

即使选择了最优的分隔符,仍然会有"硬切"的情况。这时就用 chunkOverlap(重叠) 来补救:

const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 400,        // 每个 chunk 约 400 字符
  separators: ["。", "!", "?"],
  chunkOverlap: 100,     // 相邻 chunk 重叠 100 字符
});

Overlap 的逻辑:让相邻 Chunk 之间有一定量的内容重叠。这样,即使某段语义被切在了边界上,它也会完整地出现在相邻的某个 Chunk 中,检索时不会丢失。


六、完整流程图

原始 URL
    
    
┌──────────────────────┐
  CheerioWebBaseLoader    CSS 选择器提取指定内容
  (爬虫 + Cheerio)     
└──────────┬───────────┘
           
           
    ┌──────────┐
     Document    LangChain 标准格式 (pageContent + metadata)
    └─────┬────┘
          
          
┌──────────────────────────┐
 RecursiveCharacterTextSplitter 
  · chunkSize: 400            智能语义分块
  · separators: 。!?      
  · chunkOverlap: 100         重叠补救
└──────────┬───────────────┘
           
           
    ┌──────────────┐
     splitDocuments│   可直接存入向量数据库
    └──────────────┘

七、AI 时代程序员的思考

项目的 readme 中有一段值得深思的话:

  • 程序员的价值不再是 coding,交给 AI
  • Vibe Coding 的核心能力:问出好的问题(Prompt)、提供丰富准确的上下文(Context)、驾驭并部署 Agent 产品(Harness & FDE)、设计长时间稳定运行的 Loop
  • 用好 AI,快速成为一名 AI 架构师

这其实点明了学习 RAG 这类技术的深层意义:我们不再需要手写每一行爬虫代码或正则表达式,但我们必须理解整个链路的设计逻辑——为什么要分块?分隔符怎么选?Overlap 怎么设?这些决策直接影响最终 RAG 应用的检索质量。

代码可以被 AI 代劳,但架构决策和语义理解,始终需要人来把关。


总结

环节工具/概念核心作用
内容获取Cheerio + CSS Selector精准提取网页指定区域
格式标准化LangChain Loader统一为 Document 对象
语义分块RecursiveCharacterTextSplitter按语义边界切割
完整性保障chunkOverlap用冗余换语义完整

这三个文件串起了一条完整的 RAG 文档处理链路,从原始 URL 到可直接入库的 Chunk,每一步都体现了工程上对"语义完整性"的追求。希望这篇文章能帮你建立起对 RAG 文档切割的直观理解!