RAG 实战技术文档

核心技术栈

  • 向量数据库 (Vector DB): ChromaDB (v0.5.x) - 轻量级、持久化的本地向量库。

  • 嵌入/召回模型 (Embedding/Retrieval Model): BAAI/bge-m3 - SOTA级开源文本嵌入模型(Bi-Encoder)。

  • 重排模型 (Reranker Model): BAAI/bge-reranker-large - 高精度文本相关性排序模型(Cross-Encoder)。

  • 大语言模型 (LLM): OpenAI/gpt-4o - 用于最终答案生成及评估。

  • 核心库: sentence-transformers, FlagEmbedding, langchain (仅用于文本切分), openai, pypdf, chromadb


〇、环境配置 (Environment Setup)

1. 依赖安装

# 核心依赖
!pip install chromadb sentence-transformers openai pypdf -q
# LangChain 辅助工具
!pip install langchain -q
# Re-ranker 模型库
!pip install FlagEmbedding -q

2. Colab 环境配置

  • GPU: 代码执行程序 -> 更改代码执行程序类型 -> T4 GPU

  • 密钥管理 (Secrets): 左侧边栏 密钥 -> 新建 OPENAI_API_KEY

  • 数据持久化 (Persistence): 挂载 Google Drive,并将文档和ChromaDB数据库路径指向Drive。

from google.colab import drive
import os
from google.colab import userdata

# 挂载 Drive
drive.mount('/content/drive')

# 设置环境变量
os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')

# 定义路径
DRIVE_BASE_PATH = "/content/drive/MyDrive/RAG_Project"
DOCUMENT_PATH = os.path.join(DRIVE_BASE_PATH, "DeepSeek_V3.pdf") # 替换为你的文档
CHROMA_DB_PATH = os.path.join(DRIVE_BASE_PATH, "chroma_db")

第一步: 数据加载与切分 (Loading & Chunking)

  • 目标: 将源文档(PDF)转换为结构化的、大小适中的文本块列表。

  • 核心原理:

    1. 加载: 使用 PyPDFLoader 将PDF按页加载为 Document 对象。

    2. 切分: 使用 RecursiveCharacterTextSplitter。该切分器按预设分隔符列表 (["\n\n", "\n", " ", ""]) 递归切分,优先保持段落、句子的完整性,优于固定长度切分。

  • 实现代码:

    from langchain_community.document_loaders import PyPDFLoader
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    
    # 1. 加载
    loader = PyPDFLoader(DOCUMENT_PATH)
    documents = loader.load()
    
    # 2. 切分
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,
        chunk_overlap=100,
        length_function=len,
        add_start_index=True,
    )
    chunks = text_splitter.split_documents(documents)
  • 面试要点:

    • Q: 为什么需要文本切分?

      • A: 1. 模型上下文限制: LLM的上下文窗口有限,无法处理整个文档。2. 检索精度: 过大的块引入噪声,干扰LLM;过小的块缺乏上下文。切分旨在平衡信息量与噪声。3. 成本与效率: 更小的上下文意味着更低的API成本和更快的生成速度。

    • Q: 如何选择 chunk_sizechunk_overlap

      • A: 这是需要实验调优的超参数。chunk_size 通常从500-1000字符开始,取决于文档类型。chunk_overlap 设为 chunk_size 的10-20%,其作用是防止在块边界处切断重要的语义单元,保证上下文连续性。

    • Q: 除了递归字符切分,还有哪些高级策略?

      • A: 1. 内容感知切分: 按Markdown标题、代码块等结构化标记切分。2. 语义切分: 通过计算句子间嵌入向量的相似度来找到语义的自然断点,更智能但计算成本高。


第二步: 文本嵌入 (Embedding)

  • 目标: 将文本块转换为能够进行数学运算的、代表其语义的向量。

  • 核心原理:

    1. Bi-Encoder (双塔编码器): bge-m3属于此类。它独立地将查询和文档编码为向量,可以预先计算和索引所有文档向量,速度快,适合召回。

    2. 归一化 (Normalization): 将向量的L2范数缩放为1。好处是:a) 可以使用更快的点积运算代替余弦相似度;b) 消除向量长度对相似度计算的干扰,只关注方向(语义)。

  • 实现代码:

    from sentence_transformers import SentenceTransformer
    import numpy as np
    
    # 1. 加载模型到GPU
    embedding_model = SentenceTransformer("BAAI/bge-m3", device='cuda')
    
    # 2. 提取文本并计算嵌入
    texts_to_embed = [chunk.page_content for chunk in chunks]
    embeddings = embedding_model.encode(
        texts_to_embed,
        normalize_embeddings=True,
        show_progress_bar=True
    )
    # embeddings.shape -> (num_chunks, 1024)
  • 面试要点:

    • Q: 为什么选择开源模型 bge-m3 而不是OpenAI的API?

      • A: 1. 性能: 在MTEB等权威榜单上表现优异,不逊于商业模型。2. 成本与控制: 本地/私有化部署,长期成本低,数据安全可控,无API版本迭代风险。3. 灵活性: 支持在特定领域数据上进行微调以提升性能。

    • Q: 嵌入向量的维度意味着什么?越高越好吗?

      • A: 维度是描述文本语义坐标轴的数量。维度并非越高越好,是表达能力与效率的权衡。高维度能编码更丰富信息,但带来更高的存储/计算成本,并可能遭遇“维度灾难”。


