把一篇网页放进 RAG 知识库:Loader、文档切分与语义检索实战

前两篇文章已经拆清了 RAG 的基本流程,以及 Document、Chunk、metadata 和向量数据库分别解决什么问题。

不过,前面的示例还有一个明显的理想化条件:参与检索的 Document 是我们手动创建的。

真实知识库面对的通常不是几段已经整理好的字符串,而是网页、PDF、Word、Markdown 等原始资料。

如果知识来源是一篇网页,系统至少要完成下面这条链路:

text 复制代码
网页 URL
→ 下载 HTML
→ 提取正文
→ 转换成 Document
→ 切分成 Chunk
→ 生成 Embedding
→ 写入向量数据库
→ 根据问题检索相关片段
→ 把片段交给大模型回答

本文用一篇真实网页作为知识来源,完整实现网页加载、文档切分、向量化、相似度检索和模型回答。

准备依赖

示例使用 LangChain.js、Cheerio、通义千问兼容接口和内存向量库:

bash 复制代码
pnpm add \
  @langchain/community \
  @langchain/textsplitters \
  @langchain/classic \
  @langchain/openai \
  cheerio \
  dotenv

第一步:用 Loader 把网页变成 Document

不同类型的知识来源,需要不同的加载方式。

text 复制代码
网页    → Web Loader
PDF     → PDF Loader
Word    → Docx Loader
CSV     → CSV Loader

Loader 的职责不是生成向量,而是把不同格式的原始内容统一转换成 LangChain Document

本文使用 CheerioWebBaseLoader 加载网页:

js 复制代码
import { CheerioWebBaseLoader } from 
  '@langchain/community/document_loaders/web/cheerio';

const sourceUrl = 'https://juejin.cn/post/7660707431753678854';

const cheerioLoader = new CheerioWebBaseLoader(
  sourceUrl,
  {
    selector: '.article-viewer > :not(style)',
  },
);

const documents = await cheerioLoader.load();

加载完成后得到的是一个 Document 数组。

其中:

text 复制代码
pageContent:通过 CSS 选择器提取的网页正文
metadata.source:原始网页地址
metadata.title:网页标题

此时还没有生成 Embedding,也没有执行检索。Loader 只完成了"原始网页到标准文档"的转换。

用 CSS 选择器提取完整正文

网页正文通常不只包含段落,还包含:

  • 标题
  • 列表
  • 表格
  • 代码块

因此,CSS 选择器需要覆盖正文容器中的多种内容元素。当前网页使用下面的选择器,读取正文容器的直接子元素,并排除样式标签:

js 复制代码
selector: '.article-viewer > :not(style)'

这样可以把正文、标题、列表、表格和代码统一保存到 pageContent,为后续切分和检索提供完整的原始内容。

这里的 CSS 选择器依赖目标网站当前的 DOM 结构。如果网站改版,选择器也要同步调整。正式项目还需要检查抓取结果、处理空内容,并遵守目标网站的使用规则。

第二步:递归切分长文档

网页正文加载完成后,得到的仍然是一整篇长文档。

直接把整篇文章转换成一个向量,会混合 path、同步文件读取、回调、Promise 和 async/await 等多个主题。用户只问一个局部问题时,检索粒度会过于粗糙。

因此,需要使用 RecursiveCharacterTextSplitter 切分:

js 复制代码
import { RecursiveCharacterTextSplitter }
  from '@langchain/textsplitters';

const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 400,
  chunkOverlap: 80,
  separators: [
    '\n\n', '\n',
    '。', '!', '?', ';', ',',
    ' ', ''
  ],
});

const splitDocuments =
  await textSplitter.splitDocuments(documents);

在当前网页上,这一步把 1 个原始 Document 切成了 30 个 Chunk。

具体数量会随着网页内容和切分配置变化,不是固定结果。

分隔符的递归切分策略

RecursiveCharacterTextSplitter 会按照分隔符顺序逐级尝试:

text 复制代码
先尝试段落
→ 段落仍然太长,再尝试换行
→ 仍然太长,再尝试句号
→ 再尝试感叹号、问号、分号、逗号
→ 最后按空格或字符兜底

这样做的目标是优先保留较大的自然语义边界,同时尽量让每个 Chunk 接近 chunkSize

用空字符串完成字符级兜底

分隔符数组最后保留空字符串 '',用于字符级切分。即使遇到代码、菜单、古文或没有标点的长文本,切分器仍然能够把内容控制在接近 chunkSize 的范围内。

chunkOverlap 解决什么问题?

假设一句重要信息正好位于两个 Chunk 的边界:

text 复制代码
Chunk 1:......Node.js 的主线程不适合执行耗时的同步 I/O
Chunk 2:因此 Web 服务中应优先采用异步文件读取......

两个片段单独出现时,都可能缺少完整因果关系。

chunkOverlap: 80 会让相邻 Chunk 保留一部分重叠内容,降低边界切断语义的概率。

但重叠也不是越大越好。Overlap 过大会制造大量重复向量,增加存储、Embedding 和检索成本。

chunkSizechunkOverlap 没有适用于所有项目的固定答案,需要结合文档结构、Embedding 模型和实际检索结果评估。

第三步:分批生成 Embedding

示例通过 OpenAI 兼容接口调用 DashScope:

js 复制代码
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: 10 控制一次接口请求提交多少条文本。当前示例生成了 30 个 Chunk,LangChain 会按每批 10 条自动完成多次请求。

使用 OpenAI 兼容接口时,模型名称、批量大小、向量维度和请求限制仍然要按照实际服务商的接口要求配置。

