写在前面:之前我们做 RAG,知识库里的文档都是手动写的
new Document({ pageContent: "......" })。老师说:"你不可能每篇文档都手写吧?知识从哪里来?" 是啊,真实的知识库是各种格式的文件------Word、PDF、网页链接、B 站视频......今天终于学到了关键一步:文档加载器(Loader)。一行代码把一篇掘金文章"爬"下来,再切成语义完整的块,存入向量库。我感觉我离真正的 RAG 又近了一步。
一、知识库不是手写的,是"加载"出来的
1.1 之前的问题
老师说:
"知识库放的知识,知识的来源很多------一个 Word 文档、一个 PDF 文件、一个 Bilibili 视频、一个 URL、一个靠谱的 Twitter......"
之前我们做的 RAG,Document 是手动写的:
javascript
const documents = [
new Document({
pageContent: `光光是一个活泼开朗的小男孩......`,
metadata: { chapter: 1, character: "光光" },
}),
];
这显然不现实。一个真正的知识库可能有几千几万篇文档,不可能手写。
解决方案:用 Loader(加载器)。
老师说:
"Langer ------ 知识库 → 向量数据库。各种知识文件有不同的 Loader,输入是文件,输出是 Documents。"
LangChain 内置了 180 多种 Loader------
CheerioWebBaseLoader:加载网页内容PDFLoader:加载 PDFDocxLoader:加载 WordYouTubeLoader:加载 YouTube 视频字幕
你只需要告诉 Loader "文件在哪",它自动给你生成标准的 Document 对象。
二、爬虫:用 Cheerio 以"前端思维"爬网页
2.1 传统爬虫 vs Cheerio
先看 crawl.mjs:
javascript
import axios from "axios";
import * as cheerio from 'cheerio';
const targetUrl = 'https://juejin.cn/post/7660707431753678854';
async function crawlPage() {
try {
const { data: html } = await axios.get(targetUrl);
const $ = cheerio.load(html); // html → DOM 树
const pageContent = $('.main-area p').text(); // CSS 选择器提取
console.log(pageContent);
} catch (e) {
// 错误处理
}
}
crawlPage();
cheerio 是一个"在 Node.js 里模拟浏览器 DOM"的库。
老师说:
"Cheerio 另辟蹊径------前端思维,CSS 选择器提取需要的内容。"
它的工作方式是这样的:
css
HTML 字符串(从网页下载)
↓ cheerio.load(html)
虚拟 DOM 树(在内存中)
↓ $('.main-area p').text()
提取出指定 CSS 选择器的文本内容
用 axios 下载 HTML,用 cheerio 解析 DOM,用 CSS 选择器提取内容------这就是"前端思维"的爬虫。
2.2 整个流程
| 步骤 | 工具 | 做了什么 |
|---|---|---|
| 1. 发送请求 | axios.get(url) |
下载网页 HTML |
| 2. 解析 DOM | cheerio.load(html) |
在内存构建虚拟 DOM 树 |
| 3. 提取内容 | $('.main-area p').text() |
CSS 选择器取指定内容 |
| 4. 得到文本 | 纯文本字符串 | 可以拿去向量化了 |
如果没有 cheerio,你就得用正则表达式去抠文本------一个让人想死的操作。
三、Loader:LangChain 的"文档适配器"
3.1 CheerioWebBaseLoader
看 index.mjs:
javascript
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 把"爬虫 + 解析 + 生成 Document"的流程打包成了一个类。
- 传入 URL + CSS 选择器。
- 调用
.load()方法。 - 返回的就是标准的
Document[](pageContent + metadata)。
3.2 为什么不用手动写 Loader?
老师说:
"来自社区
@langchain/community,主要由社区维护,我们都可以写 Loader。LangChain@langchain/core官方维护的。"
Loader 有 180 多种,覆盖了绝大多数知识来源。 如果你需要的格式不在里面,你也可以自己写一个 Loader 提交到社区。
四、文本切割:保持语义完整性的"艺术"
4.1 为什么要切分?
老师说:
"文件太大,要检索的是一定大小、具有一定语义的 Chunk(块)。"
一个文档可能非常长------比如一篇 10000 字的掘金文章。如果你把它整个向量化:
- 你问"什么是 RAG",它会返回整篇文章。
- 但这篇文章可能只有一段话是关于 RAG 的,其他全是废话。
- 不够精准。
所以需要切分(Split)------把长文档切成语义完整的片段。
4.2 怎么切?
老师说:
"切割的意义------保持语义的完整性。"
RecursiveCharacterTextSplitter 是 LangChain 提供的递归文本切分器:
javascript
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400, // 每个 Chunk 最大 400 个字符
separators: ["。","!","?"], // 按句号、感叹号、问号切分
chunkOverlap: 100, // 前后重叠 100 个字符
});
const splitDocuments = await textSplitter.splitDocuments(documents);
三个关键参数:
| 参数 | 作用 | 比喻 |
|---|---|---|
| chunkSize | 每块的最大字符数 | 每段话不能超过 400 字 |
| separators | 切分依据的标点符号 | 按句号切,不能按逗号切 |
| chunkOverlap | 前后块的重叠字符数 | 两段话之间重复 100 字 |
老师说:
"Separators 按
。!?切------不会是,。"
句号、感叹号、问号是一句话的结束,语义相对完整。逗号只是停顿,切在逗号上会把一句话劈成两半。
4.3 chunkOverlap 的作用
老师说:
"切断了,Chunk 的最后一句和下一句 Chunk 第一句,他们的语义相关性是最大的。但是因为 chunkSize 切开了,语义遗憾。用 Overlap 用一定的冗余来确保语义的完整性。"
想象一下:
arduino
Chunk 1: "今天天气很好,我们去公园散步。小明带了一个风筝,"
Chunk 2: "他跑得很快,风筝飞得很高。"
如果 chunkOverlap = 0,Chunk 1 以"风筝,"结尾,Chunk 2 以"他"开头。
检索"风筝"时,Chunk 2 有"风筝"但没有"小明"------语义断了。
如果 chunkOverlap = 100:
Chunk 1: "今天天气很好,我们去公园散步。小明带了一个风筝,"
Chunk 2: "小明带了一个风筝,他跑得很快,风筝飞得很高。"
↑ 重叠的 100 字
两个 Chunk 都包含"小明带了一个风筝",语义就完整了。
chunkOverlap 就像两块木板拼接时的"重叠部分"------让接缝处更牢固。
五、完整的 RAG 数据准备流程
网页 URL / PDF / Word 等
↓ Loader(180+ 种)
Document 对象数组
↓ TextSplitter(切分成语义完整的 Chunk)
小 Document 数组
↓ Embedding 模型
向量
↓ 存入
向量数据库
↓ 使用时
Retriever 检索器
之前我们手动写 Document,现在有了 Loader,真实的知识库终于可以自动化了。
六、总结:让知识库"活"起来
| 概念 | 说明 |
|---|---|
| Loader | 把各种格式的文件转为标准 Document |
| CheerioWebBaseLoader | 网页加载器,输入 URL + CSS 选择器 |
| cheerio | Node.js 中的虚拟 DOM 解析库 |
| RecursiveCharacterTextSplitter | 递归文本切分器 |
| chunkSize | 每个块的最大字符数 |
| separators | 切分依据的分隔符(句号、感叹号、问号) |
| chunkOverlap | 前后块的重叠字符数,保证语义衔接 |
Loader 解决了"知识从哪里来"的问题,TextSplitter 解决了"怎么让知识更精准"的问题。两者结合,就是 RAG 知识库的"数据管道"。
写在最后
今天最大的收获,是知道了 Loader 的存在。之前手动写 Document,总觉得 RAG 做着做着就变成了"体力活"。有了 Loader,一个 URL 扔进去,自动爬取、自动切分、自动向量化------这才是工程化的感觉。
而且 cheerio 的"前端思维"爬虫让我特别有亲切感------$('.main-area p').text(),这不就是 jQuery 吗?用熟悉的东西做新的事情,这种感觉太好了。
下次面试官问你:"RAG 的知识库文档怎么准备?"
你可以淡定地说:
"RAG 知识库的文档准备分为两步。第一步用 Loader 加载:LangChain 提供了 180 多种 Loader,比如 CheerioWebBaseLoader 可以加载网页内容,传入 URL 和 CSS 选择器,调用 .load() 方法自动返回标准 Document[]。第二步用 TextSplitter 切分:RecursiveCharacterTextSplitter 按句号、感叹号、问号等分隔符切分,chunkSize 控制每块大小,chunkOverlap 保证语义连续性。这样处理后的 Document 就可以进行 Embedding 并存入向量库了。整个过程从原始文件到可向量化的片段,实现了自动化的数据管道。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。