手把手教你用 RAG 打造专属知识库问答系统

目录

  1. 一、RAG简介
  2. 二、Embedding模型选择
  3. 三、RAG实战
  4. 四、LangChain中的问答链
  5. 五、如果LLM可以处理无限上下文,RAG还有意义吗?

大模型能力强大,却受限于静态知识库,还容易"一本正经地胡说八道"。RAG(检索增强生成)正是破局关键!本文深入浅出讲解 RAG 的核心原理------通过实时检索外部知识库,为生成模型注入最新、最准的上下文信息,显著提升回答的准确性、专业性与可解释性。

带你从零搭建本地 RAG 系统:使用 PyPDF2 解析 PDF、DashScope 嵌入模型向量化、Faiss 构建高效检索库,并通过 LangChain 调用 DeepSeek 大模型实现智能问答。全流程覆盖数据预处理、索引构建到问答生成,附关键技术细节与工程实践建议。

无论你是开发者、AI 爱好者,还是企业技术决策者,这篇干货都能帮你快速掌握 RAG 落地的核心能力!


一、RAG简介

1. 什么是RAG?

RAG(Retrieval-Augmented Generation):

检索增强生成,是一种结合信息检索(Retrieval)和文本生成(Generation)的技术。

RAG技术通过实时检索相关文档或信息,并将其作为上下文输入到生成模型中,从而提高生成结果的时效性和准确性。

2. RAG的优势是什么?

  • 解决知识时效性问题

    大模型的训练数据通常是静态的,无法涵盖最新信息,而RAG可以检索外部知识库实时更新信息。

  • 减少模型幻觉

    通过引入外部知识,RAG能够减少模型生成虚假或不准确内容的可能性。

  • 提升专业领域回答质量

    RAG能够结合垂直领域的专业知识库,生成更具专业深度的回答。

  • 生成内容的溯源(可解释性)

3. RAG 的核心原理与流程

步骤1:数据预处理,构建索引库

  • 知识库构建:

    收集并整理文档、网页、数据库等多源数据,构建外部知识库。

  • 文档分块:

    将文档切分为适当大小的片段(chunks),以便后续检索。分块策略需要在语义完整性与检索效率之间取得平衡。

  • 向量化处理

    使用嵌入模型(如BGE、M3E、ChineseAlpaca2等)将文本块转换为向量,并存储在向量数据库中

步骤2:检索阶段

  • 查询处理

    将用户输入的问题转换为向量,并在向量数据库中进行相似度检索,找到最相关的文本片段。

  • 重排序

    对检索结果进行相关性排序,选择最相关的片段作为生成阶段的输入。

步骤3:生成阶段

  • 上下文组装

    将检索到的相关文档片段与用户问题组合,形成完整的上下文。

  • 生成回答

    将组合后的上下文输入到生成模型中,生成最终答案。

💡 划重点: RAG本质上就是重构了一个新的 Prompt!

4. NativeRAG

NativeRAG的步骤:

  • Indexing

    如何更好地把知识存起来。

  • Retrieval

    如何在大量的知识中,找到一小部分有用的,给到模型参考。

  • Generation

    如何结合用户的提问和检索到的知识,让模型生成有用的答案。

⚠️ 提示: 上面三个步骤虽然看似简单,但在 RAG 应用从构建到落地实施的整个过程中,涉及较多复杂的工作内容!

5. Prompt vs RAG vs Fine-tuning 什么时候使用?


二、Embedding模型选择

1. 通用文本嵌入模型

BGE-M3(智源研究院)

  • 特点: 支持100+语言,输入长度达8192tokens,融合密集、稀疏、多向量混合检索,适合跨语言长文档检索。
  • 适用场景: 跨语言长文档检索、高精度RAG应用。
  • 文件大小: 2.3G

text-embedding-3-large(OpenAI)

  • 特点: 向量维度3072维,长文本语义捕捉能力强,英文表现优秀。
  • 适用场景: 英文内容优先的全球化应用。

jina-embedding-v2-small(Jina AI)

  • 特点: 参数量仅35M,支持实时推理(RT<50ms),适合轻量化部署。
  • 适用场景: 轻量级文本处理、实时推理任务。

