您的位置:首页 > 手游攻略 > LangChain.js RAG 实战:爬取掘金文章:让 LLM 告别幻觉

LangChain.js RAG 实战:爬取掘金文章:让 LLM 告别幻觉

作者:互联网  时间: 2026-07-14 09:02:05  

从零实现 RAG:爬取掘金文章,让 LLM 帮你读懂任何网页

什么是 RAG?

RAG(Retrieval-Augmented Generation,检索增强生成)是解决 LLM "幻觉"问题的核心方案。

LangChain.js RAG 实战:爬取掘金文章,让 LLM 告别幻觉

简单来说:LLM 的训练数据是有限的,当你问它训练集之外的问题(比如一篇昨天刚发布的文章),它就会"认认真真地胡说八道"——这就是幻觉。

RAG 的思路很直接:先检索相关知识,再让 LLM 基于这些知识回答。

用户提问 → 向量检索 → 找到相关文档片段 → 拼入 Prompt → LLM 生成回答

本文通过两个递进的实战案例,带你从手工知识库到全自动爬虫 RAG,完整走通这条链路。

案例一:手工知识库 RAG

我们先从最简单的场景入手——手动编写一个关于"光光和东东"的友情故事,让 LLM 基于这个故事回答问题。

核心代码结构

import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";import { Document } from "@langchain/core/documents";import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";// 1. 手动构造知识库(7个Document)const documents = [new Document({pageContent: `光光是一个活泼开朗的小男孩...`,metadata: { chapter: 1, character: "光光", type: "角色介绍", mood: "活泼" },}),// ... 共7段故事];// 2. 向量化并存入内存const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings);// 3. 构建检索器const retriever = vectorStore.asRetriever({ k: 3 });// 4. 提问 → 检索 → 增强Prompt → 生成回答const retrievedDocs = await retriever.invoke("光光和东东各自擅长什么?");const context = retrievedDocs.map((doc, i) => `[片段${i+1}]n${doc.pageContent}`).join("nn----nn");const prompt = `基于以下故事片段回答问题:n${context}nn问题:${question}n回答:`;const response = await model.invoke(prompt);

Document:知识的"标准载体"

Document 是 LangChain 中的核心数据结构,每个 Document 代表知识库中的一个片段(chunk):

字段作用
pageContent存储实际文本,会被 embedding 成向量参与语义搜索
metadata附加标签(章节、角色、类型等),不参与向量计算,可用于过滤检索

在 RAG 全流程中,Document 是贯穿始终的统一数据格式:

原始文本切片 → Document[] → Embedding 向量化 → 存入向量库 → 检索返回 Document[] → 提取内容拼入 Prompt

案例二:自动爬虫 RAG(loader-and-splitter.mjs)

手工编写知识库毕竟不现实。真正的 RAG 需要自动从各种来源加载文档、自动切片、自动向量化。

这个案例实现的是:爬取一篇掘金文章 → 自动切片 → 向量化 → LLM 问答。

完整流程图

掘金文章 URL→ CheerioWebBaseLoader(爬取+解析)→ RecursiveCharacterTextSplitter(智能切片)→ OpenAIEmbeddings(向量化)→ MemoryVectorStore(存入内存向量库)→ retriever(检索)→ LLM(生成回答)

三个核心 API 详解

1. CheerioWebBaseLoader — 网页加载器

import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";const loader = new CheerioWebBaseLoader("https://juejin.cn/post/7233327509919547452",{ selector: '.main-area p' }// CSS选择器,只提取正文段落);const documents = await loader.load();

底层原理:使用 cheerio(服务端的 jQuery)解析 HTML。你可以像操作前端 DOM 一样用 CSS 选择器精确定位要提取的内容。

关键配置:selector 决定了抓取哪些 DOM 节点。这里 .main-area p 只取文章正文区 <p> 标签,自动过滤掉导航栏、评论区等噪音。

2. RecursiveCharacterTextSplitter — 递归文本切片器

import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";const splitter = new RecursiveCharacterTextSplitter({chunkSize: 400, // 每个chunk最大字符数chunkOverlap: 50, // 相邻chunk重叠字符数separators: ['。', ',', '!', '?']// 分隔符优先级});const chunks = await splitter.splitDocuments(documents);

