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)转换为结构化的、大小适中的文本块列表。
核心原理:
加载: 使用
PyPDFLoader
将PDF按页加载为Document
对象。切分: 使用
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_size
和chunk_overlap
?A: 这是需要实验调优的超参数。
chunk_size
通常从500-1000字符开始,取决于文档类型。chunk_overlap
设为chunk_size
的10-20%,其作用是防止在块边界处切断重要的语义单元,保证上下文连续性。
Q: 除了递归字符切分,还有哪些高级策略?
A: 1. 内容感知切分: 按Markdown标题、代码块等结构化标记切分。2. 语义切分: 通过计算句子间嵌入向量的相似度来找到语义的自然断点,更智能但计算成本高。
第二步: 文本嵌入 (Embedding)
目标: 将文本块转换为能够进行数学运算的、代表其语义的向量。
核心原理:
Bi-Encoder (双塔编码器):
bge-m3
属于此类。它独立地将查询和文档编码为向量,可以预先计算和索引所有文档向量,速度快,适合召回。归一化 (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)
目标: 将文本、元数据和向量存入专用数据库,并建立高效检索引索。
核心原理:
ANN 索引: 向量数据库的核心是近似最近邻(ANN)搜索,通过牺牲极小精度换取巨大速度提升。
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. 引用与可追溯性: 提供
source
和page
,增强答案可信度。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生成最终答案。
核心原理:
Prompt工程: 设计一个结构清晰、指令明确的Prompt是关键。
核心指令: 必须包含“只根据提供的上下文回答”和“如果信息不足则明确说明”这两个“护栏”,以抑制幻觉。
低
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)作为“裁判”,根据一套明确的评估维度,对生成的答案进行打分或比较。
关键评估维度:
Faithfulness (忠实度): 答案是否完全基于上下文,无幻觉。
Answer Relevancy (答案相关性): 答案是否直接回应了用户问题。
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