从零搭建 RAG 应用 —— 一个故事带你彻底搞懂检索增强生成

前言

如果你正在准备 AI 应用开发方向的面试,RAG(Retrieval Augmented Generation,检索增强生成)这个词你一定绕不开。但市面上的教程要么堆概念不讲代码,要么直接丢一坨复杂项目让人无从下手。

这篇文章用一个完整的故事(光光和东东的友情故事)作为知识库,带你从 0 到 1 把 RAG 跑起来。读完你会掌握:

  • RAG 到底是什么、为什么要用它
  • LangChain 的核心组件怎么用
  • 5 个容易搞混的关键区别(面试常考)
  • 完整的 RAG 代码,可以直接跑

一、什么是 RAG?用大白话讲清楚

1.1 先看 LLM 的致命弱点

你有没有遇到过这种情况:问 ChatGPT "昨天的 NBA 比赛谁赢了",它告诉你"我的知识截止到 xxx 年"。或者问它"这是我们公司的产品介绍,帮我写一段文案",它开始一本正经地胡说八道。

LLM 的两大短板:

问题 原因
不知道新知识 训练数据有截止日期
瞎编(幻觉) 它本质是"预测下一个字",没有事实校验

1.2 RAG 怎么解决?

RAG 的思路非常朴素------让 LLM 答题之前先翻资料

复制代码
没有 RAG: 用户提问 → LLM 凭记忆回答(可能瞎编)
有了 RAG: 用户提问 → 先去知识库翻资料 → 把翻到的内容 + 问题一起给 LLM → 靠谱回答

三个字母拆开记:

字母 全称 做什么 一句话
R Retrieval 检索 去知识库"翻找"相关内容
A Augmented 增强 把翻到的资料"塞"进 prompt 当小抄
G Generation 生成 LLM 拿着小抄"写"出靠谱回答

⚠️ 面试考点:RAG 全称是 R etrieval A ugmented Generation,不是 Retrival Agument Gererative(这是常见拼写错误)。

1.3 一张图理解 RAG 全流程

复制代码
┌──────────────────────────────────────────────────────────┐
│                      摄入阶段(存)                         │
│                                                          │
│   📄 文档  →  ✂️ 切分  →  🧮 向量化  →  📦 存入向量库     │
│                                                          │
├──────────────────────────────────────────────────────────┤
│                      查询阶段(取)                         │
│                                                          │
│   ❓ 问题  →  🧮 向量化  →  🔍 相似度搜索  →  📋 相关文档  │
│                                                          │
├──────────────────────────────────────────────────────────┤
│                      生成阶段(答)                         │
│                                                          │
│   📋 相关文档 + ❓ 问题  →  📝 Prompt模板  →  🤖 LLM 回答  │
│                                                          │
└──────────────────────────────────────────────────────────┘

二、RAG 和 LangChain 的关系(面试高频)

2.1 一句话区分

RAG 是菜谱,LangChain 是厨房。RAG 告诉你怎么做,LangChain 给你工具做。

RAG LangChain
本质 架构思想 / 方法论 开发框架 / 工具包
谁提出的 学术界(Facebook AI, 2020) 一家创业公司
能跑吗 ❌ 只是思路 ✅ 几行代码就能跑

2.2 面试标准回答

"RAG 是一种检索增强生成的架构思想,它定义了'先检索、再增强、最后生成'这个流程。LangChain 是一个 LLM 应用开发框架,它把 RAG 所需的组件------Embedding 模型、向量数据库、ChatModel------封装成了开箱即用的模块。没有 LangChain 也能做 RAG(比如用 LlamaIndex、Haystack,甚至纯 OpenAI SDK),但 LangChain 是目前最流行的实现方案。"

2.3 为什么 LangChain 做 RAG 这么火?

  1. 组件解耦------换模型、换向量库、换检索策略都不改业务逻辑
  2. 链式编排 ------| 管道符串联整个流程,代码清晰
  3. 生态大------内置 700+ 集成,Chroma、Pinecone、FAISS 随便换
  4. 降门槛------不需要手写余弦相似度,不需要自己调 embedding API

