从零搭建网页RAG检索系统,保姆级向量库落地教程

📅 2026/7/22 0:55:27 👁️ 阅读次数 📝 编程学习
从零搭建网页RAG检索系统,保姆级向量库落地教程

文章目录

    • 前言
    • 一、整套链路先看懂,一步都不能少
    • 二、前期依赖包一次性装好
    • 三、第一步:Loader,网页转标准Document
      • 3.1 Loader是所有文件的统一转换器
      • 3.2 用CSS选择器精准提取正文
      • 3.3 Document自带两大核心属性
    • 四、第二步:递归切分长文档,解决检索粗糙问题
      • 4.1 为什么不能直接用整篇文章?
      • 4.2 递归切割器配置实操
      • 4.3 递归分隔符的底层逻辑
      • 4.4 chunkOverlap重叠参数到底有啥用?
    • 五、第三步:批量生成Embedding向量
    • 六、第四步:存入向量数据库,测试先用内存库
    • 七、第五步:相似度检索,拿匹配度分数
      • 7.1 检索代码实现
      • 7.2 分数怎么看懂?
    • 八、第六步:拼接上下文丢给大模型回答
    • 九、demo离线上生产还差十万八千里
    • 十、全文流程总结

P.S. 挖到宝藏AI教程!全程通俗易懂,风趣幽默,零基础轻松入门,传送门https://blog.csdn.net/qq_34419312

前言

之前聊RAG的时候,咱们举的例子全是提前整理好的文字片段,看着特别完美,像开了作弊挂。但线下写代码的时候根本不是这么回事!

我之前第一次搭demo,直接把掘金文章复制粘贴成字符串丢进去,写完沾沾自喜,结果被同事吐槽:线上谁有空手动复制网页?你这项目上线得雇十个实习生天天扒文章是吧?

现实里的知识源五花八门,网页、PDF、Word、Markdown堆一堆,今天咱们拿网页当素材,完整走一遍从链接到AI问答的整条流水线,每一步都带可运行代码。

一、整套链路先看懂,一步都不能少

随便丢一个网页URL,系统内部要走完完整的处理链条,顺序错一步都跑不通:

URL → 下载网页HTML → 过滤广告侧边栏只留正文 → 标准化成Document对象 → 切割成小块Chunk → 生成向量Embedding → 存入向量库 → 根据用户问题检索匹配片段 → 丢给大模型输出答案

这条链路跟点外卖流程一模一样:URL是下单地址,Loader是骑手取餐,切分Chunk是分装小餐盒,向量库是外卖柜,检索就是按你想吃的菜翻柜子,最后大模型是给你上菜的服务员。少一个环节你都吃不上饭。

二、前期依赖包一次性装好

本次demo用LangChain.js做主体,Cheerio解析网页,通义千问兼容接口生成向量,内存向量库做测试,安装命令直接复制运行:

pnpm add \ @langchain/community \ @langchain/textsplitters \ @langchain/classic \ @langchain/openai \ cheerio \ dotenv

所有包各司其职,不用额外装爬虫框架,开箱就能解析网页。

三、第一步:Loader,网页转标准Document

3.1 Loader是所有文件的统一转换器

不同文件对应专属加载工具,分工分得清清楚楚:

  • 网页内容:CheerioWebBaseLoader
  • PDF文件:PDF Loader
  • Word文档:Docx Loader
  • 表格CSV:CSV Loader

Loader就是RAG系统的前台接待,不管你是网页、PDF还是表格,进来全统一换成叫Document的标准工牌,后面的流程只认工牌不认原始文件,不然每个格式单独写一套处理逻辑,代码能堆成山。

3.2 用CSS选择器精准提取正文

网页里乱七八糟的导航、评论、广告全是噪音,必须靠CSS选择器锁定正文区域,示例代码:

import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio'; const sourceUrl = '目标掘金文章链接'; const cheerioLoader = new CheerioWebBaseLoader( sourceUrl, { selector: '.article-viewer > :not(style)', }, ); const documents = await cheerioLoader.load();

selector配置含义:只读取文章容器内的内容,自动过滤样式标签,标题、列表、表格、代码块全部保留到pageContent里。

这里踩坑提醒:网站一改版DOM结构直接报废,上次我写好的选择器,平台更新页面后抓出来全是空文本,排查半小时才发现容器class改名了,等于白写。

3.3 Document自带两大核心属性

加载完成后得到Document数组,两个核心字段:

  • pageContent:清洗后的文章纯正文
  • metadata:存放网页地址、文章标题等溯源信息

这一步完全不生成向量,只做格式标准化,别搞混前后步骤。

四、第二步:递归切分长文档,解决检索粗糙问题

4.1 为什么不能直接用整篇文章?

一整篇文章只生成一个向量会出现严重问题:文章里面同时讲文件API、异步回调、Promise,用户只问fs模块相关内容,检索出来一大段无关文字,AI回答精准度直接崩盘。

好比你去图书馆找一本技术书,整本书打包塞一个袋子里,你想找某一页内容,只能拎一整袋翻,效率极低;切成一页一页小纸条,检索才能精准定位。

4.2 递归切割器配置实操