2. 中文嵌入模型

M3E-Base

  • 特点: 针对中文优化的轻量模型,适合本地化部署。
  • 适用场景: 中文法律、医疗领域检索任务。
  • 文件大小: 0.4G

xiaobu-embedding-v2

  • 特点: 针对中文语义优化,语义理解能力强。
  • 适用场景: 中文文本分类、语义检索。

stella-mrl-large-zh-v3.5-1792

  • 特点: 处理大规模中文数据能力强,捕捉细微语义关系。
  • 适用场景: 中文文本高级语义分析、自然语言处理任务。

3. 指令驱动与复杂任务模型

gte-Qwen2-7B-instruct(阿里巴巴)

  • 特点: 基于Qwen大模型微调,支持代码与文本跨模态检索。
  • 适用场景: 复杂指令驱动任务、智能问答系统。

E5-mistral-7B(Microsoft)

  • 特点: 基于Microsoft架构,Zero-shot任务表现优异。
  • 适用场景: 动态调整语义密度的复杂系统。

4. 企业级与复杂系统

BGE-M3(智源研究院)

  • 特点: 适合企业级部署,支持混合检索。
  • 适用场景: 企业级语义检索、复杂RAG应用。

E5-mistral-7B(Microsoft)

  • 特点: 适合企业级部署,支持指令微调。
  • 适用场景: 需要动态调整语义密度的复杂系统。

5. CASE-BGE-M3使用

模型下载

python 复制代码
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel('/root/models/BAAI/bge-m3',
                       use_fp16=True)

示例代码

python 复制代码
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel('/root/models/BAAI/bge-m3',
                       use_fp16=True)
sentences_1 = ["什么是 BGE M3?", "BM25 的定义"]
sentences_2 = ["BGE M3 是一种嵌入模型,支持稠密检索、词汇匹配和多向量交互。",
               "BM25 是一种基于词袋的检索函数,根据查询词在每个文档中出现的情况对文档集合进行排序。"]
embeddings_1 = model.encode(sentences_1,
                            batch_size=12,
                            max_length=8192,  # 如果不需要这么长的长度,可以设置一个较小的值以加快编码速度。
                            )['dense_vecs']
embeddings_2 = model.encode(sentences_2)['dense_vecs']
similarity = embeddings_1 @ embeddings_2.T
print(similarity)
# [[0.6265, 0.3477], [0.3499, 0.678 ]]

代码解释

  • similarity = embeddings_1 @ embeddings_2.T

    计算两组嵌入向量(embeddings)之间的相似度矩阵。
    embeddings_1包含了第一组句子(sentences_1)的嵌入向量,形状为[sentences_1的数量,嵌入维度]
    embeddings_2包含了第二组句子(sentences_2)的嵌入向量,形状为[sentences_2的数量,嵌入维度]
    embeddings_2.T是对embeddings_2进行转置操作,形态变为[嵌入维度,sentences_2的数量]

  • @符号在Python中表示矩阵乘法运算

    通过矩阵乘法计算了两组句子之间的余弦相似度矩阵。结果similarity形状是[sentences_1的数量,sentences_2的数量]

6. gte-qwen2使用

模型下载

python 复制代码
from sentence_transformers import SentenceTransformer
model_dir = "/root/models/iic/gte_Qwen2-1___5B-instruct"
model = SentenceTransformer(model_dir, trust_remote_code=True)

示例代码

python 复制代码
from sentence_transformers import SentenceTransformer
model_dir = "/root/models/iic/gte_Qwen2-1___5B-instruct"
model = SentenceTransformer(model_dir, trust_remote_code=True)
model.max_seq_length = 8192
queries = [    "女性每天应该摄入多少蛋白质?",    ""summit"的定义是什么?",]
documents = [    "根据一般指南,美国疾控中心(CDC)建议19至70岁女性每日平均蛋白质摄入量为46克。但如下面图表所示,如果你正在备孕或备战马拉松,就需要增加摄入量。请查看下方图表,了解你每天应摄入多少蛋白质。",    ""summit"对英语学习者的定义:1. 山的最高点;山顶。2. 最高水平。3. 两个或多个国家领导人之间的会议或系列会议。",]
document_embeddings = model.encode(documents)
print(scores.tolist())

