几何图形计算器
40.01M · 2026-03-05
哈喽,掘金的家人们(JYM)!
还记得上一集我们讲的 RAG(检索增强生成)吗?我们用“光光和东东”的故事,成功治好了大模型的“胡说八道症”。
但是!(敲黑板)细心的同学可能发现了,上次的数据是我们 Hardcode(硬编码) 在代码里的。 而在真实世界里,老板给你的需求通常是:“嘿,把咱们公司的技术文档/这个网站的内容/那堆 PDF 喂给 AI,让它能回答客户问题。”
这就引出了 RAG 系统中两个至关重要的环节:
今天,我们就以抓取一篇掘金文章并进行问答为例,手把手带你打通 RAG 的数据大动脉!
很多初学者问:“直接把网页 HTML 扔给 LLM 不行吗?”
绝对不行!理由有三:
所以,我们需要一套**ETL(Extract, Transform, Load)**流程: 网页 -> 纯文本 (Extract) -> 切割成小块 (Transform) -> 存入向量库 (Load)
我们要实现的目标是:让 AI 读懂一篇关于“父亲去世对作者人生态度影响”的掘金文章,并回答我们的提问。
首先,我们需要引入一些强大的工具。
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L2-4
import "cheerio"; // 后端界的 jQuery,使用 CSS 选择器像操作 DOM 一样查找节点
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
cheerio:前端同学对 jQuery 肯定不陌生。Cheerio 就是 Node.js 版的 jQuery,它能让我们用 CSS 选择器(比如 .class, #id)去提取 HTML 里的内容。CheerioWebBaseLoader:LangChain 封装好的网页加载器,底层就是用的 cheerio。RecursiveCharacterTextSplitter:今天的主角!递归字符文本分割器,它是目前最智能的切割方案之一。这部分和上一篇一样,我们需要准备好 ChatModel(负责说话)和 Embeddings(负责把文字变向量)。
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L9-24
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME, // GPT-3.5 / GPT-4
apiKey: process.env.OPENAI_API_KEY,
temperature: 0 // 严谨模式
});
const embeddings = new OpenAIEmbeddings({
modelName: process.env.EMBEDDING_MODEL_NAME, // text-embedding-3-small
apiKey: process.env.OPENAI_API_KEY,
});
我们要抓取的文章链接是 。
打开掘金文章页,F12 审查元素,你会发现文章正文通常包裹在 .main-area 类或者是 article 标签里。
我们要把这些内容“吸”下来。
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L26-31
const cheerioLoader = new CheerioWebBaseLoader(
"https://juejin.cn/post/7233327509919547452?searchId=20260302193603120AE3328025B138C1FB",
{
selector: ".main-area p"
}
);
深度解析:
selector: ".main-area p":这一行代码价值千金!
.main-area 下面的 p(段落)标签。这就像吃小龙虾只吃虾尾肉,壳和头全扔掉。这就是清洗数据的第一步。执行加载:
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L33-33
const documents = await cheerioLoader.load();
此时,documents 里存放的就是经过清洗的、纯净的文章段落文本。
拿到了几千字的长文,直接存向量库效果不好。我们需要把它切成小块(Chunk)。
这里使用的是 RecursiveCharacterTextSplitter(递归字符文本分割器)。
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L35-39
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400, // 每一块的大小(字符数)
chunkOverlap: 50, // 重叠部分的大小
separators: ["。", ",", "?","!"], // ️ 语义分割符优先级
})
这一段代码是 RAG 优化的核心,我们逐行拆解:
chunkSize: 400这决定了每个切片的最大容量。
chunkOverlap: 50(关键!)为什么要有重叠? 想象一下,如果一句话是:“关键密码是...(切断)...123456”。
设置 chunkOverlap: 50,意味着切片 A 的结尾 50 个字,会重复出现在切片 B 的开头。
separators: ["。", ",", "?","!"](递归的奥义 )这个 Splitter 之所以叫“Recursive(递归)”,是因为它非常智能。它的切割逻辑是这样的:
。(句号)来切。
,(逗号)在那个长段落里继续切。
? 或 ! 继续切。总结:它会尽最大努力保持句子的完整性,不会像笨蛋一样在单词中间或者句子的一半硬生生切开。它懂中文语义!
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L41-41
const splitDocuments = await textSplitter.splitDocuments(documents);
现在,splitDocuments 变成了一个包含许多小 Document 对象的数组,每个对象大概 400 字左右,且首尾相连。
接下来的步骤就和之前一样了,我们将切好的“肉块”扔进向量锅里煮。
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L46-49
const vectorStore = await MemoryVectorStore.fromDocuments(
splitDocuments,
embeddings
);
这里,LangChain 会自动调用 OpenAIEmbeddings,把每一个文本块转成 [0.12, -0.45, ...] 这样的向量,并存入内存。
现在,我们来问一个必须读过文章才能回答的深刻问题: “父亲去世对作者的人生态度产生了怎样的根本性逆转?”
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L52-62
const retriever = await vectorStore.asRetriever({k:2}); // 只找最相关的 2 段
const retrievedDocs = await retriever.invoke(question);
const scoreResults = await vectorStore.similaritySearchWithScore(question);
作为开发者,我们不能黑盒操作。我们需要看看 RAG 到底找出了什么。
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L65-76
retrievedDocs.forEach((doc, i) => {
// ... 计算相似度分数 ...
console.log(`文档${i+1}:${doc.pageContent} 相似度评分:${similarity}`);
});
你会发现,由于我们之前切分得当(使用了递归和重叠),检索出的片段通常包含完整的句子,并且正好是文章中描写“父亲去世”和“人生感悟”的那一段。向量匹配的精度直接依赖于 Splitter 的质量。
最后,拼装 Prompt,召唤 LLM。
// c:UsersMRDesktopworkspacelesson_jpaiagentrag-loaderrag-testloader-and-splitter.mjs #L78-92
const content = retrievedDocs
.map((doc, i) => `文档${i+1}:${doc.pageContent}`)
.join("nn--------nn");
const prompt = `你是一个文章辅助阅读助手,根据文章内容来解答:
文章内容:
${content}
问题:
${question}
回答:
`
const response = await model.invoke(prompt);
console.log(response.content);
AI 的回答(示例):
完美!
今天我们从“手搓数据”进化到了“工业级数据处理”。让我们复盘一下 RAG 数据处理的黄金法则:
selector 或正则,在源头去掉噪音(广告、导航)。掌握了 CheerioLoader 和 RecursiveCharacterTextSplitter,你就拥有了处理互联网 90% 文本数据的能力。不管是爬取新闻、分析财报,还是做个人知识库,这套流程都是通用的!
思考题:如果我们要处理 PDF 文件,LangChain 里应该用什么 Loader?如果处理的是 Python 代码文件,Splitter 的分隔符应该换成什么?(提示:代码不是按句号分割的哦~)
记得点赞收藏,代码多敲几遍,RAG 原理自然通!