您的位置:首页 > 手游攻略 > 从零构建 RAG:通过 LangChain 实现检索增强生成

从零构建 RAG:通过 LangChain 实现检索增强生成

作者:互联网  时间: 2026-07-17 08:21:09  

从零构建 RAG:用 LangChain 实现检索增强生成

目录

  • RAG 是什么?开卷考试 vs 闭卷考试
  • 环境准备与依赖
  • Document:知识库的最小单元
  • Embedding 与向量存储
  • Retriever:检索器的两种用法
  • 相似度分数的秘密:距离 vs 点积
  • Augment + Generate:完整的 RAG 流程
  • 一点总结
  • 互动讨论

RAG 是什么?开卷考试 vs 闭卷考试

RAG(Retrieval-Augmented Generation,检索增强生成)是一种让 LLM 在回答问题时,先从外部知识库中检索相关文档片段,再基于这些片段生成答案的技术架构。

从零构建 RAG:用 LangChain 实现检索增强生成

一句话本质:RAG = 检索(Retrieval)+ 增强(Augmented)+ 生成(Generation)。让 LLM 从“闭卷考试”变为“开卷考试”。

场景类比
传统 LLM闭卷考试。全靠训练时背下来的知识,没学过就瞎编(幻觉)
微调考前突击补习。花大量时间把新知识强行灌进模型
RAG开卷考试。允许带参考书进考场,不知道就翻书找答案

RAG 的核心流程:

  1. Retrieval(检索) :用户提问 → 问题向量化 → 在知识库中检索最相关的文档片段
  2. Augment(增强) :将检索到的文档片段拼接到 Prompt 中
  3. 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 中的 chaptercharactertypemood 等信息虽然不参与语义计算,但在后续展示结果时非常有用——我们可以知道检索到的内容来自哪个章节、关于哪个角色。

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 做了两件事:

  1. 将每个 Document 的 pageContent 通过 Embedding 模型转为高维向量
  2. 将向量和对应的 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 项目,我理解了:

  1. RAG = 检索 + 增强 + 生成:先查资料,再回答,让 LLM 从闭卷变开卷。
  2. Document 是 Embedding 的最小单元pageContent 做向量化,metadata 用于过滤和溯源,互不干扰。
  3. Embedding 模型与对话模型分工不同:一个负责理解语义,一个负责生成答案。
  4. MemoryVectorStore 适合数据量不大的场景,将 Document 向量化后存入内存。
  5. Retriever 是生产环境的推荐用法:除了向量检索,还会做去重、过滤、Rerank 等操作。
  6. similaritySearchWithScore 返回的是距离值而非相似度值:距离越小越相似。
  7. RAG 的完整流程:文档 → Document → Embedding → 向量存储 → 检索 → 增强 → 生成。

互动讨论

  1. pageContent 和 metadata 的分工是什么?  为什么 metadata 不做 Embedding?
  2. retriever.invoke() 和 vectorStore.similaritySearchWithScore() 的区别是什么?  分别适合什么场景?
  3. 为什么 similaritySearchWithScore 返回的是距离值而不是相似度值?  1 - score 的转换精确吗?
  4. 如果知识库很大(如 10 万条文档),MemoryVectorStore 还适用吗?  应该用什么替代?
  5. RAG 中 k 值的选择会影响什么?  如果 k 太大或太小分别会有什么问题?

最新游戏

更多

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

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