爬网页+分块+向量检索,50行JS搭建专属知识库,彻底解决大模型幻觉问题

😵先说说踩过的大坑

做AI问答时,我踩过无数痛点:

  1. 直接丢整篇文章给LLM,上下文超限直接截断,关键信息丢失
  2. 大模型凭空编造内容,答非所问,幻觉严重
  3. 手动写爬虫解析网页,正则写到头秃,换个网站就得重写
  4. 文档切割不讲究语义,一句话拆成两段,检索完全失效
    在这里插入图片描述

折腾一周后,我用LangChain一套流程搞定:网页爬虫→标准化文档→语义分块→向量库存储→相似度检索→LLM精准问答

看完这篇你能学会:

  • Cheerio优雅爬取网页正文,不用手写复杂正则
  • 中文友好的语义分割,保证chunk语义完整
  • 内存向量库快速存储文本向量,本地即可测试
  • 相似度打分检索,精准匹配原文内容
  • 一套可直接复制运行的完整RAG流水线代码

🧩先搞懂核心概念:RAG预处理全链路

知识库来源五花八门:Word、PDF、B站视频、网页、推文,不能直接丢给大模型。

完整处理链路:
各类数据源 → Loader加载标准化Document → 语义分块Chunk → Embedding向量化 → 向量数据库存储 → 检索匹配上下文 → LLM生成答案

1. Document:LangChain标准数据格式

所有文件、网页最终都会转为统一Document对象,包含两个核心字段:

  • pageContent:正文文本,用于检索和向量化
  • metadata:元数据(来源链接、章节、类型等),溯源、过滤用

不能手动硬拼Document,必须用Loader做适配转换

2. Loader:数据源适配层(180+社区内置)

分两大包:

  • @langchain/core:官方核心维护
  • @langchain/community:社区维护,包含网页、PDF、Excel等各类加载器

网页专用:CheerioWebBaseLoader,底层封装axios+cheerio,用CSS选择器提取正文,告别正则地狱。

3. 文本分割Splitter:RAG成败关键

文档太大必须切割,核心目标:保留语义完整性
三个核心配置:

  1. separators:分割优先级(中文优先句号、感叹号、问号)
  2. chunkSize:单块最大字符长度
  3. chunkOverlap:块重叠冗余,解决一句话被拆分导致语义断裂问题

4. AI时代程序员核心价值

不再单纯堆代码,核心能力转变:

  • 写出高质量Prompt
  • 提供完整、准确上下文Context
  • 设计稳定可循环的Agent流程
  • 驾驭、部署AI产品,转型AI架构师

🕷️第一步:原生Cheerio爬虫基础demo

先看底层实现,理解Loader背后原理,新建crawl.js

// crawl.js
import axios from 'axios';
import * as cheerio from 'cheerio';

// 目标掘金文章链接
const targetUrl = 'https://juejin.cn/post/7660707431753678854';

async function crawlPage() {
  try {
    // 1. 请求网页拿到完整html字符串
    const { data: html } = await axios.get(targetUrl);
    // 2. cheerio在内存虚拟DOM,前端CSS选择器思维解析
    const $ = cheerio.load(html);
    // 精准提取文章正文段落,过滤导航、广告、侧边栏
    const pageContent = $('.main-area p').text();
    console.log('提取到的正文内容:', pageContent);
  } catch(e) {
    console.error('爬取失败:', e);
  }
}

crawlPage();

底层逻辑拆解

  1. axios发送http请求,获取原始HTML文本
  2. cheerio.load(html) 内存生成DOM树,无需浏览器
  3. 通过CSS选择器精准筛选需要的内容,比正则简单百倍

🚀第二步:LangChain完整RAG实战(可直接运行)

新建index.js,全套流程:爬取→分块→向量化→检索→问答

前置依赖安装

npm install dotenv cheerio axios langchain @langchain/openai @langchain/community

.env环境变量配置

OPENAI_API_KEY=你的密钥
OPENAI_BASE_URL=https://xxx
MODEL_NAME=gpt-3.5-turbo
EMBEDDINGS_MODEL_NAME=text-embedding-ada-002

完整业务代码

// index.js
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';

// 1. 初始化大模型与向量嵌入模型
const model = new ChatOpenAI({
  temperature: 0, // 关闭随机,保证回答严谨
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDINGS_MODEL_NAME,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL
  },
});

// 2. 网页Loader:爬取掘金文章,指定正文选择器
const cheerioLoader = new CheerioWebBaseLoader(
  'https://juejin.cn/post/7660707431753678854',
  {
    selector: '.main-area p' // 只提取文章段落,过滤冗余内容
  }
)
const documents = await cheerioLoader.load();

// 3. 中文语义分割器(重点配置)
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 400, // 单块最大400字符
  separators: ["。", "!", "?"], // 优先按中文句子分割
  chunkOverlap: 100, // 100字符重叠,防止语义切断
})
const splitDocuments = await textSplitter.splitDocuments(documents);
console.log(`文档分割完成,共${splitDocuments.length}个chunks`);

// 4. 内存向量库,本地快速测试
const vectorStore = await MemoryVectorStore.fromDocuments(
  splitDocuments,
  embeddings
);
console.log("向量存储完成");

// 5. 检索器:每次返回相似度最高3个文本块
const retriever = vectorStore.asRetriever({k: 3});
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);

// 打印检索结果与相似度
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)}`);
});

// 6. 拼接检索上下文,传入LLM问答
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("\n🤖AI回答:");
console.log(response.content);

⚠️实战踩坑提醒(90%开发者都会出错)

坑1:分割不设置chunkOverlap,语义断裂

一段完整说明被切成两个chunk,检索时只能匹配其中一半,答案缺失关键信息。
✅ 解决方案:中文场景重叠值建议80~120字符

坑2:直接爬取整页HTML,无关内容干扰检索

页面导航、广告、评论会混入Document,向量匹配时大量无关文本占用检索名额。
✅ 解决方案:使用selector精准限定正文DOM节点

坑3:separators只写英文逗号,中文句子切割混乱

默认分割符是英文换行、空格,中文一整段不会拆分,直接触发chunkSize硬截断。
✅ 解决方案:优先配置中文句号、感叹号、问号

坑4:temperature设置过高,模型随意编造内容

RAG知识库问答需要严谨,高温度会让模型脱离原文瞎回答。
✅ 解决方案:知识库问答固定temperature=0

坑5:不使用Loader,手动拼接Document

不同文件格式(PDF/Word/网页)文本提取逻辑完全不同,手动维护成本极高。
✅ 解决方案:直接复用社区180+种Loader,切换数据源仅换一行代码

📌整套流程核心总结

  1. Loader统一标准化:多源数据统一转为Document,一套逻辑适配所有知识库
  2. 语义分割是核心:用中文标点优先分割+重叠冗余,平衡长度与语义完整
  3. Cheerio简化爬虫:CSS选择器替代复杂正则,快速提取网页正文
  4. 向量检索解决幻觉:只拿原文相关片段喂给LLM,杜绝凭空编造
  5. 低成本落地:MemoryVectorStore无需部署向量数据库,本地快速验证RAG效果

这套流程不仅能爬网页,替换对应Loader即可读取PDF、Markdown、Excel文档,快速搭建企业内部知识库、AI文档问答机器人。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