三、RAG实战

基于LangChain,采用DeepSeek+Faiss快速搭建本地知识库检索。

3.1 环境准备

  1. 本地安装好Conda环境

  2. 推荐使用阿里大模型平台百炼:
    bailian.console.aliyun.com/

  3. 百炼平台使用:注册登录、申请api key。

3.2 技术选型

  • 向量数据库: Faiss作为高效的向量检索
  • 嵌入模型: 阿里云DashScope的text-embedding-v1
  • 大语言模型: deepseek-v3
  • 文档处理: PyPDF2用于PDF文本提取

3.3 程序逻辑结构介绍

步骤1:文档预处理

PDF文件 → 文本提取 → 文本分割 → 页码映射

1) PDF文本提取

  • 逐页提取文本内容
  • 记录每行文本对应的页码信息
  • 处理空页和异常情况

2)文本分割策略

  • 使用递归字符分割器
  • 分割参数:chunk_size=1000, chunk_overlap=200
  • 分割优先级:段落 → 句子 → 空格 → 字符

3)页面映射处理

  • 基于字符位置计算每个文本块的页面
  • 使用众数统计确定文本块的主要来源页码
  • 建立文本块与页码的映射关系

步骤2:知识库构建

文本块 → 嵌入向量 → Faiss索引 → 本地持久化

1)向量数据库构建

  • 使用DashScope嵌入模型生成向量
  • 将向量存储到Faiss索引结构

2)数据持久化

  • 保存Faiss索引文件(.faiss)
  • 保存元数据信息(.pkl)
  • 保存页码映射关系(page_info.pkl)

步骤3:问答查询

用户问题 → 向量检索 → 文档组合 → LLM生成 → 答案输出

1)相似度检索

  • 将用户问题转换为向量
  • 在Faiss中搜索最相似的文档块,返回Top-K相关文档

2)问答链处理

  • 使用LangChain的load_qa_chain
  • 采用stuff策略组合文档
  • 将组合后的上下文和问题发送给LLM

3)答案生成与展示

3.4 代码实例

安装依赖的包

bash 复制代码
!pip install pypdf2
!pip install dashscope
!pip install langchain
!pip install langchain-openai
!pip install langchain-community
!pip install faiss-cpu

导入依赖的包

python 复制代码
from PyPDF2 import PdfReader
from langchain.chains.question_answering import load_qa_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import FAISS
from typing import List, Tuple
import os
import pickle

从PDF中提取文本并记录每个字符对应的页码

python 复制代码
def extract_text_with_page_numbers(pdf) -> Tuple[str, List[Tuple[str, int]]]:
    """
    从PDF中提取文本并记录每个字符对应的页码
    参数:
        pdf: PDF文件对象
    返回:
        text: 提取的文本内容
        char_page_mapping: 每个字符对应的页码列表
    """
    text = ""
    char_page_mapping = []
    for page_number, page in enumerate(pdf.pages, start=1):
        extracted_text = page.extract_text()
        if extracted_text:
            text += extracted_text
            # 为当前页面的每个字符记录页码
            char_page_mapping.extend([page_number] * len(extracted_text))
        else:
            print(f"No text found on page {page_number}.")
    return text, char_page_mapping

处理文本并创建向量存储

