从零搭建 RAG 应用:LangChain + OpenAI 实战入门

0 阅读9分钟

前言

2024 年以来,大模型应用开发中最火的技术栈非 RAG 莫属。RAG(Retrieval Augmented Generation,检索增强生成)已经成为 LLM 落地的最佳实践之一——它让大模型能够"带着答案回答问题",有效解决了幻觉、知识过时、私有数据无法利用等痛点。

但对于刚入门的朋友来说,RAG 的概念和工具链往往让人望而生畏:Embedding、向量数据库、Retriever……这些名词到底是什么意思?LangChain 又帮我们做了什么?

本文将通过一个完整的、可运行的 Demo,带你从零理解 RAG 的核心原理,并用 LangChain + OpenAI 亲手搭建你的第一个 RAG 应用。读完本文,你将对 RAG 的完整工作流有一个清晰的认知。

完整代码可在文末找到,直接复制运行即可。


一、什么是 RAG?一句话理解

传统的 LLM 就像一个闭卷考试的考生——它只能凭借训练时"记住"的知识来回答问题。而 RAG 让它变成了开卷考试:先从一个知识库中检索出相关内容,再把检索到的内容连同问题一起交给 LLM,让它基于这些"参考资料"来生成答案。

RAG 的三大核心步骤:

  1. Retrieval(检索):从知识库中找到与用户问题最相关的文档片段
  2. Augmentation(增强):将检索到的内容作为上下文注入 Prompt
  3. Generation(生成):LLM 基于增强后的 Prompt 生成答案

这三个步骤环环相扣,缺一不可。下面我们来看看如何用代码实现。


二、技术选型与环境准备

本文 Demo 使用的技术栈:

组件选型作用
LLMChatOpenAI最终生成回答的大模型
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 背后做了三件事:

  1. 遍历所有 Document,对每个 pageContent 调用 Embedding 模型
  2. 将文本向量存入内存中的向量索引
  3. 返回一个可以进行语义搜索的 vectorStore 实例

对于生产环境,你可以将 MemoryVectorStore 替换为 PGVectorStore(基于 PostgreSQL)、ChromaPinecone 等持久化向量数据库,逻辑完全一致。


五、检索器的两种用法: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 的设计有几个值得注意的细节:

  1. 角色设定你是一个讲友情故事的老师——限定 LLM 的输出风格
  2. 边界约束如果故事中没有提到,就说"这个故事里还没有提到这个细节"——这是 RAG 的防幻觉护栏,避免 LLM 在检索不到相关内容时胡编乱造
  3. 结构清晰:用分隔符将系统指令、上下文、用户问题分开,让 LLM 明确各部分的作用
  4. 标注来源:每个片段都有编号 [片段0]、[片段1],方便追溯答案来源

七、完整工作流回顾

把以上各环节串联起来,RAG 的完整流程如下:

用户提问 "东东和光光是怎么成为朋友的"
        │
        ▼
  ┌─────────────┐
  │  Retriever   │  将问题转为向量 → 在知识库中语义搜索 → 返回 Top-3 相关文档
  └─────────────┘
        │
        ▼
  ┌─────────────┐
  │   Context    │  将检索到的文档拼接为上下文
  └─────────────┘
        │
        ▼
  ┌─────────────┐
  │   Prompt     │  系统指令 + 上下文 + 用户问题 → 组合为完整 Prompt
  └─────────────┘
        │
        ▼
  ┌─────────────┐
  │    LLM       │  ChatOpenAI 基于增强后的 Prompt 生成回答
  └─────────────┘
        │
        ▼
    温暖生动的友情故事回答

这就是 RAG 的端到端流程。用 LangChain 实现,核心代码不到 30 行。


八、生产环境进阶方向

本文 Demo 使用内存向量存储,适合学习和原型验证。在实际项目中,你需要关注以下几个方面:

1. 持久化向量数据库

MemoryVectorStore 替换为 PGVectorStore(PostgreSQL + pgvector 扩展)或 ChromaMilvusPinecone 等专业向量数据库。它们支持持久化存储、大规模数据检索和分布式部署。

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 发布于稀土掘金,转载请注明出处。