第 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 演进:基础流水线 → 增强检索 → 模块化编排¶
基础流水线 RAG: - 简单的"索引→检索→生成"三步流水线 - 固定分块 + 单路向量检索 + 直接拼接 Prompt - 问题:检索质量差、上下文噪声大、幻觉严重
增强检索 RAG: - 引入预检索优化( Query 改写、 HyDE )和后检索优化( Reranking 、压缩) - Hybrid 检索( Dense+Sparse 融合) - 更精细的分块策略和元数据过滤
模块化编排 RAG: - 将 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 )一起复盘清楚,因为它们仍是检索层设计的学术根基。
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 系列 | 可变 | 以官方说明为准 | 长文本支持较好 | 需复核当期榜单 |
选型建议: - 纯中文场景:BGE-large-zh 或 GTE-large-zh - 中英混合 / 多语言:BGE-M3 - 超长文档:优先关注长上下文 GTE 系列,并以官方上下文长度说明为准 - 资源受限: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 多向量检索示例"""
# 注意:需要安装支持 `BGEM3FlagModel` 的较新 FlagEmbedding 版本
# 安装命令请以官方文档为准
# 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}\) | 受模长影响,适合有重要性权重的场景 | 推荐系统、广告排序 |
实用建议:把 Cosine Similarity 作为常见稳妥起点通常没有问题,尤其在向量已归一化、模型训练目标也强调角度相似性时更常见。但最终仍要看你所用向量模型、索引后端以及是否显式保留向量模长信息。对归一化向量而言, 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 )。
如果希望不同召回器权重不同,可扩展为 Weighted-RRF:
下面代码示例采用的正是这种加权版本;当所有 \(\omega_r=1\) 时,就退化为标准 RRF 。
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 索引实现亚线性复杂度。因此采用先粗召回再精排的两阶段策略。
一个常见误区: Cross-Encoder 输出的分数更像同一 query 下的相对相关性 logits,适合做排序;它们通常不适合跨 query 直接比较绝对值。
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 ,速度与精度平衡 |
| Listwise Reranker | LLM Listwise | 用强生成模型做 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 typing import List, Tuple
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 系统¶
使用前先分清几个口径:
- Faithfulness:更接近“回答中的陈述是否能被检索上下文支持”,不是简单的“回答像不像参考答案”;
- Context Precision:更关注送入模型的上下文里有多少是真正有用的,噪声多会拉低它;
- Context Recall:更关注检索上下文是否覆盖了回答所需证据,通常需要参考答案或参考证据;
- Answer Correctness / Relevance:更像端到端回答质量指标,但当任务本身存在多种合理表述时,它也不是唯一裁判。
"""使用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 的答案相似度评估;更适合“有参考答案”的摘要/FAQ 类场景"""
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) 擅长精确关键词匹配(如某个模型名必须字面出现),但无法理解语义近义
很多团队会先从 Hybrid 检索起步:用 Dense 召回语义相关结果 + BM25 召回精确匹配结果 + RRF 融合排序。在 BEIR 等检索基准上,它常有不错表现,但是否比单一路线更合适仍要结合领域语料验证。
题 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 更强调在现有 Pipeline 上补一个纠错与降级环节,更容易落地到现有系统。
题 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 系统构建
总结¶
| 模块 | 核心技术 | 常见稳妥做法 |
|---|---|---|
| 分块 | 递归/语义分块 | 先从递归或语义分块起步,再按召回与延迟权衡是否引入 Late Chunking |
| Embedding | BGE/GTE 系列 | 先选与你的数据语言、长度和部署约束匹配的 Embedding 模型,再做实测 |
| 检索 | Dense + Sparse | Dense 起步;若关键词召回不足,再评估 Hybrid 检索 + RRF 融合 |
| 重排 | Cross-Encoder | 只在延迟预算允许时引入重排器 |
| 高级技术 | Query 改写/Self-RAG | 先补齐查询改写、检索诊断,再决定是否需要 Agentic RAG 或自适应检索 |
| 评估 | RAGAS | Faithfulness + Recall@K + 业务标注集联合评估 |
延伸学习: - 检索与 Embedding 基础 →第 3 章 文本表示方法 - BERT 与预训练模型 →第 10 章 预训练语言模型 - 对话系统与 Agent →第 13 章 对话系统与 Agent 化 NLP - RAG 工程实践 →LLM 应用/05-RAG 系统构建 - 高级 RAG 技术 →LLM 应用/18-高级 RAG 技术
最后更新日期: 2026-04-03