从零构建 RAG:通过 LangChain 实现检索增强生成
从零构建 RAG:用 LangChain 实现检索增强生成
目录
- RAG 是什么?开卷考试 vs 闭卷考试
- 环境准备与依赖
- Document:知识库的最小单元
- Embedding 与向量存储
- Retriever:检索器的两种用法
- 相似度分数的秘密:距离 vs 点积
- Augment + Generate:完整的 RAG 流程
- 一点总结
- 互动讨论
RAG 是什么?开卷考试 vs 闭卷考试
RAG(Retrieval-Augmented Generation,检索增强生成)是一种让 LLM 在回答问题时,先从外部知识库中检索相关文档片段,再基于这些片段生成答案的技术架构。
一句话本质:RAG = 检索(Retrieval)+ 增强(Augmented)+ 生成(Generation)。让 LLM 从“闭卷考试”变为“开卷考试”。
| 场景 | 类比 |
|---|---|
| 传统 LLM | 闭卷考试。全靠训练时背下来的知识,没学过就瞎编(幻觉) |
| 微调 | 考前突击补习。花大量时间把新知识强行灌进模型 |
| RAG | 开卷考试。允许带参考书进考场,不知道就翻书找答案 |
RAG 的核心流程:
- Retrieval(检索) :用户提问 → 问题向量化 → 在知识库中检索最相关的文档片段
- Augment(增强) :将检索到的文档片段拼接到 Prompt 中
- Generation(生成) :LLM 基于增强后的 Prompt 生成答案
环境准备与依赖
bash
复制代码pnpm i @langchain/openai @langchain/classic dotenv
@langchain/openai:提供 ChatOpenAI(对话模型)和 OpenAIEmbeddings(嵌入模型)@langchain/classic:LangChain 经典模块,包含 MemoryVectorStore 等核心组件dotenv:读取.env文件中的环境变量
LangChain 版本更新非常快,建议参考官方文档获取最新 API。
Document:知识库的最小单元
在 RAG 中,Document 是 Embedding 的最小单元。知识库由文件(文本、图片、声音、视频)构成,文件中每个段落是有语义的——之所以分段,肯定带有语义。
Document 对象包含两个字段:
javascript
复制代码new Document({
pageContent: '要单独 embedding 的内容',
metadata: {
// 元数据,不参与 embedding,用于后续过滤或溯源
chapter: 1,
character: "光光",
type: "角色介绍"
}
})
核心设计原则:
pageContent:会进行 Embedding 向量化,是语义检索的依据metadata:不参与向量化,用于过滤、溯源、展示等辅助功能
在示例中,我用 Document 存储了一个关于“光光和东东”的友情故事,分为 7 个章节,每个章节独立作为一个 Document:
javascript
复制代码const documents = [
new Document({
pageContent: `光光是一个活泼开朗的小男孩...`,
metadata: { chapter: 1, character: "光光", type: "角色介绍", mood: "活泼" }
}),
// ... 更多 Document
];
metadata 中的 chapter、character、type、mood 等信息虽然不参与语义计算,但在后续展示结果时非常有用——我们可以知道检索到的内容来自哪个章节、关于哪个角色。
Embedding 与向量存储
创建 Embedding 模型
Embedding 模型和对话模型是分开的,各有分工:
javascript
复制代码import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';// 对话模型:用于生成答案
const model = new ChatOpenAI({
temperature: 0,
model: process.env.DEEPSEEK_MODEL_PRO,
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_API_BASE_URL
})// 嵌入模型:用于将文本转为向量
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.DASHSCOPE_API_KEY,
model: process.env.DASHSCOPE_EMBEDDING,
configuration: {
baseURL: process.env.DASHSCOPE_API_BASE_URL
}
})
向量存储
MemoryVectorStore 是 LangChain 提供的内存向量存储,适合数据量不大的场景:
javascript
复制代码import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory';const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings);
fromDocuments 做了两件事:
- 将每个 Document 的
pageContent通过 Embedding 模型转为高维向量 - 将向量和对应的 Document 一起存入内存
向量化的过程就是将文本转化为“语义坐标”。语义相近的文本,在向量空间中的距离更近。这样当用户提问时,我们可以通过计算向量距离,找到语义最相关的文档片段。
Retriever:检索器的两种用法
Retriever(检索器)是 RAG 中的核心组件。它将向量数据库封装成一个标准入口:输入问题,输出相关性最强的文档列表。
javascript
复制代码const retriever = vectorStore.asRetriever({
k: 3, // 最相似的 3 条
});
k 参数控制返回的文档数量。k 值太小,LLM 可能缺少关键信息导致幻觉;k 值太大,无关信息可能误导 LLM,还会浪费 token。
用法一:retriever.invoke() —— 检索器入口
javascript
复制代码const docs = await retriever.invoke(question);
retriever.invoke() 是生产环境推荐的用法。它在向量检索的基础上,还会执行:
- 去重
- 过滤
- 阈值过滤
- Rerank(重排序)
用法二:vectorStore.similaritySearchWithScore() —— 直接查询
javascript
复制代码const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
这个方法只做向量相似度查询,返回文档和对应的分数。适合需要查看分数、调试知识库质量的场景。
两者的区别:
| 特性 | retriever.invoke() | vectorStore.similaritySearchWithScore() |
|---|---|---|
| 去重/过滤 | 会做 | 不做 |
| Rerank | 会做 | 不做 |
| 返回分数 | 不返回 | 返回 |
| 适用场景 | 生产环境 RAG | 调试、质量分析 |
相似度分数的秘密:距离 vs 点积
在 similaritySearchWithScore 中,返回的 score 是距离值,而不是相似度值。
javascript
复制代码const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
// scoredResults[0][1] 是距离值
距离和相似度的区别:
- 距离值(Distance) :越小表示越相似。0 表示完全相同,越大表示差异越大。
- 点积相似度(Cosine Similarity) :越大表示越相似。1 表示完全相同,0 表示无关。
代码中做了转换:
javascript
复制代码const similarity = score != null ? (1 - score).toFixed(4) : "N/A";
为什么用 1 - score?因为 score 是距离值,范围通常在 0 到 2 之间。1 - score 可以粗略转换为“相似度”的感觉(值越大越相似)。但这不是精确的余弦相似度,只是一个近似的指标。
Augment + Generate:完整的 RAG 流程
Augment(增强):拼接上下文
检索到的文档需要通过 Augment 步骤拼接到 Prompt 中:
javascript
复制代码const context = docs.map((doc, i) =>
`[片段${i}]n ${doc.pageContent}`
).join("nn----nn");const prompt = `你是一个讲友情故事的老师。基于以下故事片段回答问题,用温暖生动的语言。如果故事中没有提到,就说"这个故事里还没有提到这个细节"。故事片段:
${context}问题:${question}老师的回答:`;
这一步把检索到的文档片段“塞进”Prompt,让 LLM 基于这些资料生成答案。这就是 RAG 的“增强”环节——用外部知识增强 LLM 的上下文。
Generate(生成):调用 LLM
javascript
复制代码const response = await model.invoke(prompt);
console.log(response.content);
LLM 根据增强后的 Prompt 生成答案。由于 Prompt 中包含了相关文档片段,LLM 能基于事实回答,而不是凭“记忆”编造。
一点总结
通过这个 RAG 项目,我理解了:
- RAG = 检索 + 增强 + 生成:先查资料,再回答,让 LLM 从闭卷变开卷。
- Document 是 Embedding 的最小单元:
pageContent做向量化,metadata用于过滤和溯源,互不干扰。 - Embedding 模型与对话模型分工不同:一个负责理解语义,一个负责生成答案。
- MemoryVectorStore 适合数据量不大的场景,将 Document 向量化后存入内存。
- Retriever 是生产环境的推荐用法:除了向量检索,还会做去重、过滤、Rerank 等操作。
similaritySearchWithScore返回的是距离值而非相似度值:距离越小越相似。- RAG 的完整流程:文档 → Document → Embedding → 向量存储 → 检索 → 增强 → 生成。
互动讨论
pageContent和metadata的分工是什么? 为什么metadata不做 Embedding?retriever.invoke()和vectorStore.similaritySearchWithScore()的区别是什么? 分别适合什么场景?- 为什么
similaritySearchWithScore返回的是距离值而不是相似度值?1 - score的转换精确吗? - 如果知识库很大(如 10 万条文档),
MemoryVectorStore还适用吗? 应该用什么替代? - RAG 中
k值的选择会影响什么? 如果k太大或太小分别会有什么问题?
-
07.17
妹居物语五子棋玩法如何玩
-
07.17
平行人生游戏怎样生孩子
-
07.17
正式引入 LM Studio Bionic: 开放模型的AI智能体
-
07.17
伪装字体:隐匿输入的 TTF 字体
-
07.17
以"经典"机器学习(AIGC文本检测)检测 LLM- Gened Web 虚构
-
07.17
王者万象棋长安阵营英雄怎样搭配
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