第三步: 向量存储 (Vector Storage)

  • 目标: 将文本、元数据和向量存入专用数据库,并建立高效检索引索。

  • 核心原理:

    1. ANN 索引: 向量数据库的核心是近似最近邻(ANN)搜索,通过牺牲极小精度换取巨大速度提升。

    2. HNSW (Hierarchical Navigable Small World): ChromaDB等使用的底层ANN算法。它构建一个多层的图结构,搜索时从稀疏的顶层图(高速公路)逐层进入密集的底层图(本地街道),实现对搜索空间的大幅剪枝,达到O(log N)级别的搜索复杂度。

  • 实现代码:

    import chromadb
    from chromadb.config import Settings
    
    # 1. 初始化持久化客户端,并关闭遥测
    client = chromadb.PersistentClient(
        path=CHROMA_DB_PATH,
        settings=Settings(anonymized_telemetry=False)
    )
    
    # 2. 创建或获取Collection,指定距离度量
    collection = client.get_or_create_collection(
        name="deepseek_v3_paper",
        metadata={"hnsw:space": "cosine"} # 必须与嵌入方式匹配
    )
    
    # 3. 添加数据
    collection.add(
        embeddings=embeddings,
        documents=[c.page_content for c in chunks],
        metadatas=[c.metadata for c in chunks],
        ids=[f"chunk_{i}" for i in range(len(chunks))] # 必须提供唯一ID
    )
  • 面试要点:

    • Q: 为什么需要向量数据库而不是MySQL?

      • A: MySQL等关系型数据库为精确匹配优化,对高维向量的相似度搜索(需要计算所有向量距离)是O(N)复杂度,无法扩展。向量数据库使用ANN索引,可将复杂度降至O(log N)

    • Q: metadata={'hnsw:space': 'cosine'} 的作用是?

      • A: 它指定HNSW索引内部使用的距离度量函数。cosine适合归一化的文本向量,只关心方向。其他还有l2(欧氏距离)和ip(内积)。选择的度量必须与嵌入模型的训练目标和使用方式一致。

    • Q: 元数据 (Metadata) 在RAG中的价值?

      • A: 1. 引用与可追溯性: 提供sourcepage,增强答案可信度。2. 过滤: 可在检索前进行元数据过滤(如按日期、作者),缩小检索范围,提升效率和精度。3. 增强功能: start_index等可用于前端高亮显示。


第四步: 检索 (Retrieval)

  • 目标: 根据用户问题,从数据库中找出Top-K个最相关的文本块。

  • 核心原理: 将用户问题用完全相同的嵌入模型转换为查询向量,然后在向量数据库中执行ANN搜索。

  • 实现代码:

    user_query = "What is Mixture-of-Experts (MoE) and how does DeepSeek-V3 use it?"
    
    # 1. 嵌入查询 (使用相同模型和设置)
    query_embedding = embedding_model.encode(user_query, normalize_embeddings=True)
    
    # 2. 查询数据库
    results = collection.query(
        query_embeddings=[query_embedding.tolist()],
        n_results=10 # K值,召回10个用于后续重排
    )
    
    # results['documents'][0] 中即为召回的文本列表
  • 面试要点:

    • Q: 如何选择n_results (K值)?

      • A: 这是召回率与精确率的权衡。K值小,噪声少但可能遗漏信息(低召回);K值大,信息全但噪声多,且可能遭遇LLM“迷失在中间”的问题。通常从3-5开始,根据最终答案质量进行调优。如果后续有Re-ranker,K值可以适当调大(如10-20)。

    • Q: 检索结果不佳如何优化?

      • A: 从四方面排查:1. 切分策略: 调整chunk_size或尝试语义切分。2. 嵌入模型: 更换或微调更适合领域数据的模型。3. 查询转换: 使用HyDE、Multi-Query等技术优化用户输入。4. 检索后处理: 引入Re-ranker进行二次精排。