python 复制代码
def process_text_with_splitter(text: str, char_page_mapping: List[int], save_path: str = None) -> FAISS:
    """
    处理文本并创建向量存储
    参数:
        text: 提取的文本内容
        char_page_mapping: 每个字符对应的页码列表
        save_path: 可选,保存向量数据库的路径
    返回:
        knowledgeBase: 基于FAISS的向量存储对象
    """
    # 创建文本分割器,用于将长文本分割成小块
    text_splitter = RecursiveCharacterTextSplitter(
        separators=["\n\n", "\n", ".", " ", ""],
        chunk_size=1000,
        chunk_overlap=200,
        length_function=len,
    )
    # 分割文本
    chunks = text_splitter.split_text(text)
    print(f"文本被分割成 {len(chunks)} 个块。")
    # 创建嵌入模型
    embeddings = DashScopeEmbeddings(
        model="text-embedding-v1",
        dashscope_api_key=DASHSCOPE_API_KEY,
    )
    # 从文本块创建知识库
    knowledgeBase = FAISS.from_texts(chunks, embeddings)
    print("已从文本块创建知识库。")
    # 为每个文本块找到对应的页码信息
    page_info = {}
    current_pos = 0
    for chunk in chunks:
        chunk_start = current_pos
        chunk_end = current_pos + len(chunk)
        # 找到这个文本块中字符对应的页码
        chunk_pages = char_page_mapping[chunk_start:chunk_end]
        # 取页码的众数(出现最多的页码)作为该块的页码
        if chunk_pages:
            # 统计每个页码出现的次数
            page_counts = {}
            for page in chunk_pages:
                page_counts[page] = page_counts.get(page, 0) + 1
            # 找到出现次数最多的页码
            most_common_page = max(page_counts, key=page_counts.get)
            page_info[chunk] = most_common_page
        else:
            page_info[chunk] = 1  # 默认页码
        current_pos = chunk_end
    knowledgeBase.page_info = page_info
    print(f'页码映射完成,共 {len(page_info)} 个文本块')
    # 如果提供了保存路径,则保存向量数据库和页码信息
    if save_path:
        # 确保目录存在
        os.makedirs(save_path, exist_ok=True)
        # 保存FAISS向量数据库
        knowledgeBase.save_local(save_path)
        print(f"向量数据库已保存到: {save_path}")
        # 保存页码信息到同一目录
        with open(os.path.join(save_path, "page_info.pkl"), "wb") as f:
            pickle.dump(page_info, f)
        print(f"页码信息已保存到: {os.path.join(save_path, 'page_info.pkl')}")
    return knowledgeBase

从磁盘加载向量数据库和页码信息

python 复制代码
def load_knowledge_base(load_path: str, embeddings = None) -> FAISS:
    """
    从磁盘加载向量数据库和页码信息
    参数:
        load_path: 向量数据库的保存路径
        embeddings: 可选,嵌入模型。如果为None,将创建一个新的DashScopeEmbeddings实例
    返回:
        knowledgeBase: 加载的FAISS向量数据库对象
    """
    # 如果没有提供嵌入模型,则创建一个新的
    if embeddings is None:
        embeddings = DashScopeEmbeddings(
            model="text-embedding-v1",
            dashscope_api_key=DASHSCOPE_API_KEY,
        )
    # 加载FAISS向量数据库,添加allow_dangerous_deserialization=True参数以允许反序列化
    knowledgeBase = FAISS.load_local(load_path, embeddings, allow_dangerous_deserialization=True)
    print(f"向量数据库已从 {load_path} 加载。")
    # 加载页码信息
    page_info_path = os.path.join(load_path, "page_info.pkl")
    if os.path.exists(page_info_path):
        with open(page_info_path, "rb") as f:
            page_info = pickle.load(f)
        knowledgeBase.page_info = page_info
        print("页码信息已加载。")
    else:
        print("警告: 未找到页码信息文件。")
    return knowledgeBase

读取PDF文件,并写入到知识库

python 复制代码
# 读取PDF文件
pdf_reader = PdfReader('./考核办法.pdf')
# 提取文本和页码信息
text, char_page_mapping = extract_text_with_page_numbers(pdf_reader)
#print('page_numbers=',page_numbers)
print(f"提取的文本长度: {len(text)} 个字符。")
# 处理文本并创建知识库,同时保存到磁盘
save_dir = "./vector_db"
knowledgeBase = process_text_with_splitter(text, char_page_mapping, save_path=save_dir)

如何加载已保存的向量数据库

