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

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

摘要:RAG 让 LLM 从"闭卷考试"变为"开卷考试"------先检索相关资料,再基于资料生成答案。本文用 LangChain 从零搭建一个完整的 RAG 系统,涵盖 Document 设计、Embedding 向量化、MemoryVectorStore 存储、Retriever 检索,以及 Augment + Generate 的全流程。

📑 目录

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

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

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

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

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

RAG 的核心流程:

  1. Retrieval(检索) :用户提问 → 问题向量化 → 在知识库中检索最相关的文档片段
  2. Augment(增强) :将检索到的文档片段拼接到 Prompt 中
  3. Generation(生成) :LLM 基于增强后的 Prompt 生成答案

环境准备与依赖

bash

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

go 复制代码
new Document({
    pageContent: '要单独 embedding 的内容',
    metadata: { 
        // 元数据,不参与 embedding,用于后续过滤或溯源
        chapter: 1,
        character: "光光",
        type: "角色介绍"
    }
})

核心设计原则

  • pageContent:会进行 Embedding 向量化,是语义检索的依据
  • metadata:不参与向量化,用于过滤、溯源、展示等辅助功能

在示例中,我用 Document 存储了一个关于"光光和东东"的友情故事,分为 7 个章节,每个章节独立作为一个 Document:

javascript

go 复制代码
const documents = [
  new Document({
    pageContent: `光光是一个活泼开朗的小男孩...`,
    metadata: { chapter: 1, character: "光光", type: "角色介绍", mood: "活泼" }
  }),
  // ... 更多 Document
];

metadata 中的 chaptercharactertypemood 等信息虽然不参与语义计算,但在后续展示结果时非常有用------我们可以知道检索到的内容来自哪个章节、关于哪个角色。

Embedding 与向量存储

创建 Embedding 模型

Embedding 模型和对话模型是分开的,各有分工:

javascript

arduino 复制代码
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

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

ini 复制代码
const retriever = vectorStore.asRetriever({
    k: 3,  // 最相似的 3 条
});

k 参数控制返回的文档数量。k 值太小,LLM 可能缺少关键信息导致幻觉;k 值太大,无关信息可能误导 LLM,还会浪费 token。

用法一:retriever.invoke() ------ 检索器入口

javascript

ini 复制代码
const docs = await retriever.invoke(question);

retriever.invoke()生产环境推荐的用法。它在向量检索的基础上,还会执行:

  • 去重
  • 过滤
  • 阈值过滤
  • Rerank(重排序)

用法二:vectorStore.similaritySearchWithScore() ------ 直接查询

javascript

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

这个方法只做向量相似度查询,返回文档和对应的分数。适合需要查看分数、调试知识库质量的场景。

两者的区别:

特性 retriever.invoke() vectorStore.similaritySearchWithScore()
去重/过滤 ✅ 会做 ❌ 不做
Rerank ✅ 会做 ❌ 不做
返回分数 ❌ 不返回 ✅ 返回
适用场景 生产环境 RAG 调试、质量分析

相似度分数的秘密:距离 vs 点积

similaritySearchWithScore 中,返回的 score距离值 ,而不是相似度值

javascript

ini 复制代码
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
// scoredResults[0][1] 是距离值

距离和相似度的区别:

  • 距离值(Distance) :越小表示越相似。0 表示完全相同,越大表示差异越大。
  • 点积相似度(Cosine Similarity) :越大表示越相似。1 表示完全相同,0 表示无关。

代码中做了转换:

javascript

ini 复制代码
const similarity = score != null ? (1 - score).toFixed(4) : "N/A";

为什么用 1 - score?因为 score 是距离值,范围通常在 0 到 2 之间。1 - score 可以粗略转换为"相似度"的感觉(值越大越相似)。但这不是精确的余弦相似度,只是一个近似的指标。

Augment + Generate:完整的 RAG 流程

Augment(增强):拼接上下文

检索到的文档需要通过 Augment 步骤拼接到 Prompt 中:

javascript

javascript 复制代码
const context = docs.map((doc, i) => 
    `[片段${i}]\n ${doc.pageContent}`
).join("\n\n----\n\n");

const prompt = `你是一个讲友情故事的老师。基于以下故事片段回答问题,用温暖生动的语言。如果故事中没有提到,就说"这个故事里还没有提到这个细节"。

故事片段:
${context}

问题:${question}

老师的回答:`;

这一步把检索到的文档片段"塞进"Prompt,让 LLM 基于这些资料生成答案。这就是 RAG 的"增强"环节------用外部知识增强 LLM 的上下文。

Generate(生成):调用 LLM

javascript

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

📌 一点心得:RAG 的价值不在于"让 AI 更聪明",而在于"让 AI 在特定场景下更靠谱"。它不修改模型,却能让模型回答出原本答不出的私有知识问题。LangChain 把 RAG 的每个环节都标准化了------Document 设计、Embedding、向量存储、检索器、Augment------你只需要组合这些组件,就能搭出完整的 RAG 系统。

相关推荐
Darling噜啦啦1 小时前
告别 AI 幻觉!LangChain RAG 完全实战:Document + MemoryVectorStore 打造轻量级语义知识库
langchain
呆瑜nuage2 小时前
LangChain 聊天模型 —— 结构化输出详解与实战
langchain·大模型应用开发·结构化输出·agent基础
花千树_0104 小时前
插件加载方式选不好,迟早被同事吐槽——四种姿势全解析
langchain·agent
fenglllle5 小时前
python3t pip3安装langchain失败心得
langchain
大模型真好玩7 小时前
LangChain DeepAgents 速通指南(十一)—— DeepAgents Code 记忆与状态管理
人工智能·langchain·agent
lhxcc_fly7 小时前
LangGraph基础知识点
python·langchain·llm·langgraph
元Y亨H8 小时前
Skills vs MCP vs Function Calling 的介绍
llm
武子康8 小时前
LingBot 四线全拆:Video / World / VLA / VA 按最终输出选型 + 开源许可证避坑表
人工智能·llm·agent
为你学会写情书9 小时前
RAG 性能总卡在检索?先搞懂 Milvus 向量数据库再说
llm·agent