第14章 RAG系统设计¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
NLP视角的检索增强生成系统
完整的RAG工程实践请参考→LLM应用/05-RAG系统构建和LLM应用/18-高级RAG技术。本章侧重NLP视角的检索、Embedding和评估技术。
14.1 RAG系统架构¶
14.1.1 RAG的NLP本质¶
RAG(Retrieval-Augmented Generation)本质上是检索增强的生成式问答——将经典NLP中的信息检索(IR)与神经语言生成(NLG)在统一框架下融合。
从NLP任务分类的角度看,RAG同时涉及:
| NLP子任务 | 在RAG中的角色 | 对应模块 |
|---|---|---|
| 信息检索(IR) | 从知识库召回相关段落 | Retriever |
| 文本匹配/语义相似度 | 计算query与文档的相关性 | Embedding + Reranker |
| 阅读理解(MRC) | 基于上下文抽取/生成答案 | Generator(LLM) |
| 文本摘要 | 压缩多段落为连贯回答 | Generator(LLM) |
| 查询理解 | 解析用户意图、改写查询 | Query Processor |
RAG的核心思想源自2020年Facebook(Meta)的论文 "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks",将参数化记忆(LLM权重)与非参数化记忆(外部知识库)相结合,解决了纯生成模型的幻觉、知识过时和不可验证等问题。
14.1.2 完整Pipeline¶
一个生产级RAG系统的数据流如下:
用户Query
↓
[Query处理] → 意图识别 / 查询改写 / 查询扩展
↓
[检索阶段] → Dense检索 + Sparse检索 → 多路召回
↓
[重排序] → Cross-Encoder / ColBERT Reranker
↓
[上下文构建] → 截断、去重、排序、Prompt组装
↓
[生成阶段] → LLM生成答案(含引用标注)
↓
[后处理/评估] → 忠实度校验 / 答案质量评估
14.1.3 RAG演进:Naive → Advanced → Modular¶
Naive RAG(2023初): - 简单的"索引→检索→生成"三步流水线 - 固定分块 + 单路向量检索 + 直接拼接Prompt - 问题:检索质量差、上下文噪声大、幻觉严重
Advanced RAG(2023-2024): - 引入预检索优化(Query改写、HyDE)和后检索优化(Reranking、压缩) - Hybrid检索(Dense+Sparse融合) - 更精细的分块策略和元数据过滤
Modular RAG(2024-2025): - 将RAG拆解为可组合的功能模块 - 支持路由(Router)、自适应检索(Self-RAG)、迭代检索 - Agent驱动的动态编排,根据query复杂度选择不同Pipeline
┌─────────────────────────────────────────────┐
│ Modular RAG 架构 │
├─────────┬──────────┬──────────┬─────────────┤
│ Query │ Retrieval│ Reading │ Generation │
│ Module │ Module │ Module │ Module │
├─────────┼──────────┼──────────┼─────────────┤
│ 改写 │ Dense │ Rerank │ LLM生成 │
│ 扩展 │ Sparse │ 压缩 │ 引用标注 │
│ 路由 │ 图检索 │ 过滤 │ 忠实度校验 │
│ 分解 │ 混合 │ 摘要 │ 后处理 │
└─────────┴──────────┴──────────┴─────────────┘
↑ ↑
└──── Agent/Router ────┘
(动态编排决策)
14.1.4 与传统信息检索(IR)的关系¶
RAG的检索阶段继承了经典IR的核心理念,但做了深度演进:
| 维度 | 传统IR | RAG检索 |
|---|---|---|
| 匹配方式 | 关键词精确匹配(BM25) | 语义向量 + 关键词混合 |
| 排序模型 | Learning to Rank(LTR) | Cross-Encoder Reranker |
| 索引单元 | 完整文档 | 文本块(Chunk) |
| 返回形式 | 文档排序列表 | 上下文片段 → 生成式回答 |
| 评估指标 | MAP、NDCG | 新增Faithfulness、Answer Relevance |
| 查询理解 | 查询扩展(同义词) | LLM驱动的Query改写 |
面试要点:回答RAG相关问题时,展示对IR经典理论(倒排索引、BM25评分、TF-IDF)的理解会加分,这是RAG的学术根基。
14.2 文档处理与分块策略¶
14.2.1 文档解析¶
不同格式的文档需要专门的解析器:
"""文档解析:多格式统一处理"""
from dataclasses import dataclass
from typing import List
@dataclass # @dataclass自动生成__init__等方法
class Document:
content: str
metadata: dict # 来源、页码、标题等
class DocumentParser:
"""多格式文档解析器"""
@staticmethod # @staticmethod不需要实例即可调用
def parse_pdf(path: str) -> List[Document]:
"""PDF解析 - 使用PyMuPDF提取文本和表格"""
import fitz # PyMuPDF
docs = []
pdf = fitz.open(path)
for page_num, page in enumerate(pdf): # enumerate同时获取索引和元素
text = page.get_text("text")
if text.strip():
docs.append(Document(
content=text.strip(),
metadata={"source": path, "page": page_num + 1, "type": "pdf"}
))
pdf.close()
return docs
@staticmethod
def parse_markdown(path: str) -> List[Document]:
"""Markdown解析 - 按标题层级拆分"""
import re
with open(path, 'r', encoding='utf-8') as f: # with自动管理文件关闭
content = f.read()
# 按一级/二级标题拆分
sections = re.split(r'\n(?=#{1,2}\s)', content)
docs = []
for section in sections:
if section.strip():
title_match = re.match(r'^(#{1,2})\s+(.+)', section)
title = title_match.group(2) if title_match else "untitled"
docs.append(Document(
content=section.strip(),
metadata={"source": path, "title": title, "type": "markdown"}
))
return docs
@staticmethod
def parse_html(path: str) -> List[Document]:
"""HTML解析 - 使用BeautifulSoup提取正文"""
from bs4 import BeautifulSoup
with open(path, 'r', encoding='utf-8') as f:
soup = BeautifulSoup(f.read(), 'html.parser')
# 移除script和style标签
for tag in soup(['script', 'style', 'nav', 'footer']):
tag.decompose()
text = soup.get_text(separator='\n', strip=True)
return [Document(content=text, metadata={"source": path, "type": "html"})]
14.2.2 Chunking策略对比¶
分块策略直接影响检索质量,是RAG的关键设计决策。
1. 固定大小分块(Fixed-size Chunking)¶
def fixed_size_chunking(text: str, chunk_size: int = 512,
chunk_overlap: int = 128) -> List[str]:
"""固定大小分块:按字符数切分,含重叠"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk.strip())
start += chunk_size - chunk_overlap
return [c for c in chunks if c]
2. 递归分块(Recursive Character Splitting)¶
def recursive_chunking(text: str, chunk_size: int = 512,
chunk_overlap: int = 128,
separators: List[str] = None) -> List[str]:
"""递归分块:按层级分隔符逐步切分"""
if separators is None:
separators = ["\n\n", "\n", "。", ";", ",", " ", ""]
chunks = []
sep = separators[0]
remaining_seps = separators[1:]
# 按当前分隔符拆分
splits = text.split(sep) if sep else list(text)
current_chunk = ""
for split in splits:
candidate = current_chunk + sep + split if current_chunk else split
if len(candidate) <= chunk_size:
current_chunk = candidate
else:
if current_chunk:
chunks.append(current_chunk.strip())
# 如果单个split仍然超长,递归使用下级分隔符
if len(split) > chunk_size and remaining_seps:
sub_chunks = recursive_chunking(
split, chunk_size, chunk_overlap, remaining_seps
)
chunks.extend(sub_chunks)
current_chunk = ""
else:
current_chunk = split
if current_chunk:
chunks.append(current_chunk.strip())
return [c for c in chunks if c]
3. 语义分块(Semantic Chunking)¶
import numpy as np
def semantic_chunking(sentences: List[str], embeddings: np.ndarray,
threshold: float = 0.5,
max_chunk_size: int = 512) -> List[str]:
"""语义分块:基于句间相似度断点切分
Args:
sentences: 句子列表
embeddings: 每个句子的嵌入向量 (n_sentences, dim)
threshold: 相似度断点阈值(低于此值则切分)
max_chunk_size: 最大chunk字符数
"""
# 计算相邻句子的余弦相似度
similarities = []
for i in range(len(embeddings) - 1):
sim = np.dot(embeddings[i], embeddings[i + 1]) / ( # np.dot矩阵/向量点乘
np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i + 1]) # np.linalg线性代数运算
)
similarities.append(sim)
# 在相似度低谷处切分
chunks = []
current_chunk_sents = [sentences[0]]
for i, sim in enumerate(similarities):
current_text = "".join(current_chunk_sents)
next_sent = sentences[i + 1]
if sim < threshold or len(current_text) + len(next_sent) > max_chunk_size:
chunks.append("".join(current_chunk_sents))
current_chunk_sents = [next_sent]
else:
current_chunk_sents.append(next_sent)
if current_chunk_sents:
chunks.append("".join(current_chunk_sents))
return chunks
4. 层级分块(Hierarchical Chunking)¶
层级分块维护文档的树状结构,检索时先匹配细粒度子块,返回时附带父块上下文:
@dataclass
class HierarchicalChunk:
content: str
level: int # 0=文档, 1=章节, 2=段落, 3=句子
children: list
parent: 'HierarchicalChunk' = None
def get_context_window(self, levels_up: int = 1) -> str:
"""获取包含上级上下文的文本"""
node = self
for _ in range(levels_up):
if node.parent:
node = node.parent
return node.content
5. Late Chunking¶
Late Chunking是2024年提出的新方法,核心思想是先用长上下文Embedding模型编码整篇文档,再在token级别进行分块的向量池化,使每个chunk的嵌入都包含全局上下文信息:
优势:解决了传统分块中chunk失去上下文的问题(如指代消解丢失)。
14.2.3 chunk_size / chunk_overlap 的选择¶
| 参数 | 小值(128-256) | 中值(512) | 大值(1024+) |
|---|---|---|---|
| 检索精度 | 高(定位精准) | 平衡 | 低(噪声多) |
| 上下文完整性 | 低(信息断裂) | 平衡 | 高(保留上下文) |
| 索引数量 | 多(存储成本高) | 适中 | 少 |
| 适用场景 | FAQ、定义查询 | 通用 | 长文档摘要 |
经验法则: - chunk_size:中文场景通常选 256-512字符,英文 512-1024 tokens - chunk_overlap:一般为 chunk_size 的 10%-25% - 建议实验驱动:在目标数据集上测试多组参数,用检索指标选最优
面试考点:chunk太小丢失上下文、chunk太大引入噪声,需要根据具体查询类型和文档结构做权衡。
14.3 Embedding模型¶
14.3.1 文本嵌入模型演进¶
Word2Vec (2013) → 词级别静态嵌入,无法处理一词多义
↓
ELMo (2018) → 上下文化词嵌入,双向LSTM
↓
BERT (2019) → Transformer双向编码,但直接用[CLS]做句子嵌入效果差
↓
Sentence-BERT (2019) → 孪生网络 + 对比学习,首个高质量句子嵌入模型
↓
E5/BGE/GTE (2023) → 大规模预训练 + 指令微调,检索专用嵌入
↓
BGE-M3 (2024) → 多语言 + 多粒度 + 多检索模式(Dense+Sparse+ColBERT)
14.3.2 中文Embedding模型选型¶
| 模型 | 维度 | 最大长度 | 特点 | MTEB-中文排名 |
|---|---|---|---|---|
| BGE-large-zh | 1024 | 512 | 智源开源,中文效果好 | Top |
| GTE-large-zh | 1024 | 8192 | 阿里通义,支持长文本 | Top |
| M3E-large | 1024 | 512 | MokaAI,中文社区常用 | 中上 |
| text2vec-large | 1024 | 512 | 苏剑林,轻量级 | 中 |
| BGE-M3 | 1024 | 8192 | 多语言+多检索模式 | Top(多语言) |
| gte-Qwen2 | 可变 | 32000 | 基于Qwen2的最新模型 | Top |
选型建议: - 纯中文场景:BGE-large-zh 或 GTE-large-zh - 中英混合 / 多语言:BGE-M3 - 超长文档:GTE-Qwen2(支持32K上下文) - 资源受限:BGE-small-zh(384维,速度快3倍)
14.3.3 BGE-M3:多向量检索¶
BGE-M3是目前最先进的多语言Embedding模型之一,其核心创新在于单模型同时输出三种检索表示:
- Dense向量(单向量):传统的[CLS] pooling,用于ANN检索
- Sparse向量(词权重):类似learned sparse retrieval,用于精确匹配
- ColBERT向量(多向量):每个token一个向量,用于细粒度交互
"""BGE-M3 多向量检索示例"""
# 注意:需要 FlagEmbedding >= 1.2.0
# 安装:pip install FlagEmbedding>=1.2.0
# API可能随版本变化,请参考官方文档:https://github.com/FlagOpen/FlagEmbedding
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)
queries = ["什么是检索增强生成?"]
documents = [
"RAG(Retrieval-Augmented Generation)通过检索外部知识库来增强大模型的回答。",
"强化学习是一种通过与环境交互来学习最优策略的方法。",
"BGE-M3支持超过100种语言的文本嵌入。"
]
# 编码时同时返回三种表示
q_output = model.encode(queries, return_dense=True, return_sparse=True,
return_colbert_vecs=True)
d_output = model.encode(documents, return_dense=True, return_sparse=True,
return_colbert_vecs=True)
# Dense检索得分
dense_scores = q_output['dense_vecs'] @ d_output['dense_vecs'].T
print("Dense scores:", dense_scores)
# Sparse检索得分(词级别精确匹配)
sparse_scores = model.compute_lexical_matching_score(
q_output['lexical_weights'][0], d_output['lexical_weights'][0]
)
# ColBERT检索得分(token级别MaxSim)
colbert_scores = model.colbert_score(
q_output['colbert_vecs'][0], d_output['colbert_vecs'][0]
)
面试考点:BGE-M3为什么使用多向量检索?
- Dense擅长语义匹配,但会丢失细粒度词汇信息(如专有名词)
- Sparse擅长精确匹配关键词,但无法捕捉语义同义
- ColBERT多向量通过token级别的MaxSim交互,兼顾语义理解和细粒度匹配
- 三者互补:Dense做粗召回 + Sparse保证关键词命中 + ColBERT做精细排序
- 单模型多任务训练,部署成本低于分别维护多个独立模型
14.3.4 相似度度量选择¶
| 度量 | 公式 | 特点 | 适用场景 |
|---|---|---|---|
| Cosine | \(\frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|}\) | 方向相似性,归一化后不受模长影响 | 通用首选 |
| L2(欧式距离) | \(\|\mathbf{a} - \mathbf{b}\|_2\) | 空间距离,值越小越相似 | 归一化后等价Cosine |
| Dot Product | \(\mathbf{a} \cdot \mathbf{b}\) | 受模长影响,适合有重要性权重的场景 | 推荐系统、广告排序 |
实用建议:绝大多数RAG场景选 Cosine Similarity,且多数Embedding模型(如BGE系列)训练时就是基于Cosine优化的。向量归一化后,Cosine与Dot Product等价。
14.3.5 代码:Embedding生成与性能对比¶
"""Embedding模型性能对比benchmark"""
import time
import numpy as np
from sentence_transformers import SentenceTransformer
# 准备测试数据
test_sentences = [
"什么是Transformer架构?",
"BERT模型如何进行预训练?",
"注意力机制的计算复杂度是多少?",
"大语言模型的涌现能力有哪些?",
"如何解决大模型的幻觉问题?",
] * 100 # 500条句子
# 对比不同模型
models_to_test = {
"BGE-small-zh": "BAAI/bge-small-zh-v1.5",
"BGE-large-zh": "BAAI/bge-large-zh-v1.5",
"GTE-large-zh": "thenlper/gte-large-zh",
}
results = {}
for name, model_id in models_to_test.items():
model = SentenceTransformer(model_id)
# 计时编码
start = time.time()
embeddings = model.encode(test_sentences, batch_size=64,
show_progress_bar=False,
normalize_embeddings=True)
elapsed = time.time() - start
results[name] = {
"维度": embeddings.shape[1],
"编码速度(句/秒)": len(test_sentences) / elapsed,
"耗时(秒)": round(elapsed, 2),
}
# 测试语义相似度(query与第一个句子最相关)
query_emb = model.encode(["Transformer的结构是什么样的?"],
normalize_embeddings=True)
similarities = query_emb @ embeddings[:5].T # 切片操作,取前n个元素
results[name]["Top1相似度"] = round(float(similarities[0][0]), 4)
# 展示结果
for name, metrics in results.items():
print(f"\n{name}:")
for k, v in metrics.items():
print(f" {k}: {v}")
14.4 检索策略¶
14.4.1 Dense检索(向量检索)¶
Dense检索将query和文档映射到同一向量空间,通过最近邻搜索(ANN)找到最相关的文档块。
ANN索引选择:
| 索引库 | 算法 | 特点 | 适用规模 |
|---|---|---|---|
| FAISS | IVF/HNSW/PQ | Meta开源,GPU加速,工业标准 | 百万-十亿级 |
| Milvus | HNSW/IVF_PQ | 分布式向量数据库,云原生 | 十亿级+ |
| Qdrant | HNSW | Rust实现,支持过滤 | 百万-亿级 |
| Chroma | HNSW | 轻量级,适合原型 | 十万级 |
14.4.2 Sparse检索(BM25 / TF-IDF)¶
BM25是经典IR算法,在RAG中仍然不可替代——它擅长精确匹配专有名词、代码片段、ID等Dense检索容易遗漏的内容。
BM25评分公式:
其中 \(f(t,d)\) 是词频,\(k_1\)(默认1.2)控制词频饱和度,\(b\)(默认0.75)控制文档长度归一化。
14.4.3 Hybrid检索¶
Hybrid检索结合Dense和Sparse的优势,是当前RAG系统的最佳实践。
融合策略:Reciprocal Rank Fusion (RRF)
其中 \(R\) 是各路召回的排序列表,\(r(d)\) 是文档 \(d\) 在排序列表 \(r\) 中的排名(从1开始),\(k\) 是平滑常数(默认60)。
14.4.4 代码:Hybrid检索完整实现¶
"""Hybrid检索完整实现:BM25 + 向量 + RRF融合"""
import numpy as np
from dataclasses import dataclass, field
from typing import List, Tuple, Dict
from collections import defaultdict
import math
import re
@dataclass
class SearchResult:
doc_id: int
content: str
score: float
source: str # "dense" / "sparse" / "hybrid"
class BM25Retriever:
"""BM25稀疏检索器"""
def __init__(self, k1: float = 1.2, b: float = 0.75):
self.k1 = k1
self.b = b
self.documents: List[str] = []
self.doc_freqs: Dict[str, int] = {} # 词 -> 包含该词的文档数
self.doc_term_freqs: List[Dict[str, int]] = [] # 每个文档的词频
self.avgdl: float = 0.0
self.n_docs: int = 0
def _tokenize(self, text: str) -> List[str]:
"""中文简单分词(生产中建议用jieba)"""
# 按非中文字符分割 + 单字切分
tokens = re.findall(r'[\u4e00-\u9fff]|[a-zA-Z0-9]+', text.lower()) # re.findall返回所有匹配项列表
return tokens
def index(self, documents: List[str]):
"""建立BM25索引"""
self.documents = documents
self.n_docs = len(documents)
doc_lengths = []
for doc in documents:
tokens = self._tokenize(doc)
doc_lengths.append(len(tokens))
term_freq = defaultdict(int) # defaultdict访问不存在的键时返回默认值
for token in tokens:
term_freq[token] += 1
self.doc_term_freqs.append(dict(term_freq))
for token in set(tokens):
self.doc_freqs[token] = self.doc_freqs.get(token, 0) + 1
self.avgdl = sum(doc_lengths) / len(doc_lengths) if doc_lengths else 0
def search(self, query: str, top_k: int = 10) -> List[SearchResult]:
"""BM25检索"""
query_tokens = self._tokenize(query)
scores = []
for doc_id in range(self.n_docs):
score = 0.0
doc_len = sum(self.doc_term_freqs[doc_id].values())
for token in query_tokens:
if token not in self.doc_freqs:
continue
df = self.doc_freqs[token]
idf = math.log((self.n_docs - df + 0.5) / (df + 0.5) + 1)
tf = self.doc_term_freqs[doc_id].get(token, 0)
tf_norm = (tf * (self.k1 + 1)) / (
tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
)
score += idf * tf_norm
scores.append(score)
# 排序取Top-K
top_indices = np.argsort(scores)[::-1][:top_k]
return [
SearchResult(
doc_id=int(idx), content=self.documents[idx],
score=scores[idx], source="sparse"
)
for idx in top_indices if scores[idx] > 0
]
class DenseRetriever:
"""Dense向量检索器"""
def __init__(self, model_name: str = "BAAI/bge-small-zh-v1.5"):
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer(model_name)
self.doc_embeddings: np.ndarray = None
self.documents: List[str] = []
def index(self, documents: List[str]):
"""建立向量索引"""
self.documents = documents
self.doc_embeddings = self.model.encode(
documents, normalize_embeddings=True, show_progress_bar=False
)
def search(self, query: str, top_k: int = 10) -> List[SearchResult]:
"""向量检索"""
query_emb = self.model.encode(
[query], normalize_embeddings=True, show_progress_bar=False
)
scores = (query_emb @ self.doc_embeddings.T)[0]
top_indices = np.argsort(scores)[::-1][:top_k]
return [
SearchResult(
doc_id=int(idx), content=self.documents[idx],
score=float(scores[idx]), source="dense"
)
for idx in top_indices
]
class HybridRetriever:
"""混合检索器:Dense + Sparse + RRF融合"""
def __init__(self, dense_retriever: DenseRetriever,
sparse_retriever: BM25Retriever, rrf_k: int = 60):
self.dense = dense_retriever
self.sparse = sparse_retriever
self.rrf_k = rrf_k
def search(self, query: str, top_k: int = 10,
dense_weight: float = 0.5,
sparse_weight: float = 0.5) -> List[SearchResult]:
"""混合检索 + RRF融合"""
dense_results = self.dense.search(query, top_k=top_k * 2)
sparse_results = self.sparse.search(query, top_k=top_k * 2)
# RRF融合
rrf_scores = defaultdict(float)
doc_contents = {}
for rank, result in enumerate(dense_results):
rrf_scores[result.doc_id] += dense_weight / (self.rrf_k + rank + 1)
doc_contents[result.doc_id] = result.content
for rank, result in enumerate(sparse_results):
rrf_scores[result.doc_id] += sparse_weight / (self.rrf_k + rank + 1)
doc_contents[result.doc_id] = result.content
# 按RRF分数排序
sorted_docs = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True) # lambda匿名函数
return [
SearchResult(
doc_id=doc_id, content=doc_contents[doc_id],
score=score, source="hybrid"
)
for doc_id, score in sorted_docs[:top_k]
]
# --- 使用示例 ---
if __name__ == "__main__":
documents = [
"RAG通过检索外部知识库增强大语言模型的回答质量,减少幻觉。",
"BM25是经典的稀疏检索算法,基于词频和逆文档频率计算相关性。",
"BERT是Google提出的预训练语言模型,采用Transformer编码器架构。",
"向量数据库如Milvus、Qdrant支持高效的近似最近邻搜索。",
"Reranker使用Cross-Encoder对初步检索结果进行精细排序。",
"知识图谱可以为RAG提供结构化的实体关系信息。",
"Embedding模型将文本映射到稠密向量空间用于语义检索。",
]
# 初始化检索器
sparse = BM25Retriever()
sparse.index(documents)
dense = DenseRetriever(model_name="BAAI/bge-small-zh-v1.5")
dense.index(documents)
hybrid = HybridRetriever(dense, sparse)
# 对比三种检索
query = "什么是BM25检索算法?"
print(f"Query: {query}\n")
for name, results in [
("BM25 (Sparse)", sparse.search(query, top_k=3)),
("Dense", dense.search(query, top_k=3)),
("Hybrid (RRF)", hybrid.search(query, top_k=3)),
]:
print(f"--- {name} ---")
for r in results:
print(f" [{r.score:.4f}] {r.content[:50]}...")
print()
面试考点:Dense vs Sparse各自优势?
维度 Dense检索 Sparse检索(BM25) 语义理解 ✅ 捕捉同义词、上下位 ❌ 仅字面匹配 精确匹配 ❌ 专有名词可能丢失 ✅ 精确关键词命中 零样本泛化 ✅ 预训练模型天然具备 ❌ 依赖term overlap 长尾查询 ✅ 语义表示鲁棒 ❌ 罕见词IDF极端 计算成本 高(需GPU编码) 低(CPU即可) 可解释性 ❌ 黑盒向量 ✅ 词级贡献可分析 最佳实践:Hybrid检索(Dense召回语义结果 + BM25召回精确结果 + RRF融合)在大多数基准上优于任何单一方法。
14.5 重排序(Reranking)¶
14.5.1 为什么需要Reranker¶
检索阶段(无论Dense还是Sparse)使用的是Bi-Encoder架构——query和文档分别编码再计算相似度,效率高但精度有限。Reranker使用Cross-Encoder,将query和文档拼接后联合编码,能捕捉更精细的交互信息。
Bi-Encoder (检索阶段): Cross-Encoder (重排阶段):
Query → Encoder → q_emb [CLS] Query [SEP] Doc [SEP]
Doc → Encoder → d_emb ↓
score = cos(q_emb, d_emb) Transformer全层交互
↓
score = σ(h_CLS)
为什么不直接用Cross-Encoder做检索? 因为Cross-Encoder需要对每个(query, doc)对进行前向计算,复杂度 \(O(N)\)(N=语料库大小),而Bi-Encoder通过ANN索引实现亚线性复杂度。因此采用先粗召回再精排的两阶段策略。
14.5.2 主流Reranker¶
| 模型 | 类型 | 特点 |
|---|---|---|
| bge-reranker-v2-m3 | Cross-Encoder | BAAI开源,多语言,效果好 |
| bge-reranker-v2-gemma | LLM-based | 基于Gemma微调,长文本支持好 |
| Cohere Rerank | API服务 | 商业API,效果顶尖 |
| ColBERT | Late Interaction | token级别MaxSim,速度与精度平衡 |
| RankGPT | LLM Listwise | 用GPT-4做Listwise排序 |
14.5.3 ColBERT重排原理¶
ColBERT(Contextualized Late Interaction over BERT)采用Late Interaction机制:
- Query和Doc分别通过BERT编码为token级别的向量序列
- 通过MaxSim操作计算相关性:对query的每个token,找到doc中最相似的token,求和
优势:比Cross-Encoder快(Doc向量可预计算),比Bi-Encoder精(保留token级交互)。
14.5.4 LLM-based Reranking(RankGPT思路)¶
RankGPT使用LLM以Listwise方式对候选段落排序:
Prompt: 以下是针对查询"{query}"检索到的{n}个段落,
请按照与查询的相关性从高到低排序,只输出编号序列。
[1] 段落A内容...
[2] 段落B内容...
[3] 段落C内容...
排序结果:
优势:利用LLM的深度理解能力,适合复杂查询。 劣势:延迟高、成本高,通常只在Top-10结果上使用。
14.5.5 代码:使用bge-reranker进行重排¶
"""使用BGE-Reranker进行重排序"""
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
class BGEReranker:
"""BGE Cross-Encoder重排器"""
def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForSequenceClassification.from_pretrained(model_name)
self.model.eval() # eval()评估模式
@torch.no_grad() # 禁用梯度计算,节省内存
def rerank(self, query: str, documents: List[str],
top_k: int = 5) -> List[Tuple[int, str, float]]:
"""对候选文档重排序
Returns:
List of (原始索引, 文档内容, 重排分数)
"""
pairs = [[query, doc] for doc in documents]
inputs = self.tokenizer(
pairs, padding=True, truncation=True,
max_length=512, return_tensors="pt"
)
scores = self.model(**inputs).logits.squeeze(-1).tolist() # squeeze压缩维度
# 按分数降序排列
indexed_scores = [(i, documents[i], s) for i, s in enumerate(scores)]
indexed_scores.sort(key=lambda x: x[2], reverse=True)
return indexed_scores[:top_k]
# 使用示例
reranker = BGEReranker()
query = "RAG系统如何减少幻觉?"
candidates = [
"RAG通过引入外部知识源,让模型基于检索到的事实生成回答,显著减少幻觉。",
"大语言模型的幻觉问题是指模型生成看似合理但实际错误的内容。",
"向量数据库支持高效的相似度搜索,是RAG系统的核心组件。",
"通过检索增强,模型回答可以锚定在可验证的文档上,降低捏造风险。",
"Transformer注意力机制通过Q、K、V矩阵计算注意力权重。",
]
results = reranker.rerank(query, candidates, top_k=3)
print(f"Query: {query}\n")
for idx, doc, score in results:
print(f" [{score:.4f}] (原第{idx}条) {doc[:60]}...")
面试考点: - Reranker的本质是精度-效率权衡:Cross-Encoder精度高但慢,只能在小候选集上使用 - 生产系统的典型流程:Hybrid检索 Top-100 → Reranker精排 Top-10 → LLM生成 - ColBERT是precision和latency之间的折中方案
14.6 高级RAG技术¶
14.6.1 Query改写/扩展¶
HyDE(Hypothetical Document Embeddings)¶
思路:让LLM先生成一个"假设性回答",用这个回答(而非原始query)去做向量检索。
def hyde_retrieval(query: str, llm, retriever) -> List[str]:
"""HyDE:用假设性文档增强检索"""
prompt = f"请详细回答以下问题(即使不确定也请给出你的最佳推测):\n{query}"
hypothetical_doc = llm.generate(prompt)
# 用假设文档做检索(语义更接近答案文档的表述)
results = retriever.search(hypothetical_doc, top_k=5)
return results
原理:query通常很短且是疑问句式,与知识库中的陈述句式存在分布偏差(distribution gap)。HyDE通过生成假设文档弥合这个gap。
Multi-Query¶
将一个复杂query拆分为多个子查询,分别检索后合并去重:
def multi_query_retrieval(query: str, llm, retriever) -> List[str]:
"""Multi-Query:多角度查询扩展"""
prompt = f"""请将以下问题改写为3个不同角度的查询,每行一个:
问题:{query}
改写:"""
sub_queries = llm.generate(prompt).strip().split('\n')
all_results = {}
for sub_q in sub_queries:
results = retriever.search(sub_q.strip(), top_k=5)
for r in results:
if r.doc_id not in all_results:
all_results[r.doc_id] = r
return list(all_results.values())
Step-back Prompting¶
将具体问题抽象为更宽泛的查询以获取更全面的上下文:
14.6.2 Self-RAG:自适应检索决策¶
Self-RAG(2023)让LLM自己决定是否需要检索以及生成结果是否忠实:
┌─────────────────────────────────┐
│ LLM生成带有反思tokens的输出: │
│ [Retrieve] → 是否需要检索 │
│ [ISREL] → 检索结果是否相关 │
│ [ISSUP] → 生成是否有证据支持 │
│ [ISUSE] → 回答是否有用 │
└─────────────────────────────────┘
核心机制:模型通过特殊token实现自我评估和自我纠正,避免盲目检索和不忠实生成。
14.6.3 Corrective RAG(CRAG)¶
CRAG在检索后增加一个检索质量评估步骤:
- Correct:检索结果高度相关 → 直接使用
- Incorrect:检索结果不相关 → 降级到Web搜索或知识补充
- Ambiguous:不确定 → 结合检索结果和外部搜索
def corrective_rag(query: str, retriever, llm, web_search_fn):
"""Corrective RAG:检索质量自动纠正"""
results = retriever.search(query, top_k=5)
# 评估检索质量
eval_prompt = f"""判断以下检索结果与查询的相关性。
查询:{query}
检索结果:{results[0].content[:200]}
回答 CORRECT / INCORRECT / AMBIGUOUS:"""
assessment = llm.generate(eval_prompt).strip()
if assessment == "CORRECT":
context = "\n".join([r.content for r in results])
elif assessment == "INCORRECT":
# 降级到Web搜索
web_results = web_search_fn(query)
context = "\n".join(web_results)
else: # AMBIGUOUS
context = "\n".join([r.content for r in results])
web_results = web_search_fn(query)
context += "\n" + "\n".join(web_results)
return llm.generate(f"基于以下信息回答问题:\n{context}\n\n问题:{query}")
14.6.4 GraphRAG¶
GraphRAG将知识图谱和文本检索结合:
- 离线阶段:从文档中抽取实体和关系,构建知识图谱;对社区结构生成摘要
- 在线阶段:同时进行向量检索和图检索,合并结果作为上下文
Global Search vs Local Search: - Local Search:从query相关的实体出发,沿图谱探索局部邻居 - Global Search:在社区摘要上检索,适合需要全局概览的问题
14.6.5 Multi-hop RAG¶
处理需要多步推理的复杂问题:
问题:"研发BERT的公司的CEO是谁?"
Step 1: 检索 → "BERT由Google AI开发"
Step 2: 检索 → "Sundar Pichai是Google/Alphabet的CEO"
最终答案: Sundar Pichai
实现策略:Query分解 → 逐步检索 → 中间结果串联 → 最终生成。
14.6.6 Agentic RAG¶
将RAG嵌入到Agent框架中,由Agent根据任务动态编排检索策略:
"""Agentic RAG概念示例"""
class AgenticRAG:
"""Agent驱动的RAG系统"""
def __init__(self, retrievers: dict, llm):
self.retrievers = retrievers # {"vector": ..., "bm25": ..., "graph": ...}
self.llm = llm
def route(self, query: str) -> str:
"""Agent决策:选择检索策略"""
prompt = f"""分析查询类型并选择最佳检索策略:
查询:{query}
可选策略:
- vector: 语义相似的段落检索
- bm25: 关键词精确匹配
- hybrid: 混合检索
- graph: 知识图谱推理
- multi_hop: 多步推理检索
- none: 无需检索(常识问题)
输出策略名称:"""
return self.llm.generate(prompt).strip()
def execute(self, query: str) -> str:
"""Agent执行RAG Pipeline"""
strategy = self.route(query)
if strategy == "none":
return self.llm.generate(query)
if strategy == "multi_hop":
return self._multi_hop(query)
retriever = self.retrievers.get(strategy,
self.retrievers["hybrid"])
results = retriever.search(query, top_k=5)
context = "\n".join([r.content for r in results])
return self.llm.generate(
f"基于以下信息回答问题。如果信息不足请说明。\n\n"
f"参考信息:\n{context}\n\n问题:{query}"
)
def _multi_hop(self, query: str) -> str:
"""多跳推理检索"""
sub_questions = self._decompose(query)
accumulated_context = ""
for sub_q in sub_questions:
enriched_q = f"{sub_q}\n已知信息:{accumulated_context}" if accumulated_context else sub_q
results = self.retrievers["hybrid"].search(enriched_q, top_k=3)
answer = self.llm.generate(
f"简洁回答:{sub_q}\n参考:{results[0].content}"
)
accumulated_context += f"\n{sub_q} → {answer}"
return self.llm.generate(
f"综合以下信息回答原问题。\n{accumulated_context}\n\n原问题:{query}"
)
def _decompose(self, query: str) -> List[str]:
"""将复杂query分解为子问题"""
prompt = f"将以下复杂问题分解为2-3个简单子问题,每行一个:\n{query}"
return self.llm.generate(prompt).strip().split('\n')
14.7 RAG评估体系¶
14.7.1 检索评估指标¶
| 指标 | 公式 | 含义 |
|---|---|---|
| Recall@K | \(\frac{\|Retrieved_K \cap Relevant\|}{\|Relevant\|}\) | Top-K结果中召回了多少相关文档 |
| Precision@K | \(\frac{\|Retrieved_K \cap Relevant\|}{K}\) | Top-K结果中有多少是相关的 |
| MRR | \(\frac{1}{\|Q\|}\sum_{i=1}^{\|Q\|}\frac{1}{rank_i}\) | 首个相关结果的平均排名倒数 |
| NDCG@K | \(\frac{DCG_K}{IDCG_K}\) | 排序质量(考虑位置折扣和相关度等级) |
其中DCG的定义为:
14.7.2 生成评估维度¶
RAG生成质量的核心评估维度:
- Faithfulness(忠实度):生成内容是否忠实于检索到的上下文?不包含臆造信息?
- Answer Relevance(答案相关性):回答是否切题、完整地回答了用户问题?
- Context Relevance(上下文相关性):送入LLM的上下文是否与query相关?是否有噪声?
- Answer Correctness(答案正确性):与ground truth对比的准确度
14.7.3 RAGAS评估框架¶
RAGAS(Retrieval Augmented Generation Assessment)是当前最流行的RAG评估框架,提供一套基于LLM的自动化评估指标。
核心指标体系:
Context Answer
Precision Relevance
↑ ↑
Query ──→ [检索上下文] ──→ [生成答案] ──→ 最终评估
↓ ↓
Context Faithfulness
Recall (忠实度)
14.7.4 代码:使用RAGAS评估RAG系统¶
"""使用RAGAS v0.2+评估RAG系统"""
from ragas import evaluate, EvaluationDataset, SingleTurnSample
from ragas.metrics import (
Faithfulness,
ResponseRelevancy,
LLMContextPrecisionWithoutReference,
LLMContextRecall,
)
# 准备评估数据集(RAGAS v0.2使用SingleTurnSample)
samples = [
SingleTurnSample(
user_input="什么是RAG?",
response="RAG是检索增强生成,通过检索外部知识库来增强大语言模型的回答,减少幻觉问题。",
retrieved_contexts=[
"RAG(Retrieval-Augmented Generation)通过检索外部知识增强LLM回答,有效减少幻觉。"
],
reference="RAG是一种将信息检索与文本生成结合的技术,通过检索外部知识库中的相关文档来增强大语言模型的回答质量。",
),
SingleTurnSample(
user_input="BM25算法的核心思想是什么?",
response="BM25基于词频(TF)和逆文档频率(IDF)计算查询与文档的相关性得分,并考虑文档长度归一化。",
retrieved_contexts=[
"BM25是基于概率的检索模型,使用TF-IDF变体计算相关性,考虑了词频饱和度和文档长度归一化。"
],
reference="BM25基于词频和逆文档频率计算文档与查询的相关性,核心是TF饱和度和文档长度归一化。",
),
SingleTurnSample(
user_input="为什么需要Reranker?",
response="Reranker使用Cross-Encoder对检索结果精排,因为初始检索使用的Bi-Encoder精度有限。",
retrieved_contexts=[
"Cross-Encoder将query和文档拼接后联合编码,比Bi-Encoder捕捉更精细的交互信息。"
],
reference="Reranker使用Cross-Encoder对初检结果进行精细排序,解决Bi-Encoder在语义交互上的精度不足。",
),
]
eval_dataset = EvaluationDataset(samples=samples)
# 运行评估(需要配置LLM作为评估器,如OpenAI API)
# import os
# os.environ["OPENAI_API_KEY"] = "your-api-key"
# RAGAS v0.2使用类实例而非模块级对象
results = evaluate(
dataset=eval_dataset,
metrics=[
Faithfulness(), # 生成内容是否忠实于上下文
ResponseRelevancy(), # 答案是否相关
LLMContextPrecisionWithoutReference(), # 上下文精度
LLMContextRecall(), # 上下文召回
],
)
print(results)
# 输出示例:
# {'faithfulness': 0.92, 'response_relevancy': 0.88,
# 'llm_context_precision_without_reference': 0.85, 'llm_context_recall': 0.90}
# 查看每条数据的详细得分
df = results.to_pandas()
print(df[['user_input', 'faithfulness', 'response_relevancy']].to_string())
注意:上述代码使用 RAGAS v0.2+ API。如果你使用 v0.1.x,指标名为小写模块变量(如
from ragas.metrics import faithfulness),数据集使用Dataset.from_dict()格式,字段名分别为question、answer、contexts、ground_truth。建议升级到 v0.2+。
不使用LLM的轻量评估方案:
"""轻量级RAG评估(不依赖LLM API)"""
import numpy as np
from typing import List, Set
from rouge_score import rouge_scorer
def retrieval_recall_at_k(retrieved_ids: List[int],
relevant_ids: Set[int], k: int) -> float:
"""Recall@K: Top-K中召回的相关文档比例"""
retrieved_at_k = set(retrieved_ids[:k])
return len(retrieved_at_k & relevant_ids) / len(relevant_ids)
def mrr(ranked_results: List[List[int]],
ground_truths: List[Set[int]]) -> float:
"""MRR: 首个相关结果的平均排名倒数"""
rr_sum = 0.0
for ranked, truth in zip(ranked_results, ground_truths): # zip按位置配对
for rank, doc_id in enumerate(ranked, 1):
if doc_id in truth:
rr_sum += 1.0 / rank
break
return rr_sum / len(ranked_results)
def answer_similarity(prediction: str, reference: str) -> dict:
"""基于ROUGE的答案相似度评估"""
scorer = rouge_scorer.RougeScorer(
['rouge1', 'rouge2', 'rougeL'], use_stemmer=False
)
scores = scorer.score(reference, prediction)
return {
'rouge1': scores['rouge1'].fmeasure,
'rouge2': scores['rouge2'].fmeasure,
'rougeL': scores['rougeL'].fmeasure,
}
# 使用示例
retrieved = [3, 1, 5, 2, 4]
relevant = {1, 3}
print(f"Recall@3: {retrieval_recall_at_k(retrieved, relevant, k=3)}") # 1.0
print(f"Recall@1: {retrieval_recall_at_k(retrieved, relevant, k=1)}") # 0.5
similarity = answer_similarity(
prediction="RAG通过检索外部知识增强LLM回答",
reference="RAG是检索增强生成技术,利用外部知识库提升LLM回答质量"
)
print(f"ROUGE-L: {similarity['rougeL']:.4f}")
14.8 面试高频题¶
题1:请描述一个完整RAG系统的架构,以及各模块的作用¶
答:一个生产级RAG系统包含以下核心模块:
- 文档处理层:文档解析(PDF/HTML/Markdown)→ 文本清洗 → 分块(Chunking)→ 元数据提取
- 索引层:Embedding编码 → 向量数据库(FAISS/Milvus)建索引 + 倒排索引(BM25)
- 查询理解层:意图识别 → 查询改写/扩展(HyDE/Multi-Query)→ 路由决策
- 检索层:Dense检索(ANN)+ Sparse检索(BM25)→ 多路召回 → RRF融合
- 重排层:Cross-Encoder Reranker 精排 Top-K
- 生成层:Prompt构建(上下文 + 指令)→ LLM生成 → 引用标注
- 评估层:Faithfulness + Relevance + Context Quality 评估
关键设计原则:先粗召回再精排(漏斗模型),各模块可独立优化和替换。
题2:Chunking策略如何选择?chunk_size对检索有什么影响?¶
答:
策略选择: - 固定大小分块:实现简单,适合结构化程度低的文本;缺点是可能在语义中间断裂 - 递归分块:按段落→句子→字符逐级拆分,保持语义完整性,是通用首选 - 语义分块:用Embedding计算句间相似度,在语义断点处切分;效果最好但成本高 - 按文档结构分块:Markdown按标题层级、代码按函数/类,保持逻辑完整
chunk_size影响: - 太小(<128字符):语义不完整,检索命中但上下文不够生成好答案 - 太大(>1024字符):引入大量无关信息(噪声),降低检索精度,浪费LLM上下文窗口 - 最佳实践:中文256-512字符,通过在目标数据集上实验确定
题3:Dense检索 vs Sparse检索,各自优劣势和适用场景?¶
答:
- Dense 擅长语义匹配("如何减少模型幻觉" ↔ "mitigating hallucination"),但可能丢失专有名词精确匹配(如型号、代码)
- Sparse (BM25) 擅长精确关键词匹配("GPT-4o-mini"必须字面出现),但无法理解语义近义
最佳实践是Hybrid检索:用Dense召回语义相关结果 + BM25召回精确匹配结果 + RRF融合排序。在BEIR等检索基准上,Hybrid稳定优于单一方法。
题4:解释RRF融合公式,为什么它比简单分数加权更好?¶
答:
RRF基于排名而非分数进行融合,好处是: - 分数不可比:Dense返回cosine similarity(0-1),BM25返回TF-IDF分数(0-∞),直接加权无意义 - 鲁棒性:RRF只关心相对顺序,不受分数分布差异影响 - 简单有效:只有一个超参数 \(k\)(通常取60),在实践中效果接近甚至超过学习型融合方法
题5:为什么Reranker能提升检索效果?Cross-Encoder vs Bi-Encoder?¶
答:
| 维度 | Bi-Encoder(检索阶段) | Cross-Encoder(重排阶段) |
|---|---|---|
| 编码方式 | Query/Doc独立编码 | Query+Doc拼接联合编码 |
| 交互深度 | 仅在嵌入空间计算余弦 | Transformer全部层都有注意力交互 |
| 精度 | 中等 | 高 |
| 速度 | 快(ANN亚线性) | 慢(线性,需逐对计算) |
Reranker能提升效果是因为Cross-Encoder在token级别进行Query-Document交叉注意力,能捕捉词级别的匹配模式(如否定、条件限定),而Bi-Encoder将整个句子压缩到一个向量中会丢失这些信息。
题6:HyDE的原理和局限性?¶
答:
原理:让LLM先对Query生成一个"假设性回答(Hypothetical Document)",用这个假设文档的Embedding替代原始Query去检索。因为假设文档的语体(陈述句、详细描述)更接近知识库中的文档表述,弥合了查询和文档之间的分布偏差。
局限性: - LLM生成的假设文档可能包含错误信息,导致检索被误导 - 多了一次LLM调用,增加延迟和成本 - 对事实型查询(有明确答案)效果好,对开放性/观点类查询提升有限 - 依赖LLM已有知识,对LLM完全不了解的领域可能生成离谱的假设文档
题7:Self-RAG和CRAG的核心区别?¶
答:
- Self-RAG:训练模型内置反思能力(通过特殊token),让模型自己决定是否需要检索、检索结果是否可用、生成是否忠实。是一种端到端的自适应方案
- CRAG:在标准RAG流程中增加一个检索质量评估步骤,如果检索结果不好则降级到Web搜索或其他知识源。是一种Pipeline级别的纠正机制
Self-RAG更优雅(模型内置决策能力)但需要专门训练;CRAG更实用(即插即用,不需要微调模型)。
题8:如何评估RAG系统的质量?关键指标有哪些?¶
答:
RAG评估分三个层次:
- 检索评估:Recall@K(是否召回了相关文档)、MRR(首个相关结果排名)、NDCG(排序质量)
- 生成评估:
- Faithfulness:生成内容是否忠实于检索上下文(不添加臆造信息)
- Answer Relevance:回答是否切题
- Answer Correctness:与标准答案的匹配度
- 端到端评估:RAGAS框架综合以上维度
实操建议: - 先用RAGAS做快速粗评 - 关键场景用人工标注做精评 - 建立回归测试集,每次系统更新后跑评估
题9:GraphRAG相比传统RAG有什么优势?适合什么场景?¶
答:
优势: - 能回答需要全局概览的问题(如"这个代码库的整体架构是什么"),传统RAG只能检索局部片段 - 利用实体关系进行多跳推理(A→B→C) - 通过社区摘要提供结构化的知识组织
适用场景: - 需要跨文档关联推理的问答 - 需要全局视角的总结性问题 - 实体关系丰富的领域(医疗、法律、金融)
局限:构建和维护知识图谱的成本高,实体抽取和关系抽取的准确性直接影响效果。
题10:设计一个生产级RAG系统,应该关注哪些工程问题?¶
答:
- 数据管道:增量索引更新(不能每次全量重建)、文档去重和版本管理
- 检索性能:ANN索引选择(HNSW vs IVF-PQ,百万级 vs 亿级)、量化压缩、缓存热门query
- 质量保障:
- 上线前:在标注集上跑Recall@K和RAGAS评估
- 上线后:收集用户反馈(点踩/引用验证)做持续改进
- 成本控制:Embedding批量预计算、LLM缓存(语义相似query复用答案)、模型蒸馏
- 安全合规:PII过滤、权限隔离(不同用户只能检索授权范围的文档)、审计日志
- 可观测性:端到端链路追踪(query → retrieval → generation的每步耗时和中间结果)、异常检测
- 容灾:向量数据库高可用、降级策略(检索服务不可用时fallback到纯LLM)
更多工程实践细节请参考→LLM应用/05-RAG系统构建
总结¶
| 模块 | 核心技术 | 2025最佳实践 |
|---|---|---|
| 分块 | 递归/语义分块 | 语义分块 + Late Chunking |
| Embedding | BGE/GTE系列 | BGE-M3多向量 或 GTE-Qwen2 |
| 检索 | Dense + Sparse | Hybrid检索 + RRF融合 |
| 重排 | Cross-Encoder | bge-reranker-v2 |
| 高级技术 | Query改写/Self-RAG | Agentic RAG(Agent动态编排) |
| 评估 | RAGAS | Faithfulness + Recall@K |
延伸学习: - 检索与Embedding基础 →第3章 文本表示方法 - BERT与预训练模型 →第10章 预训练语言模型 - 对话系统与Agent →第13章 对话系统与Agent化NLP - RAG工程实践 →LLM应用/05-RAG系统构建 - 高级RAG技术 →LLM应用/18-高级RAG技术