第四步:写入内存向量数据库

完成切分后,可以直接从 Document 创建内存向量库:

js 复制代码
import { MemoryVectorStore }
  from '@langchain/classic/vectorstores/memory';

const vectorStore = await MemoryVectorStore.fromDocuments(
  splitDocuments,
  embeddings,
);

fromDocuments() 内部会完成两件事:

text 复制代码
读取每个 Document.pageContent
→ 调用 Embedding Model 生成向量
→ 保存向量、pageContent 和 metadata

MemoryVectorStore 适合教学、测试和小型 Demo。它的数据只保存在当前进程内存中,程序重启后就会消失,不适合作为正式知识库的持久化方案。

第五步:执行带分数的相似度检索

用户问题仍然是一段普通字符串:

js 复制代码
const question = 'fs 模块有哪些常用 API?';

使用 similaritySearchWithScore() 可以直接传入问题字符串,并返回最相关的文档及其分数:

js 复制代码
const scoredResults =
  await vectorStore.similaritySearchWithScore(
    question,
    3,
  );

这个方法会先调用 Embedding Model 生成问题向量,再与向量库中的文档向量进行比较。

读取相似度分数

当前使用的 MemoryVectorStore 默认计算余弦相似度,数值越大表示越相关。

运行结果类似:

text 复制代码
[文档 1] 余弦相似度:0.6843
[文档 2] 余弦相似度:0.6454
[文档 3] 余弦相似度:0.6131

不同向量数据库返回的可能是相似度,也可能是距离。当前 MemoryVectorStore 返回余弦相似度,数值越大越相关;切换其他向量数据库时,应按照对应实现解释分数。

第六步:把检索结果加入 Prompt

向量检索返回的是相关 Document,还没有调用生成模型。

先把 Top 3 片段组成上下文:

js 复制代码
const docs = scoredResults.map(([doc]) => doc);

const context = docs
  .map(
    (doc, index) =>
      `[片段 ${index + 1}]\n${doc.pageContent}`,
  )
  .join('\n\n-----\n\n');

再将上下文和问题一起交给模型:

js 复制代码
const prompt = `
你是一个文章阅读助手。请只根据给定文章片段回答问题;
如果片段没有提供答案,请明确说明信息不足。

文章片段:
${context}

问题:${question}

回答:
`;

const response = await model.invoke(prompt);
console.log(response.content);

修正网页选择器后,检索片段中包含了文章里的代码和列表,模型能够根据上下文识别 readFileSyncreadFilenode:fs/promises 等内容。

完整的数据流是:

text 复制代码
Juejin 网页
→ CheerioWebBaseLoader
→ 原始 Document
→ RecursiveCharacterTextSplitter
→ 30 个 Chunk
→ OpenAIEmbeddings
→ MemoryVectorStore
→ similaritySearchWithScore
→ Top 3 Document
→ Prompt
→ Chat Model 回答

这个 Demo 距离生产环境还有什么差距?

现在的示例已经跑通 RAG 链路,但还不是生产级知识库。

至少还要继续处理:

  • 网页改版后选择器失效
  • 抓取失败、空正文和重复内容
  • 文档更新后的增量同步
  • 持久化向量数据库
  • 相似度阈值与无答案判断
  • metadata 过滤和来源展示
  • Chunk 参数评估
  • 检索结果重排(Rerank)
  • Prompt 注入与不可信网页内容

特别是网页内容不能默认视为可信指令。RAG 系统应把检索结果当作参考资料,而不是允许网页文本覆盖系统规则。

总结

把网页接入 RAG,不是"请求一下 URL"就结束了。

Loader 负责把网页正文转换成标准 Document,Splitter 按语义边界生成 Chunk,Embedding Model 将文本转换成向量,Vector Store 保存并检索相关内容,最后再由生成模型根据检索片段回答问题。

完整流程可以概括为:

text 复制代码
URL
→ CheerioWebBaseLoader
→ Document
→ RecursiveCharacterTextSplitter
→ Embedding
→ MemoryVectorStore
→ Top K 检索
→ Prompt
→ 模型回答

网页内容经过这条数据管道后,才真正从一份外部资料变成了可以参与语义检索的 RAG 知识。

相关推荐
禅与计算机程序设计艺术18 小时前
【DeepThink Research】长程任务 Agent 的工程实现机制:中断续跑、记忆分层、长上下文不丢失与分布式优雅升级
人工智能
糖果店的幽灵19 小时前
【langgraph 从入门到精通graphApi 篇】Command 与动态流程控制
android·java·数据库·人工智能·langgraph
深蓝AI19 小时前
KTransformers 实战:消费级显卡本地跑满血 DeepSeek,CPU-GPU 异构推理全攻略
人工智能
魏祖潇19 小时前
AI幻觉不是用更大模型解决——RAG增强+引用溯源+置信度标注让AI开口必带出处
人工智能·ai编程
QN1幻化引擎19 小时前
认知架构调度与语言模型辅助:DalinX V8 Track 1 实验报告
人工智能·语言模型·架构
大模型丫丫19 小时前
Agent开发的难点是什么呢?
大数据·人工智能·学习
kp0000019 小时前
NER(Named Entity Recognition)命名实体识别
人工智能·网络安全·信息安全·ai安全
hqyjzsb19 小时前
非计算机专业学生怎么进入AI相关方向
人工智能·职场和发展·金融·数据挖掘·数据分析·aigc·业界资讯
龙虾PRO19 小时前
金融 AI 分析系统:人工智能驱动的金融行情分析系统落地方案
人工智能·金融