三、核心数据结构:Document(知识的最小单元)

3.1 它长这样

js 复制代码
new Document({
  pageContent: '光光是一个活泼开朗的小男孩...',  // ← 会转成向量,用来搜索
  metadata: {                                     // ← 不转向量,用来过滤
    chapter: 1,
    character: '光光',
    type: '角色介绍',
    mood: '活泼'
  }
})

3.2 pageContent vs metadata(面试高频区别)

pageContent metadata
做 embedding? ✅ 是 ❌ 否
参与搜索? ✅ 语义匹配"像不像" ❌ 精确匹配"是不是"
存什么 文本正文 标签:章节、作者、来源
搜索方式 向量相似度(模糊) 等号匹配(精确)
类比 书的正文 书的书脊标签

🔑 面试官追问:"metadata 为什么不也做向量化?"

答:没必要。metadata 是精确标签("第1章"、"角色=光光"),用等号就能解决,绕远路做向量化既浪费资源又不精确。两种搜索各司其职------向量搜"像不像",标签筛"是不是"。


四、手写完整 RAG 代码(逐行讲解)

技术栈:Node.js + LangChain + 阿里云 DashScope(OpenAI 兼容接口)

4.1 环境准备

bash 复制代码
npm install @langchain/openai @langchain/core @langchain/classic dotenv

.env 文件:

env 复制代码
MODEL_NAME=qwen-plus
EMBEDDING_MODEL_NAME=text-embedding-v3
OPENAI_API_KEY=你的API_KEY
OPENAI_API_BASE_URL=https://dashscope.aliyuncs.com/compatible/v1

🐛 踩坑:.env 变量名和 process.env.XXX 必须字母完全一致 ,多一个 S 少一个 S 都会读到 undefined,导致 API 404。

4.2 Step 1-2:初始化模型 + 准备知识库

js 复制代码
import 'dotenv/config'
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'
import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory'
import { Document } from '@langchain/core/documents'

// 初始化 LLM(大脑)
const model = new ChatOpenAI({
  temperature: 0,                    // 0 = 稳定输出,不做随机发散
  model: process.env.MODEL_NAME,     // qwen-plus
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_API_BASE_URL  // 换成阿里云地址
  }
})

// 初始化 Embedding 模型(翻译官:文字→向量)
const embeddings = new OpenAIEmbeddings({
  model: process.env.EMBEDDING_MODEL_NAME,  // text-embedding-v3
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_API_BASE_URL
  }
})

4.3 Step 3:构建知识库文档

我们用"光光和东东"的友情故事作为知识库,一共 7 个 Document:

js 复制代码
const documents = [
  new Document({
    pageContent: '光光是一个活泼开朗的小男孩...他特别擅长踢足球...',
    metadata: { chapter: 1, character: '光光', type: '角色介绍', mood: '活泼' }
  }),
  new Document({
    pageContent: '东东是光光最好的朋友...从幼儿园就认识了...',
    metadata: { chapter: 2, character: '东东', type: '角色介绍', mood: '温馨' }
  }),
  // ... 共 7 个 Document
]

4.4 Step 4:Embed + Store(一行代码完成向量化 + 存储)

js 复制代码
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings)

这一行代码背后做了两件事:

步骤 做了什么
Embed 把每个 Document 的 pageContent 调 embedding API 转成向量
Store 把向量 + 原 Document 一起存入内存

4.5 Step 5:创建检索器 + 检索

js 复制代码
// 把向量库包装成标准检索器接口
const retriever = vectorStore.asRetriever({ k: 3 })

// 检索最相关的 3 条文档
const question = '东东和光光是怎么成为朋友的'
const docs = await retriever.invoke(question)

retriever.invoke() 内部做了什么?