第五步: 生成 (Generation)

  • 目标: 将问题和检索到的上下文整合后,由LLM生成最终答案。

  • 核心原理:

    1. Prompt工程: 设计一个结构清晰、指令明确的Prompt是关键。

    2. 核心指令: 必须包含“只根据提供的上下文回答”和“如果信息不足则明确说明”这两个“护栏”,以抑制幻觉。

    3. temperature: RAG追求事实准确性,应使用较低的temperature(如0-0.2)来降低生成内容的随机性。

  • 实现代码:

    from openai import OpenAI
    
    client = OpenAI()
    # retrieved_context 是上一步整合后的文本
    
    prompt_template = f"""
    你是一位顶级的AI技术专家。请严格根据下面【上下文信息】回答【用户问题】。
    规则:
    1. 只使用【上下文信息】作答,禁止使用任何外部知识。
    2. 如果信息不足,直接回答“根据提供的资料,我无法回答这个问题。”
    3. 综合信息,不要简单复述。
    
    ---
    [上下文信息]: {retrieved_context}
    ---
    [用户问题]: {user_query}
    ---
    [你的回答]:
    """
    
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "你是一位顶级的AI技术专家。"},
            {"role": "user", "content": prompt_template}
        ],
        temperature=0.1,
    )
    final_answer = response.choices[0].message.content
  • 面试要点:

    • Q: 为什么需要LLM进行生成,而不是直接返回检索结果?

      • A: 1. 信息综合: LLM能将零散的文本块融合成连贯的答案。2. 人性化: 将书面语转换为直接回答问题的对话式语言。3. 指令遵循: 通过Prompt控制,确保答案的忠实度和安全性。


进阶优化 (Advanced Optimizations)

1. 重排 (Re-ranking)

  • 目标: 解决召回的“相关性”不等于生成所需的“有用性”问题。

  • 核心原理:

    • Cross-Encoder (交叉编码器):[查询, 文档]对一起输入模型,进行深度交互,输出一个精准的相关性分数。速度慢但精度高。

    • 两阶段流程: 先用快的Bi-Encoder召回Top-K,再用精准的Cross-Encoder对这K个结果进行重排,取Top-N送入LLM。

  • 实现代码:

    from FlagEmbedding import FlagReranker
    
    # 1. 初始化模型
    reranker = FlagReranker('BAAI/bge-reranker-large', use_fp16=True)
    
    # 2. 准备句子对并计算分数
    sentence_pairs = [[user_query, doc] for doc in results['documents'][0]]
    rerank_scores = reranker.compute_score(sentence_pairs)
    
    # 3. 排序并提取Top-N
    reranked_results = sorted(zip(rerank_scores, results['documents'][0]), key=lambda x: x[0], reverse=True)
    final_context = "\n\n---\n\n".join([doc for score, doc in reranked_results[:3]])
  • 面试要点:

    • Q: 请解释Bi-Encoder和Cross-Encoder的区别和在RAG中的应用。

      • A: Bi-Encoder独立编码查询和文档,速度快,用于第一阶段的召回。Cross-Encoder联合编码查询和文档,进行深度交互,精度高但速度慢,用于第二阶段对召回结果的精排

2. 评估 (Evaluation): LLM-as-a-Judge

  • 目标: 自动化、可扩展地评估RAG系统性能。

  • 核心原理: 使用一个强大的LLM(如GPT-4o)作为“裁判”,根据一套明确的评估维度,对生成的答案进行打分或比较。

  • 关键评估维度:

    1. Faithfulness (忠实度): 答案是否完全基于上下文,无幻觉。

    2. Answer Relevancy (答案相关性): 答案是否直接回应了用户问题。

    3. Context Relevancy (上下文相关性): 召回的上下文是否与问题相关。

  • 实现代码:

    import json
    
    # 准备好 user_query, context_for_judge, answer_A, answer_B
    
    judge_prompt_template = f"""
    你是一个公正的AI评测员。请根据【背景知识】和【用户问题】,评估【答案 A】和【答案 B】的质量,并以JSON格式输出裁决。
    评估维度: 1. 忠实度 2. 相关性 3. 深度。
    
    [用户问题]: {user_query}
    [背景知识]: {context_for_judge}
    [答案 A]: {answer_A}
    [答案 B]: {answer_B}
    
    请按以下JSON格式输出:
    {{
      "reasoning": "你的详细思考过程...",
      "winner": "裁决结果 ('答案 A', '答案 B', 或 '平局')"
    }}
    """
    
    judge_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": judge_prompt_template}],
        temperature=0,
        response_format={"type": "json_object"}
    )
    judge_result = json.loads(judge_response.choices[0].message.content)
  • 面试要点:

    • Q: 如何缓解LLM-as-a-Judge中的偏见?

      • A: 1. 设计无偏Prompt: 指令清晰、中立。2. 使用强大、中立的裁判模型。 3. 缓解位置偏见: 在大规模测试中,随机交换答案A答案B的位置进行多次评估。4. 要求详细的推理过程: 让模型为自己的裁决提供理由,增加透明度。

    • Q: 知道哪些成熟的RAG评估框架吗?

      • A: RAGAs, TruLens, ARES。它们将LLM-as-a-Judge等理念产品化,提供了更系统的评估工具。

3. 其他前沿技术

  • 查询转换 (Query Transformation): 通过HyDE、Multi-Query等技术,在检索前用LLM优化用户查询。

  • 自适应RAG (Adaptive RAG): 用LLM作为Agent来动态决策是否需要检索、是否需要再次检索,将线性管道变为智能循环。

  • 混合检索与RAG-Fusion (Hybrid Search & RAG-Fusion): 结合关键词检索(如BM25)和向量检索,并使用倒数排序融合(RRF)算法合并多路结果,提升召回的鲁棒性。

Last updated