LangChain RAG 实战:网页爬取、文本分块与检索问答完整教程
前置依赖安装
npm install dotenv cheerio axios @langchain/community @langchain/textsplitters @langchain/classic @langchain/openai
.env 环境配置文件
OPENAI_API_KEY=你的密钥
OPENAI_BASE_URL=中转地址
MODEL_NAME=gpt-3.5-turbo
EMBEDDINGS_MODEL_NAME=text-embedding-ada-002
一、配套理论笔记(完整保留你原文)
Document 切割
知识库存储各类知识,知识来源十分丰富:Word 文档、PDF 文件、B 站视频、网页 URL、优质推文等。 所有异构文件不能直接向量化,需要统一转换成 LangChain 标准Document对象,转换工具就是 Loader。
Document 结构
LangChain 定义标准文档结构,包含两个核心字段:
pageContent:文本主体内容;metadata:来源、标签、章节等溯源元数据。
loader 加载器
整体链路:知识库文件 → Loader → Document 数组 → 向量数据库 不同后缀、不同类型文件,对应专属 Loader,使用分两步:
- 根据文件类型选择对应 Loader,官方 + 社区合计 180 + 款;
- 超大 Document 必须分块,检索单元是具备完整语义的小块 chunk。
包区分:
@langchain/community:社区维护,可自定义开发 Loader;@langchain/core:官方底层核心能力。
网页爬虫 crawl 原理
-
请求目标 URL,拿到完整 HTML 字符串(Axios 简单实现);
-
解析 HTML 提取正文:纯正则匹配难度高、容错差;
-
Cheerio 替代方案:贴合前端开发思维,用 CSS 选择器精准提取内容
cheerio.load(html)在内存生成虚拟 DOM 对象;$(css选择器).text()直接提取标签内纯文本。
文本切割的核心意义
分割的核心目标:全程保证语义完整性
separators:选用句子终止符(。!?)作为基础语义分隔符号,不会用逗号切割;chunkSize:控制单块字符上限,超出阈值才执行分割;chunkOverlap重叠补偿逻辑: 一段完整语义刚好被切割线拆分,前一块末尾、后一块开头关联性最强,但被硬分割断开。 依靠 overlap 预留冗余文本,两块共享交叉内容,弥补断裂丢失的上下文,保证语义完整。
AI 时代程序员价值
单纯基础编码工作可以交给 AI 完成。 现在主流 Vibe Coding 工作模式,程序员核心工作:
- 设计高质量 Prompt,提出精准问题;
- 构建丰富、干净、准确的上下文 Context(RAG 分块、数据源提纯);
- 驾驭、部署落地 Agent 类 AI 产品;
- 设计可长时间稳定运行的循环 Loop; 熟练驾驭 AI 工具,转型 AI 架构师才是核心竞争力。
二、完整主程序 index.js(分段解析)
1. 模块导入段
import "dotenv/config";
import "cheerio";
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio'
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory'
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
代码解析
dotenv/config:自动读取项目根目录.env 环境变量,无需手动调用 config 方法cheerio:网页 DOM 解析依赖,给 CheerioWebBaseLoader 提供解析能力CheerioWebBaseLoader:社区维护网页加载器,基于 cheerio 实现定向网页文本抓取RecursiveCharacterTextSplitter:递归字符文本分割器,RAG 标准语义分块工具MemoryVectorStore:内存临时向量库,仅用于本地测试,程序销毁数据丢失ChatOpenAI/OpenAIEmbeddings:大对话模型、文本嵌入向量模型,对接 OpenAI 接口
2. 大模型实例初始化段
const model = new ChatOpenAI({
temperature: 0,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
代码解析
temperature:0:温度设为 0,关闭模型随机发散,输出严谨、无编造幻觉,知识库问答必用model:读取环境变量指定对话大模型configuration.baseURL:配置接口中转地址,解决国内无法直连 OpenAI 问题
3. 向量嵌入模型初始化段
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME,
configuration: {
baseURL: process.env.OPENAI_BASE_URL
},
});
代码解析
嵌入模型作用:将自然文本转为高维数字向量,向量相似度用于知识库检索;配置规则与对话模型统一,共用密钥与中转地址。
4. Cheerio 网页加载器 & 文档拉取段
const cheerioLoader = new CheerioWebBaseLoader(
'https://juejin.cn/post/7660707431753678854',
{
selector: '.main-area p'
}
)
const documents = await cheerioLoader.load();
代码解析
- 构造器第一个参数:目标爬取网页链接
- selector 配置
.main-area p:CSS 选择器,仅提取文章正文段落,过滤导航、广告、侧边栏等无效内容,减少冗余文本干扰检索 .load():发送网络请求、解析网页、提取文本,返回 LangChain 标准Document数组,每个 Document 包含 pageContent(文本)、metadata(网页元信息)
5. 递归文本分割器分块核心代码
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400,
separators: ["。", "!", "?"],
chunkOverlap: 100,
})
const splitDocuments = await textSplitter.splitDocuments(documents);
console.log(`文档分割完成, 共${splitDocuments.length}个chunks`);
代码解析
separators: ["。", "!", "?"]:中文专属语义分隔符,分割器会优先按完整句子切割,优先保证语义完整,不会截断单句话chunkSize:400:单个文本块最大字符上限,超过阈值才会触发切割chunkOverlap:100:块重叠长度,相邻两个文本块首尾复用 100 字符;解决完整语义被分割线切断,丢失上下文关联的问题;通用规范:重叠长度取 chunkSize 的 10%~20%splitDocuments:将完整长 Document 切割为多个小块 Document,每个小块独立存入向量库
6. 内存向量库构建 & 检索器生成
const vectorStore = await MemoryVectorStore.fromDocuments(
splitDocuments,
embeddings
);
console.log("向量存储完成");
const retriever = vectorStore.asRetriever({k: 3});
代码解析
fromDocuments:批量把分块文本送入 embedding 转为向量,存入内存向量库asRetriever({k:3}):生成检索工具,用户提问时自动返回语义最相近的 3 个文本块
7. 问题定义、双模式检索代码
const question = "fs模块有哪些api";
console.log('='.repeat(80));
console.log(question);
console.log('='.repeat(80));
const docs = await retriever.invoke(question);
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
代码解析
retriever.invoke():封装好的简易检索 API,直接返回匹配 Document 数组,业务问答使用similaritySearchWithScore():底层检索方法,返回[Document, 余弦距离]二元组,用于调试相似度,距离数值越小代表文本越相似
8. 检索结果格式化打印(相似度转换)
console.log("\n [检索到的文档及相似度评分]");
docs.forEach((doc, i) => {
const scoredResult = scoredResults.find(([scoredDoc]) =>
scoredDoc.pageContent === doc.pageContent
)
const score = scoredResult? scoredResult[1]: null;
const similarity = score != null ? (1 - score).toFixed(4): "N/A"
console.log(`\n[文档 ${i + 1}] 相似度指标: ${similarity} (原始分: ${score})`);
console.log(`内容: ${doc.pageContent.substring(0, 50)}...`);
console.log(`元数据:${JSON.stringify(doc.metadata)}`);
});
代码解析
- 循环匹配检索文档与带距离分的结果,绑定每条文本对应的余弦距离
1 - score:余弦距离转 0~1 区间直观相似度,数值越高代表匹配度越高substring(0,50):仅打印前 50 字符防止控制台刷屏,线上调试专用- 打印 metadata:溯源信息,可区分文本来源网页、章节
9. 上下文拼接、Prompt 构造、大模型问答
const context = docs
.map((doc, i) => `[片段${i}]\n ${doc.pageContent}`)
.join("\n\n-----\n\n");
const prompt = `
你是一个文章辅助阅读助手,根据文章内容来解答:
文章内容:
${context}
问题:${question}
你的回答:`;
const response = await model.invoke(prompt);
console.log(response.content);
代码解析
- 遍历检索到的文本块,拼接带编号的参考上下文,用分割线区分不同片段,方便模型区分信息边界
- Prompt 强约束:要求模型仅依靠提供的网页片段回答,限制模型凭空编造内容,缓解 AI 幻觉
model.invoke(prompt):传入完整提示词,调用大模型生成最终答案,.content取出文本回复
三、Cheerio 原生爬虫底层实现(分段解析)
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);
const pageContent = $('.main-area p').text();
console.log(pageContent);
} catch(e) {
console.error("爬取失败", e)
}
}
crawlPage();
分段解析
axios:发送 HTTP GET 请求,获取网页原始 HTML 字符串cheerio.load(html):内存中虚拟生成 DOM 树,复刻浏览器解析 HTML 逻辑,无需打开浏览器$('.main-area p').text():使用前端熟悉的 CSS 选择器筛选目标标签,提取纯文本,相比正则解析 HTML 更简洁稳定- try/catch:捕获网络超时、404、页面结构变更等爬取异常
四、整体流程总结
网页 HTML 抓取 → 筛选正文生成 Document → 语义分块(chunkSize+chunkOverlap)→ 文本向量化存入向量库 → 用户问题向量检索 top-k 片段 → 片段拼接上下文注入提示词 → 大模型基于参考内容输出答案