三个参数的作用:

  • chunkSize:每个文本块的最大字符数。这是上限,不是固定值。
  • chunkOverlap:相邻块之间重叠的字符数。比如 chunk1 末尾 50 字和 chunk2 开头 50 字相同,防止关键信息在切割边界处被"腰斩"。
  • separators:按优先级排序的分隔符。优先在句号处切,其次是逗号,再次是感叹号、问号,保证切分尽量发生在自然语义边界。

分隔符和 chunkSize 会冲突吗?

不会。RecursiveCharacterTextSplitter 采用"先全切再合并"的策略:

  1. 先用 把所有句子切开
  2. 贪心合并:A句+A句+A句 → 如果不超 400 字就继续加
  3. 加到某句超了 400 就停止,这个 chunk 完成
  4. 从下一句开始新的 chunk

所以最终一个 chunk 里可能包含 2-4 个完整句子,大小接近但不超 400 字。

3. MemoryVectorStore — 内存向量数据库

import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";const vectorStore = await MemoryVectorStore.fromDocuments(splitDocuments, embeddings);const retriever = vectorStore.asRetriever({ k: 2 });

MemoryVectorStore 把所有向量数据存在内存中,适合学习和原型验证。

两个核心方法:

方法作用
fromDocuments(docs, embeddings)将文档列表 embedding 后存入内存
asRetriever({ k: 2 })将向量库包装成检索器,k=每次返回几个结果

检索时实际发生的是:

  1. 用户问题 → embedding → 问题向量
  2. 问题向量与库中所有文档向量做余弦相似度计算
  3. 余弦值越接近 1 表示语义越相似,越接近 0 表示不相关
  4. 按相似度排序,返回 Top K 个 Document

LangChain 常见 Splitter 行为对比

不同的 Splitter 切割策略差异很大,选对工具很重要:

Splitter策略特点
RecursiveCharacterTextSplitter按分隔符优先级递归切割,贪心合并凑近 chunkSize保持句子完整性,最常用
CharacterTextSplitter按固定字符数 + 滑动窗口切割简单粗暴,可能截断句子
TokenTextSplitter按 token 数切割精确控制 token 消耗,但不关心语义边界
MarkdownHeaderTextSplitter按 Markdown 标题层级切割适合结构化文档
HTMLHeaderTextSplitter按 HTML 标题标签切割适合网页文档

关键区别:只有 RecursiveCharacterTextSplitter 是"先全部按分隔符切开,再贪心合并凑满 chunkSize",其他都是一刀一刀按固定窗口切。所以 Recursive 的 chunk 质量通常最高。

构建 Prompt 与 LLM 调用

检索到相关文档后,将其拼入 Prompt:

const context = retrievedDocs.map((doc, i) => `[片段${i+1}]n${doc.pageContent}`).join("nn----nn");const prompt = `你是一个文章辅助阅读助手,根据文章内容来解答:文章内容:${context}问题:${question}回答:`;const response = await model.invoke(prompt);

这就是 RAG 中 Augmented(增强) 的核心:给 LLM 的 Prompt 不只是用户问题,还追加了检索到的相关上下文。

关于国内模型的兼容性

代码中使用的是 @langchain/openai,但实际调用的可能是通义千问(Qwen)或 DeepSeek 等国内模型。关键在于:

const model = new ChatOpenAI({apiKey: process.env.OPENAI_API_KEY, // 国内模型的 API Keyconfiguration: {baseURL: process.env.OPENAI_BASE_URL// 国内模型的 API 地址},});

国内主流大模型(阿里 Qwen、DeepSeek、智谱 GLM、月之暗面 Kimi、零一万物 Yi 等)都提供 OpenAI 兼容接口,只需要改 baseURLapiKey,代码不用动。

总结

从两个案例可以看到 RAG 的完整链路:

  1. Loader:从各种来源加载文档(网页、PDF、数据库…)
  2. Splitter:将长文档智能切片成 chunks
  3. Embedding:将每个 chunk 转为向量
  4. VectorStore:存储向量,支持相似度检索
  5. Retriever:根据用户问题检索最相关的 chunks
  6. Augmented Prompt:将检索结果拼入 Prompt
  7. Generation:LLM 基于增强后的 Prompt 生成准确回答

掌握了这个流程,你就能让 LLM 读懂任意文档,从"通用聊天机器人"升级为"领域知识专家"。

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4