scss 复制代码
问题字符串 → embeddings.embedQuery() → 问题向量 → 计算余弦距离 → 返回 Top-K Document

4.6 彩蛋:带分数检索(调试神器)

js 复制代码
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3)
// 返回:[[Document, 0.23], [Document, 0.45], [Document, 0.61]]
//       ↑ 文档              ↑ 分数(越小越相似)

// 转成正向相似度
const similarity = (1 - score).toFixed(4)  // 0.85 = 很相关

4.7 Step 6:Augment + Generate(增强 + 生成)

js 复制代码
// Augment:把检索到的文档拼成一段 context
const context = docs
  .map((doc, i) => `[片段${i}]\n ${doc.pageContent}`)
  .join('\n\n-----\n\n')

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

故事片段:
${context}

问题:${question}

老师的回答:`

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

到这里,一个完整的 RAG 就跑通了!🎉


五、5 个你必须搞清的区别(面试重点)

区别 1:retriever.invoke() vs vectorStore.similaritySearch()

retriever.invoke(q) vectorStore.similaritySearch(q, k)
返回 [Document, ...] [Document, ...]
k 值 创建时锁死 asRetriever({k:3}) 每次调用时灵活传
能否链式 ` ` ✅ 可以
抽象层级 (Retriever 标准接口) (VectorStore 原生方法)

类比:retriever 是餐厅的"套餐"(k 已定好),vectorStore 是可以"单点"(每次自己选 k)。

区别 2:similaritySearch() vs similaritySearchWithScore()

similaritySearch similaritySearchWithScore
返回 [Document, Document, Document] [[Document, 0.85], [Document, 0.72], ...]
带分数
场景 生产环境,只关心内容 调试 / 过滤低分 / 评估检索质量

区别 3:pageContent vs metadata

pageContent metadata
做 embedding?
搜索方式 向量相似度 精确匹配(像 SQL WHERE)
搜索语义 "意思相近" "是不是等于"
js 复制代码
// 实战配合
const results = await vectorStore.similaritySearch('光光的朋友是谁', 5, {
  filter: { chapter: 1 }  // metadata 精确筛选:只看第1章
})
// 先 metadata 筛 → 再 pageContent 向量搜

区别 4:MemoryVectorStore vs 向量数据库

MemoryVectorStore Chroma / Pinecone / FAISS
数据位置 内存 磁盘
持久化 ❌ 程序关了数据就没了 ✅ 持久保存
适用场景 学习 / 原型 / 数据量小 生产环境 / 数据量大
配置 零配置 需要安装部署

区别 5:retriever.invoke() vs 直接调 vectorStore

retriever 不只是简单转发------它会做额外处理:

操作 retriever 裸 vectorStore
向量查询
去重
过滤
重排序(rerank)

retriever = vectorStore + 去重 + 过滤 + rerank。面试时能说出这个区别,会加分。


六、LCEL 链式调用(进阶但实用)

手动写法每步都要接数据、传数据,代码越长越乱。LangChain 的 LCEL(LangChain Expression Language)用 | 管道把步骤串起来:

js 复制代码
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { StringOutputParser } from '@langchain/core/output_parsers'
import { RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables'

// 一条链穿到底
const chain = RunnableSequence.from([
  {
    context: retriever,                // 自动检索
    question: new RunnablePassthrough() // 原样传递问题
  },
  ChatPromptTemplate.fromTemplate(`
    根据以下资料回答:{context}\n问题:{question}
  `),
  model,                               // LLM 生成
  new StringOutputParser()             // 转纯文本
])

// 一行调用
const answer = await chain.invoke('东东和光光是怎么成为朋友的')

对比:

scss 复制代码
手动版: question → retriever.invoke() → 拿到docs → 手动拼prompt → model.invoke() → 拿到结果
链式版: question → [ retriever | prompt | model | outputParser ] → 最终结果

七、完整 RAG 流程图(可以写进简历)

scss 复制代码
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Load    │ →  │  Embed   │ →  │  Store   │ →  │ Retrieve │ →  │ Generate │
│  加载文档 │    │  向量化   │    │  存向量库 │    │  语义搜索 │    │  LLM生成 │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘
     ↑               ↑               ↑               ↑               ↑
  Document       Embedding       VectorStore      Retriever       ChatModel
  (知识单元)      (翻译官)        (笔记本)         (检索器)         (大脑)

八、面试快速自测 5 题

遮住答案,看能答对几题。

1. RAG 全称是什么?三个字母分别做什么?
答案

  • Retrieval 检索:把问题转成向量,去向量库找最相似的文档
  • Augmented 增强:把搜到的文档拼进 prompt 当参考资料
  • Generation 生成:LLM 基于参考资料 + 问题,生成回答

2. Document 的两个字段,哪个做 embedding?哪个不参与搜索?
答案

  • pageContent:做 embedding,参与向量相似度搜索(搜"像不像")
  • metadata:不做 embedding,用于精确过滤(筛"是不是")

3. fromDocuments() 一行代码背后做了哪两步?
答案

  1. Embed :把每个 Document 的 pageContent 调 embedding API 转成向量
  2. Store:把向量 + 原 Document 一起存入向量库

4. retriever.invoke()vectorStore.similaritySearch() 有什么区别?
答案

  • 返回值一样,但 retriever 的 k 值在 asRetriever({k}) 时就锁定了,similaritySearch 每次调用都可传 k
  • retriever 实现了标准 Retriever 接口,能链式调用 |similaritySearch 是 VectorStore 原生方法,不能链式
  • retriever 额外做了去重、过滤、rerank 等处理

5. RAG 和 LangChain 是什么关系?
答案

RAG 是架构思想(方法论),LangChain 是实现框架(工具包)。RAG 定义"先检索再生成"的流程,LangChain 提供 ChatOpenAI、Embeddings、VectorStore 等组件把流程落地。RAG 也可以不用 LangChain 实现(LlamaIndex、Haystack、纯 SDK),但 LangChain 是最流行的实现方案。


九、总结:一句口诀记住 RAG

先存后搜,搜了拼,拼了答。

css 复制代码
存:文档 → Embedding → 向量库
搜:问题 → Embedding → 相似度 → Top-K 文档
拼:相关文档 → Prompt 模板
答:Prompt → LLM → 回答

代码核心就 5 行:

js 复制代码
// 1. 存
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings)

// 2. 搜
const retriever = vectorStore.asRetriever({ k: 3 })
const docs = await retriever.invoke('你的问题')

// 3. 拼
const context = docs.map(d => d.pageContent).join('\n')

// 4. 答
const answer = await model.invoke(`根据:${context}\n回答:${question}`)

这篇文章的完整代码见我的 GitHub:链接,跑通之后你会发现 RAG 其实就这么简单。面试别再被问住了! 🚀

相关推荐
众人皆醒我独醉2 小时前
etcd,其实不是你以为的"数据库"——Kubernetes 大脑的六个反直觉真相
面试·kubernetes
Hyyy2 小时前
Electron-Vite是什么,和Vite有什么区别
前端·面试
兰令水4 小时前
hot100【acm版】【2026.7.14打卡-java版本】
java·数据结构·算法·leetcode·面试
SamDeepThinking6 小时前
Java面向对象在JVM里怎么实现
java·后端·面试
不简说8 小时前
JS 代码技巧 vol.1 — 10 个让代码少写 30% 的小套路
前端·javascript·面试
Mr_凌宇8 小时前
# AI 应用工程入门学习笔记 一
人工智能·面试·程序员
Mr_凌宇8 小时前
AI 应用工程进阶扩展学习笔记 二
前端·面试
Hyyy8 小时前
Git Worktree 完全讲解
前端·后端·面试
云技纵横9 小时前
Redis 延迟双删还是读到脏数据?一次缓存一致性事故
redis·后端·面试