python 复制代码
# 创建嵌入模型
embeddings = DashScopeEmbeddings(
    model="text-embedding-v1",
    dashscope_api_key=DASHSCOPE_API_KEY,
)
# 从磁盘加载向量数据库
loaded_knowledgeBase = load_knowledge_base("./vector_db", embeddings)
# 使用加载的知识库进行查询
docs = loaded_knowledgeBase.similarity_search("客户经理每年评聘申报时间是怎样的?")
# 直接使用FAISS.load_local方法加载(替代方法)
# loaded_knowledgeBase = FAISS.load_local("./vector_db", embeddings, allow_dangerous_deserialization=True)
# 注意:使用这种方法加载时,需要手动加载页码信息

对话调用

python 复制代码
from langchain_community.llms import Tongyi
llm = Tongyi(model_name="deepseek-v3", dashscope_api_key=DASHSCOPE_API_KEY) # qwen-turbo
# 设置查询问题
query = "客户经理被投诉了,投诉一次扣多少分"
#query = "客户经理每年评聘申报时间是怎样的?"
if query:
    # 执行相似度搜索,找到与查询相关的文档
    docs = knowledgeBase.similarity_search(query,k=10)
    # 加载问答链
    chain = load_qa_chain(llm, chain_type="stuff")
    # 准备输入数据
    input_data = {"input_documents": docs, "question": query}
    # 执行问答链
    response = chain.invoke(input=input_data)
    print(response["output_text"])
    print("来源:")
    # 记录唯一的页码
    unique_pages = set()
    # 显示每个文档块的来源页码
    for doc in docs:
        #print('doc=',doc)
        text_content = getattr(doc, "page_content", "")
        source_page = knowledgeBase.page_info.get(
            text_content.strip(), "未知"
        )
        if source_page not in unique_pages:
            unique_pages.add(source_page)
            print(f"文本块页码: {source_page}")

3.5 示例总结

1. PDF文本提取与处理

  • 使用PyPDF2库的PdfReader从PDF文件中提取文本在提取过程中记录每行文本对应的页码,便于后续溯源。
  • 使用RecursiveCharacterTextSplitter将长文本分割成小块,便于向量化处理。

2. 向量数据库构建

  • 使用OpenAIEmbeddings / DashScopeEmbeddings将文本块转换为向量表示。
  • 使用FAISS向量数据库存储文本向量,支持高效的相似度搜索为每个文本块保存对应的页码信息,实现查询结果溯源。

3. 语义搜索与问答链

  • 基于用户查询,使用similarity_search在向量数据库中检索相关文本块。
  • 使用文本语言模型和load_qa_chain构建问答链将检索到的文档和用户问题作为输入,生成回答。

4. 成本跟踪与结果展示

  • 使用get_openai_callback跟踪API调用成本。
  • 展示问答结果和来源页码,方便用户验证信息。

四、LangChain中的问答链

在LangChain问答链中,有4种chain_type,chain_type参数决定了RAG流程中,如何将检索到的多个相关文档块与用户的问题组合起来,发送给大模型以生成最终答案。需要根据应用场景,选择不同的chain_type,以保证在效果、成本和速度上的权衡。

1. stuff(堆叠式)

工作原理:

将所有检索到的文档片段直接拼接成一个长上下文,与问题一起构造 Prompt,单次调用 LLM 生成答案。

优点:

  • 实现最简单、延迟最低;
  • 单次LLM调用,成本最低;
  • 所有信息同时可见,利于全局理解。

缺点:

  • 受限于LLM的最大上下文长度,文档过多或过长时会截断;
  • 若检索结果包含噪声,可能干扰生成质量。

适用场景:

  • 检索返回的文档数量少、内容精炼(如chunk_size较小);
  • 对响应速度和成本敏感的轻量级应用。

2. map_reduce(映射-归约)

工作原理:

  • Map阶段: 对每个文档(chunk)独立构造Prompt,分别调用LLM生成中间摘要或回答。
  • Reduce阶段: 将所有中间结果合并,再次调用LLM生成最终答案。

优点:

  • 可处理超长文档集合,不受单次上下文长度限制;
  • Map阶段可并行执行,提升吞吐率;
  • 适合信息分散在多个片段中的场景。

缺点:

  • 至少需要N+1次LLM调用(N为文档数),成本高,延迟大;
  • 各文档独立处理,缺乏文档上下文关联。

