RAG (检索增强生成) 核心原理与实战总结

本文档总结了 RAG (Retrieval-Augmented Generation) 的核心概念、关键组件以及如何构建本地知识库并接入大模型 Agent 的实战经验。通过 RAG,我们可以有效解决大模型的知识幻觉、知识滞后以及缺乏企业私有数据等痛点。
一、核心架构:RAG 的标准工作流
RAG 系统的核心目标是“先检索,再生成”,其标准流程主要包含以下三个核心阶段:
- 数据索引 (Indexing):将各种格式的私有文档(PDF、Markdown、Word 等)读取进来,切分成小块 (Chunks),然后使用 Embedding 模型将其转化为向量,并存储到向量数据库中。
- 检索 (Retrieval):当用户提出问题时,同样使用 Embedding 模型将问题转化为向量,然后在向量数据库中进行相似度检索,召回最相关的文档块。
- 生成 (Generation):将用户的问题和检索到的相关文档块组装成 Prompt,交给大模型(如 GPT-4、Claude 等),让大模型基于这些背景知识生成准确的回答。
二、关键组件选型与实践
1. Embedding 模型与向量化原理
在 RAG 系统中,向量化(Embedding)是将非结构化数据转化为大模型能够理解的形式的关键步骤。
- 什么是向量化:简单来说,向量化就是将文本(文字、词语或句子)映射为一个多维实数数组(即向量)。这个多维数组表示了该文本在语义空间中的位置。
- 语义相似度:在语义空间中,含义相近的文本对应的向量距离会更近。例如,“苹果”和“香蕉”的向量距离,会比“苹果”和“汽车”的距离更近。这使得我们能通过数学计算(如余弦相似度、欧氏距离)来判断两段文本的相关性。
- 为什么需要向量化:计算机无法直接理解文字,但可以快速处理数字和矩阵。通过向量化,我们将复杂的语义检索问题转化为了数学上的向量距离计算问题,从而实现了高效、精准的相似内容召回。
- 常见模型:OpenAI 的
text-embedding-3-small/large,开源领域的 BGE (BAAI General Embedding)、M3E 等。
2. Token 计算与分词器 (Tokenizer)
在将文本送入大模型或进行 Embedding 之前,我们需要将其切分为 Token。Token 是大模型处理文本的基本单位,不同的模型往往使用不同的分词方案(Encoding)。
- 实战经验:为了准确预估大模型 API 的消耗(通常按 Token 计费)并控制输入文本的长度以免超出模型的 Context Window,在本地通过代码进行 Token 计算是非常重要的一步。
- 工具推荐:我们可以使用
js-tiktoken库在 Node.js 环境下获取模型对应的分词方案并计算 Token。
示例代码(获取 gpt-4o 模型的 Encoding):
import { getEncodingNameForModel, getEncoding } from 'js-tiktoken'
const modelName = 'gpt-4o'
// 获取模型对应的分词编码方案名称,例如 'o200k_base'
const encodingName = getEncodingNameForModel(modelName)
console.log(encodingName);3. 数据加载与文档拆分 (Loaders & Text Splitters)
在实际业务中,我们获取的数据往往是网页、PDF 或大段文本。直接将其进行向量化会导致上下文过长,因此需要加载并将其拆分为较小的“块”(Chunks)。
Document Loaders (数据加载器):用于将不同格式的外部数据转换为框架可识别的
Document对象。例如,在代码中我们使用了CheerioWebBaseLoader(依赖cheerio)来直接从给定的网页 URL 获取特定选择器下的 HTML 内容,并转化为纯文本。Text Splitters (文本拆分器):长文本需要切片。常用的有
RecursiveCharacterTextSplitter。chunkSize: 设定切片的最大字符数(比如 500)。chunkOverlap: 设定相邻切片之间的重叠字符数(比如 50),这是为了防止一句话在中间被切断而丢失上下文联系。separators: 自定义按照段落、句号、感叹号等标点符号进行优先级切分。
4. 向量存储与检索 (Vector Stores & Retriever)
完成切分和 Embedding 后的数据需要存储起来,以便能够进行相似度搜索。
- MemoryVectorStore:在我们的演示代码中,使用了基于内存的向量存储库。它适合本地调试和快速验证。通过
MemoryVectorStore.fromDocuments,它会自动调用传入的embeddingModel把文本转为向量并存入内存。 - Retriever (检索器):调用
vectorStore.asRetriever({ k: 3 })可以把存储库转化为一个检索器接口,k: 3表示当传入用户问题时,它会召回最相似的 3 个文档块 (Top-K 召回)。
5. 组装 Prompt 与 LLM 生成 (Prompt Assembly & Generation)
检索出相关文档(Context)后,我们需要把它们拼接成字符串,并结合用户的问题,组成最终喂给大模型(如 GPT-4)的 Prompt。
- 上下文格式化:例如
utils.ts中的formatDocumentsAsString方法,将多个检索到的文档数组拼接为一个带有[片段1]、[片段2]标记的长字符串。 - 防止幻觉:在 Prompt 中,我们通常需要明确指示大模型:“如果背景知识中没有提及相关内容,请直接回答‘基于当前知识库我不知道’,千万不要编造答案”。这极大程度地遏制了大模型的“幻觉”(Hallucination)。
- 生成回答:最后调用
model.invoke(prompt)让大模型进行总结回答。整个从读取、向量化、检索到生成的 RAG 链路就此闭环。

