前言
2024 年以来,大模型应用开发中最火的技术栈非 RAG 莫属。RAG(Retrieval Augmented Generation,检索增强生成)已经成为 LLM 落地的最佳实践之一——它让大模型能够"带着答案回答问题",有效解决了幻觉、知识过时、私有数据无法利用等痛点。
但对于刚入门的朋友来说,RAG 的概念和工具链往往让人望而生畏:Embedding、向量数据库、Retriever……这些名词到底是什么意思?LangChain 又帮我们做了什么?
本文将通过一个完整的、可运行的 Demo,带你从零理解 RAG 的核心原理,并用 LangChain + OpenAI 亲手搭建你的第一个 RAG 应用。读完本文,你将对 RAG 的完整工作流有一个清晰的认知。
完整代码可在文末找到,直接复制运行即可。
一、什么是 RAG?一句话理解
传统的 LLM 就像一个闭卷考试的考生——它只能凭借训练时"记住"的知识来回答问题。而 RAG 让它变成了开卷考试:先从一个知识库中检索出相关内容,再把检索到的内容连同问题一起交给 LLM,让它基于这些"参考资料"来生成答案。
RAG 的三大核心步骤:
- Retrieval(检索):从知识库中找到与用户问题最相关的文档片段
- Augmentation(增强):将检索到的内容作为上下文注入 Prompt
- Generation(生成):LLM 基于增强后的 Prompt 生成答案
这三个步骤环环相扣,缺一不可。下面我们来看看如何用代码实现。
二、技术选型与环境准备
本文 Demo 使用的技术栈:
| 组件 | 选型 | 作用 |
|---|---|---|
| LLM | ChatOpenAI | 最终生成回答的大模型 |
| Embedding 模型 | OpenAIEmbeddings | 将文本转换为向量 |
| 向量存储 | MemoryVectorStore | 内存中的向量数据库(适合学习) |
| 框架 | LangChain | 统一编排上述组件 |
环境变量配置
首先在项目根目录创建 .env 文件:
OPENAI_API_KEY=your-api-key
OPENAI_BASE_URL=https://api.openai.com/v1 # 或其他兼容端点
MODEL_NAME=gpt-3.5-turbo # 对话模型
EMBEDDING_MODEL_NAME=text-embedding-ada-002 # 向量化模型
安装依赖:
npm install @langchain/openai @langchain/classic @langchain/core dotenv
三、核心概念:Document —— 知识的最小单元
在 LangChain 中,知识库由 Document 对象构成。每个 Document 包含两个字段:
{
pageContent: '要单独做 embedding 的文本内容',
metadata: {
// 元数据,不参与向量化,但可用于后续过滤和溯源
chapter: 1,
character: '光光',
type: '角色介绍',
mood: '活泼'
}
}
- pageContent:实际会被 Embedding 模型向量化的文本,也是检索时匹配的核心内容
- metadata:附属信息,不做向量化,但在检索结果展示和过滤时非常有用(比如标注来源链接、作者、章节等)
这是一个非常重要的设计模式——将语义搜索的内容和业务元数据分离。向量化只关心"说了什么",而 metadata 告诉你"谁说的、在哪说的、什么时候说的"。
本 Demo 构建了一个关于"光光和东东友谊"的小故事作为知识库,包含 7 个 Document,涵盖了角色介绍、友情发展、高潮转折到结局的完整叙事。
四、知识入库:Embedding + Vector Store
有了 Document,下一步就是把它们存入向量数据库。这个过程分为两步:
1. Embedding——文本转向量
Embedding 模型将文本映射为高维空间中的向量(通常 1536 维),语义相近的文本在向量空间中的距离也更近。这是整个 RAG 的数学基础。
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDING_MODEL_NAME,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
}
})
2. MemoryVectorStore——存入内存向量数据库
const vectorStore = await MemoryVectorStore
.fromDocuments(documents, embeddings)
MemoryVectorStore.fromDocuments() 这个 API 背后做了三件事:
- 遍历所有 Document,对每个
pageContent调用 Embedding 模型 - 将文本向量存入内存中的向量索引
- 返回一个可以进行语义搜索的 vectorStore 实例
对于生产环境,你可以将 MemoryVectorStore 替换为 PGVectorStore(基于 PostgreSQL)、Chroma、Pinecone 等持久化向量数据库,逻辑完全一致。
五、检索器的两种用法:Retriever vs 原生查询
这是很多初学者容易混淆的地方。LangChain 提供了两种检索方式:
方式一:Retriever(推荐)
const retriever = vectorStore.asRetriever({ k: 3 })
const docs = await retriever.invoke(question)
asRetriever() 将向量数据库包装为一个标准的检索器接口。调用 invoke(question) 时,内部自动完成:
- 将 question 转为向量
- 在向量数据库中计算相似度距离
- 返回 Top-K 个最相关的 Document
- 可能还包括去重、过滤、**Rerank(重排序)**等后处理
Retriever 是 LangChain 工作流编排的标准入口,输入问题,输出相关文档列表,干净利落。
方式二:原生相似度查询
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3)
这个方法是向量数据库的底层接口,只做纯粹的向量相似度计算,不做 Rerank、去重等高级处理。它的返回值包含一个 score 字段:
- score 表示向量距离(越小越相似)
- 对于余弦相似度(Cosine Similarity),计算
1 - score可以得到直观的相似度百分比 - score 低说明匹配质量高——高质量的数据 + 精准的向量化是 RAG 效果的关键
一句话总结:Retriever 是高级封装,带后处理能力;
similaritySearchWithScore是底层查询,给你原始的相似度数据。日常开发用 Retriever 就够了。
六、Prompt 增强与生成:RAG 的最后一步
检索到相关文档后,就进入了 Augmented + Generation 阶段。核心思路是将检索结果注入 Prompt:
// 构建上下文
const context = docs
.map((doc, i) => `[片段${i}]\n ${doc.pageContent}`)
.join('\n\n-----\n\n')
// 组装增强后的 Prompt
const prompt = `你是一个讲友情故事的老师。基于以下故事片段回答问题,用温暖生动的语言。如果故事中没有提到,就说"这个故事里还没有提到这个细节"。
故事片段:
${context}
问题:${question}
老师的回答:`
// 调用 LLM 生成
const response = await model.invoke(prompt)
这个 Prompt 的设计有几个值得注意的细节:
- 角色设定:
你是一个讲友情故事的老师——限定 LLM 的输出风格 - 边界约束:
如果故事中没有提到,就说"这个故事里还没有提到这个细节"——这是 RAG 的防幻觉护栏,避免 LLM 在检索不到相关内容时胡编乱造 - 结构清晰:用分隔符将系统指令、上下文、用户问题分开,让 LLM 明确各部分的作用
- 标注来源:每个片段都有编号
[片段0]、[片段1],方便追溯答案来源
七、完整工作流回顾
把以上各环节串联起来,RAG 的完整流程如下:
用户提问 "东东和光光是怎么成为朋友的"
│
▼
┌─────────────┐
│ Retriever │ 将问题转为向量 → 在知识库中语义搜索 → 返回 Top-3 相关文档
└─────────────┘
│
▼
┌─────────────┐
│ Context │ 将检索到的文档拼接为上下文
└─────────────┘
│
▼
┌─────────────┐
│ Prompt │ 系统指令 + 上下文 + 用户问题 → 组合为完整 Prompt
└─────────────┘
│
▼
┌─────────────┐
│ LLM │ ChatOpenAI 基于增强后的 Prompt 生成回答
└─────────────┘
│
▼
温暖生动的友情故事回答
这就是 RAG 的端到端流程。用 LangChain 实现,核心代码不到 30 行。
八、生产环境进阶方向
本文 Demo 使用内存向量存储,适合学习和原型验证。在实际项目中,你需要关注以下几个方面:
1. 持久化向量数据库
将 MemoryVectorStore 替换为 PGVectorStore(PostgreSQL + pgvector 扩展)或 Chroma、Milvus、Pinecone 等专业向量数据库。它们支持持久化存储、大规模数据检索和分布式部署。
2. 文档切分策略
本文直接使用了完整的段落作为 Document,但在实际场景中,你需要用 RecursiveCharacterTextSplitter 等工具对长文档进行语义分块。chunk 太大会稀释语义,太小会丢失上下文——找到平衡点是一门艺术。
3. Rerank 与混合检索
单纯的向量检索在精确关键词匹配上不如传统全文搜索。生产环境常采用混合检索(Hybrid Search):结合向量相似度 + BM25 关键词匹配 + Rerank 模型二次排序,大幅提升召回精度。
4. 对话历史管理
如果构建的是多轮对话 RAG,需要引入 ConversationBufferMemory 或类似机制来管理历史消息,让 LLM 能结合上下文理解用户的追问。
九、总结
本文通过一个完整的 Demo,带你实践了 RAG 的核心流程:
- Document 是知识的最小单元,
pageContent做向量化,metadata存业务信息 - Embedding 将文本转为向量,语义相近的文本在向量空间中距离更近
- Retriever 是 LangChain 的标准检索入口,封装了向量查询 + 后处理
- similaritySearchWithScore 提供原始相似度数据,帮你评估检索质量
- Prompt 增强是将检索结果注入 LLM 的关键一步,边界约束可以有效防幻觉
RAG 不是一个复杂的算法,而是一个工程化的组合模式。理解了这个流程,你就掌握了 LLM 应用开发最重要的基础能力。
AI 发展太快,框架版本更新更快。最好的学习方式是理解原理 + 动手实践。 当你看到 LangChain 又发布了新版本时,不用慌——核心概念不会变,变的只是 API 的调用方式。
希望这篇文章能帮助你在 RAG 的学习之路上少走弯路。如果你觉得有帮助,欢迎点赞收藏,也欢迎在评论区交流你的 RAG 实践经验!
附:完整代码
以下是本文 Demo 的完整代码(src/index.mjs):
import 'dotenv/config'
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'
import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory'
import { Document } from '@langchain/core/documents'
// 1. 初始化 LLM
const model = new ChatOpenAI({
temperature: 0,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL }
})
// 2. 初始化 Embedding 模型
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDING_MODEL_NAME,
configuration: { baseURL: process.env.OPENAI_BASE_URL }
})
// 3. 构建知识库 Documents(7个友情故事片段)
const documents = [
new Document({
pageContent: '光光是一个活泼开朗的小男孩……',
metadata: { chapter: 1, character: '光光', type: '角色介绍', mood: '活泼' }
}),
// ... 更多文档
]
// 4. 向量化并存入内存向量数据库
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings)
// 5. 创建检索器 (Top-3)
const retriever = vectorStore.asRetriever({ k: 3 })
// 6. 检索 + 增强 + 生成
const question = '东东和光光是怎么成为朋友的'
const docs = await retriever.invoke(question)
const context = docs
.map((doc, i) => `[片段${i}]\n ${doc.pageContent}`)
.join('\n\n-----\n\n')
const prompt = `你是一个讲友情故事的老师。基于以下故事片段回答问题……
故事片段:
${context}
问题:${question}
老师的回答:`
const response = await model.invoke(prompt)
console.log(response.content)
本文基于 rag-demo 项目撰写,源码见 GitHub。
发布于稀土掘金,转载请注明出处。