适用场景:

  • 检索结果较多且分散;
  • 需要对大量文档进行综合归纳(如报告生成,多源信息整合)。

3. refine(迭代细化)

在第一个chunk上做prompt得到结果,然后再合并下一个文件再输出结果

工作原理:

先用第一个文档和问题生成初步答案;依次将后续文档与当前答案合并,逐步"细化"输出,每次更新答案。

优点:

  • 能在一定程度上保留跨文档上下文信息;
  • 每次只处理一个新文档 + 当前答案,Token使用较为可控;
  • 最终答案融合了所有文档的信息流。

缺点:

  • 必须串行处理,无法并行,延迟较高;
  • 早期错误可能被累积放大;

适用场景:

  • 文档间存在逻辑递进或补充关系;
  • 需要在有限上下文窗口内处理中等数量文档;
  • 对答案连续性要求较高。

4. map_rerank(映射重排序)

对每个chunk做prompt,然后打个分,然后根据分数返回最好的文档中的结果。

会大量地调用LLM,每个document之间是独立处理。

工作原理:

对每个文档独立构造Prompt,要求LLM输出一个答案+置信度分数;根据分数排序,返回得分最高的答案(通常只取Top-1)。

优点:

  • 能有效过滤低质量或无关文档;
  • 返回的答案来自最相关片段,精准度高。

缺点:

  • 每个文档都需要调用一次LLM,成本高;
  • 不副合多文档信息,仅返回单个最佳答案,可能丢失互补信息;
  • 依赖LLM对"打分"的一致性,部分模型不擅长此任务。

适用场景:

  • 检索结果中存在明显"最佳匹配"文档;
  • 任务为事实型问答(如"XX 的定义是什么?");
  • 需要高置信度、简洁答案,而非综合归纳。

5. 总结

特性 stuff map_reduce refine map_rerank
LLM调用次数 1 N + 1 N N
速度 最快 中等 最慢
Token 消耗
处理长文档能力
答案质量 高 (上下文完整) 中等 (可能丢失关联) 最高 (迭代细化) 取决于最佳块
适用场景 默认选择,文档短 文档多,需平衡 文档多,要求高精度 事实型问题,答案在单块中

五、如果LLM可以处理无限上下文,RAG还有意义吗?

  • 效率成本

    LLM处理上下文时计算资源消耗大,响应时间增加。RAG通过检索相关片段,减少输入长度。

  • 知识更新

    LLM的知识截止于训练数据,无法实时更新。RAG可以连接外部知识库,增强时效性。

  • 可解释性

    RAG的检索过程透明,用户可查看来源,增强信任。LLM的生成过程则较难追溯。

  • 定制化

    RAG可针对特定领域定制检索系统,提供更精准的结果,而LLM的通用性可能无法满足特定需求。

  • 数据隐私

    RAG允许在本地或私有数据源上检索,避免敏感数据上传云端,适合隐私要求高的场景。

结合LLM的生成能力和RAG的检索能力,可以提升整体性能,提供更全面、准确的回答。


如果你觉得本文有帮助,欢迎点赞、在看、转发,也欢迎留言分享你的经验!

往期文章回顾:

相关推荐
喵个咪6 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:深度解析 Wire 依赖注入集成实践
后端·go
回家路上绕了弯6 小时前
代码的三大核心素养:如何同时兼顾可维护性、可扩展性、可测试性
分布式·后端
快手技术6 小时前
入围AA总榜Top 10,Non-Reasoning Model榜单第一!KAT-Coder-Pro V1 新版本踏浪归来!
前端·后端·前端框架
小坏讲微服务6 小时前
Spring Boot4.0整合RabbitMQ死信队列详解
java·spring boot·后端·rabbitmq·java-rabbitmq
苏三说技术6 小时前
工作中最常用的6种缓存
后端
tc&6 小时前
Kamailio SIP脚本编程核心概念总结
后端·kamailio
考虑考虑7 小时前
maven项目使用指定JDK版本打包
java·后端·maven
咕白m6257 小时前
使用 C# 设置 Word 段落对齐样式
后端·c#
Kiri霧7 小时前
Go切片详解
开发语言·后端·golang