import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 400, chunkOverlap: 80, separators: [ '\n\n', '\n', '。', '!', '?', ';', ',', ' ', '' ], }); const splitDocuments = await textSplitter.splitDocuments(documents);

测试掘金文章直接从1个完整文档,拆成30个独立Chunk,数量随文章长度变动。

4.3 递归分隔符的底层逻辑

切割会按顺序逐级尝试拆分,优先保留完整语义:

段落换行 → 单行换行 → 中文句号 → 感叹号/问号/分号 → 逗号 → 空格 → 字符兜底

数组最后放空字符串,就算遇到无标点代码块、纯英文内容,也能强制控制文本长度。

4.4 chunkOverlap重叠参数到底有啥用?

关键语句刚好卡在两个Chunk中间时,单独读取任意一块都会丢失完整逻辑。

示例:前一段末尾讲主线程不适合同步IO,后一段开头讲优先异步读取,分开看完全看不懂因果。

重叠就像电视剧两集片尾片头重复一小段剧情,防止你断片看不懂前后关联。但重叠值不能拉满,数值太大重复文本暴增,向量存储、接口调用成本直接翻倍,纯纯浪费钱。

chunkSize和chunkOverlap没有万能固定值,要根据文档类型、向量模型、线上检索效果反复调试。

五、第三步:批量生成Embedding向量

咱们用通义千问兼容OpenAI接口生成文本向量,配置代码如下:

import { OpenAIEmbeddings } from '@langchain/openai'; const embeddings = new OpenAIEmbeddings({ apiKey: process.env.DASHSCOPE_API_KEY, model: process.env.EMBEDDINGS_MODEL_NAME, batchSize: 10, configuration: { baseURL: process.env.DASHSCOPE_BASE_URL, }, });

batchSize控制单次接口请求文本数量,本次30个Chunk会自动分3批调用,不用手动循环处理。

批量参数别乱填,服务商有接口限流,一次塞50条直接返回429限流报错,半夜调试接口卡在这里,差点怀疑人生。

六、第四步:存入向量数据库,测试先用内存库

import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory'; const vectorStore = await MemoryVectorStore.fromDocuments( splitDocuments, embeddings, );

fromDocuments内部自动完成两件事:读取每个Chunk文本、调用向量接口生成向量、统一保存文本、向量和元数据。

内存向量库只适合学习和小demo,程序一关数据直接清空,线上项目敢用这个等于每天手动重建知识库,运维能找你谈话。生产必须换持久化向量库。

七、第五步:相似度检索,拿匹配度分数

7.1 检索代码实现

const question = 'fs 模块有哪些常用 API?'; const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);

方法会自动把用户问题转为向量,和库内所有Chunk向量做比对,返回Top3匹配内容+对应分数。

7.2 分数怎么看懂?

内存向量库默认计算余弦相似度,数值区间0~1,数字越大代表和问题关联性越强。

示例返回结果:0.6843、0.6454、0.6131,分数越低越无关。

换其他向量库要注意,有的返回距离值,数字越小越匹配,上次我直接沿用余弦相似度判断逻辑,检索结果全是无关内容,查了半天才发现指标定义完全相反。

八、第六步:拼接上下文丢给大模型回答

检索出来的片段不能直接丢给模型,要格式化拼接进提示词,限制AI只能根据检索内容作答:

const docs = scoredResults.map(([doc]) => doc); const context = docs .map( (doc, index) => `[片段 ${index + 1}]\n${doc.pageContent}`, ) .join('\n\n-----\n\n'); const prompt = ` 你是一个文章阅读助手。请只根据给定文章片段回答问题; 如果片段没有提供答案,请明确说明信息不足。 文章片段: ${context} 问题:${question} 回答: `; const response = await model.invoke(prompt); console.log(response.content);

格式化片段后,模型能精准提取文章内readFileSync、readFile、node:fs/promises等API信息,不会凭空编造内容。

九、demo离线上生产还差十万八千里

咱们跑通的示例只能用来学习,上线必须补齐这些功能:

  1. 网页DOM改版自动适配、捕获抓取失败、空内容过滤
  2. 重复文档去重、网页内容增量更新同步
  3. 替换持久化向量数据库,保证重启不丢数据
  4. 相似度阈值过滤低匹配片段,处理无答案场景
  5. 元数据筛选、回答附带原文来源链接
  6. Chunk参数批量测试调优,接入Rerank重排优化检索
  7. 提示词注入防护,不信任外部网页内容,不能覆盖系统规则

很多新手跑通demo就敢上线,结果用户搜问题全是无关内容、网页抓取一堆广告、重启服务知识库清空,线上bug堆一堆,改起来比从头写还费劲。

十、全文流程总结

网页接入RAG不是丢个URL就能完事,完整流水线一句话概括:

网页链接 → Cheerio加载器标准化文档 → 递归切割语义块 → 批量生成向量 → 存入向量库 → 相似度检索高匹配片段 → 拼接上下文交给大模型输出答案

外部网页经过这套完整处理流程,才能转化成支持语义检索的可用知识库,少任意一环,RAG效果都会大打折扣。

P.S. 挖到宝藏AI教程!全程通俗易懂,风趣幽默,零基础轻松入门,传送门https://blog.csdn.net/qq_34419312