三、实战进阶:接入生产级向量库与电子书处理
随着数据量的增加,基于内存的 MemoryVectorStore 已无法满足需求,因此我们将项目升级为使用专业的向量数据库(如 Milvus),并打通了处理整本电子书(EPUB)的复杂链路。
1. Milvus 向量数据库接入与封装
在工业级 RAG 应用中,稳定且可扩展的向量数据库是核心基础设施。
- 客户端复用:为了避免在每个任务中重复创建连接,我们将
MilvusClient的实例化逻辑提取到了公共模块(如model.ts)中,对外导出一个统一的createMilvusClient函数供整个项目复用。 - 集合与模式 (Collection & Schema):在存储复杂的业务数据时,不能只存向量和文本。在创建集合时,我们定义了丰富的元数据字段(如
book_id,book_name,chapter_num,index等),这为后续进行基于标量的混合检索(如“仅搜索《长安的荔枝》第三章的内容”)打下了基础。 - 索引策略 (Indexing):我们使用了
IVF_FLAT索引类型和COSINE(余弦相似度) 距离算法,在保证精准度的同时兼顾检索效率。
2. 长文本解析:EPUB 电子书加载与清理
相较于纯文本或简单网页,解析整本电子书通常会面临更多环境和数据清洗问题。
- 动态依赖问题:使用 LangChain 的
EPubLoader读取电子书时,其底层实际上是动态依赖epub2(用于解析 EPUB 目录和文件结构)以及html-to-text(用于剥离 HTML 标签,清洗出干净的纯文本)。在开发过程中需注意主动补齐这些外部依赖。 - 按章加载 (
splitChapters):利用EPubLoader提供的方法参数{ splitChapters: true },我们可以将一整本 200 多 KB 的书籍初步拆分为独立的章节,天然保留了书籍最原始的宏观逻辑结构。
3. 文档二次拆分与公共方法抽离
- 核心逻辑抽离:我们将诸如
getVector(调用 Embedding 模型将文本转化为向量数组)的高频基础方法抽离到了utils.ts工具库中,使得各业务脚本的代码结构更加纯粹、高内聚。 - 为什么需要二次切分? 尽管按章节加载保留了逻辑,但单章内容的长度动辄数千字,若直接交由 Embedding 模型转换,极易产生“特征失真”,并且召回时也会迅速占满大模型的上下文 Token 额度。
- 小说类切片(Chunking)的最佳实践:对于小说或叙事类文本,我们继续使用
RecursiveCharacterTextSplitter进行了细粒度的二次切割:chunkSize: 500:500 字的切块是一个很好的“甜点区间”。它能容纳 2 到 3 个段落,既保证了剧情语义的饱满度,又足够聚焦。chunkOverlap: 100:相较于说明文档,小说对于上下文的连贯性要求极高。将重叠度提升至 100 个字符,能有效防止人物对话或连贯的动作描写被生硬切断,使得最终被召回的多个文本片段在逻辑上依旧能平滑衔接。
4. 学习与实践推荐
在学习和实践 RAG 以及向量数据库的过程中,如果想要免去本地繁琐的数据库部署与环境配置,快速上手体验,推荐大家使用 Zilliz Cloud 来进行学习。作为 Milvus 的原厂全托管云服务版本,它提供了开箱即用的高性能向量数据库体验,能够让你把更多精力专注在 RAG 核心链路的开发上,非常适合开发者进行测试以及快速验证想法。
