Skip to content

RAG 实战

本页速览 用"文档解析→chunking→embedding→向量库→混合检索→重排序→生成→评测"的端到端流程搭一个能上线的 RAG 系统,并用 chunk 实验、HyDE、失败模式表和评测指标把检索质量变成可度量、可优化的工程。

RAG 实战

RAG(Retrieval-Augmented Generation)的本质是把"记忆"从模型参数里搬进外部索引:模型不再负责记住你的私有文档,而是学会"先检索、再回答"。

RAG 是生产级 LLM 应用里被使用最多的模式,没有之一:它同时解决知识截止、幻觉、私有数据、可溯源四个问题。但 RAG 也最容易"看起来能跑、上线就崩"——因为质量瓶颈不在模型,而在检索链路。本文给出端到端的可落地流程,每步都配参数建议与验证方法。概念与演进背景见案例研究:RAG

一、端到端全景

一个 RAG 系统的完整链路:

离线(索引构建)
文档 → 解析 → 清洗 → chunking → embedding → 写入向量库(含元数据)


在线(查询)
用户问题 → 查询改写 → 检索(向量 + BM25)→ 融合 → 重排序 → 组装提示 → 生成 → 校验输出

本文按链路顺序展开。先记住一句总纲:每一环的错误都会向下游放大,而评估只发生在最后——所以你在优化"检索召回"时花的每一分钟,最终都会体现在答案质量上。

这条链路的迭代节奏

阶段关注指标迭代节奏
起步端到端能跑通一版即可,别在细节上停留
检索优化Recall@k / Hit@k每次改动 chunk/检索/重排后重跑评测
生成优化忠实度 / 答案相关提示改动后回归
上线线上反馈 + 抽检持续,回流到 golden set

二、文档解析与 chunking

1. 解析:把"文件"变成"干净的文本"

文档类型解析工具注意点
PDF(文本型)pypdfpdfplumber表格、多栏排版容易乱
PDF(扫描件)OCR(Tesseract、PaddleOCR)先 OCR 再解析,成本高
Word / PPTpython-docxpython-pptx注意页眉页脚、批注
HTML / 网页BeautifulSouptrafilatura去掉导航、广告、脚本
Markdown / 代码直接按结构切保留代码块语义

解析是 RAG 失败的第一大来源

很多"检索不到"的排查最终发现是解析阶段把 PDF 表格切碎了。上线前务必抽样人工检查解析结果(每类文档抽 5~10 页),不要相信解析器默认输出。

2. chunking:切块的学问

chunk(文本块)是检索的最小单元。chunk 太小则上下文不完整,太大则向量被稀释、召回不精准。工程上常用两条经验路线:

策略做法优点缺点
固定大小 + 重叠每 300~800 token 切一块,重叠 10~20%简单、可控可能切断语义完整的句子/段落
结构感知(recursive)按段落→句子→句子的优先级切(如 LangChain RecursiveCharacterTextSplitter尊重文本结构依赖源文档结构质量
语义切分用嵌入相似度找"语义断点"切块块内主题一致计算成本高
python
# LangChain 结构感知切分(最常用的默认方案)
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 每个块的目标字符数(中英文混排时建议按 token 估算)
    chunk_overlap=80,      # 相邻块重叠字符数,防止关键句被切在边界
    separators=["\n\n", "\n", "。", "!", "?", ". ", "! ", "? ", " ", ""],
)
chunks = splitter.split_text(long_document)
print(f"切出 {len(chunks)} 个块,平均 {sum(len(c) for c in chunks)/len(chunks):.0f} 字符")

3. chunk 大小与重叠:用实验说话

chunk 大小没有"标准答案",必须用你自己的文档和问题集做实验。做法是控制变量:

python
import json

# 固定一份 50~100 条真实问题的评测集(q, expected_chunk_id)
test_questions = json.load(open("rag_questions.json"))

def evaluate_chunk_size(chunk_size, chunk_overlap):
    """重建索引,测这批问题的 Top-5 召回率"""
    chunks = split_document(doc, chunk_size, chunk_overlap)
    index = build_vector_index(chunks)          # 见第三节
    hit = 0
    for q in test_questions:
        retrieved = search(index, q, k=5)        # 见第四节
        if q["expected_chunk_id"] in [r["id"] for r in retrieved]:
            hit += 1
    return hit / len(test_questions)

for size in [300, 500, 800, 1200]:
    print(size, evaluate_chunk_size(size, size // 5))

默认起点与调整方向

chunk_size≈500 token、overlap≈10~15% 为起点。若问题是"文档级概述"(需要全局理解),加大 chunk;若问题是"精确定位"(如合同条款),减小 chunk 并提高重叠。永远用召回率/答案质量说话,不要凭感觉。

三、Embedding 与向量库

1. 选 embedding 模型

embedding 把文本映射为稠密向量,语义相近的文本向量距离近。选型维度:

维度建议
语言中文场景优先中文优化模型(如 bge-large-zh、text-embedding-v3 等),通用英文用 text-embedding-3-large 或开源 E5/BGE 系列
维度与成本1024~3072 维精度更高但存储/检索更贵;可先 768~1024 维起步
长文本支持处理长文档块时,注意模型的 max sequence length(多数 512~8192 token)
是否开源数据不出内网时选开源模型(sentence-transformers 生态)
python
# 开源 embedding(sentence-transformers 一行搞定)
pip install sentence-transformers

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-large-zh-v1.5")   # 中文,1024 维
vec = model.encode("退款政策的第三条讲了什么")             # 得到向量
print(vec.shape)                                          # (1024,)

2. 向量库选型

向量库承载"存储向量 + 近似最近邻检索(ANN)"。选型决策表:

向量库形态规模上限场景
FAISS库(嵌入进程)千万级原型、离线批处理、单机
pgvectorPostgreSQL 插件百万~千万级已有 PG 的业务系统、事务与向量共存
Qdrant / Milvus独立服务亿级生产级、多副本、混合检索与过滤
Chroma / LanceDB轻量嵌入式百万级本地原型、教学
python
# FAISS 最小索引(内存版,适合原型)
pip install faiss-cpu

import faiss
import numpy as np

dim = 1024
index = faiss.IndexFlatIP(dim)        # 内积索引(与归一化向量配合=余弦相似度)
vecs = np.array([model.encode(c) for c in chunks])
faiss.normalize_L2(vecs)              # L2 归一化,让内积等价于余弦
index.add(vecs)

q_vec = model.encode("退款政策第三条")
q_vec = q_vec / np.linalg.norm(q_vec)
D, I = index.search(q_vec[np.newaxis, :], k=5)
print("Top-5 chunk 编号:", I[0])       # 按相似度排序

向量检索的局限

向量检索擅长"语义相似",不擅长"精确关键词匹配"(型号、编号、专有名词),这正是第四节混合检索要补的短板。

四、检索:混合检索、查询改写与 HyDE

1. 混合检索(Hybrid Search):BM25 + 向量

关键词检索(BM25) 对精确术语强,向量检索对语义近义强。生产 RAG 几乎都用混合检索再融合:

python
# 示意:把 BM25 与向量检索的 Top-k 结果按权重融合
# rank fusion:对每个结果,score = sum( 1/(60 + rank_i) for i in methods )

def reciprocal_rank_fusion(bm25_results, vector_results, k=60):
    scores = {}
    for rank, doc_id in enumerate(bm25_results + vector_results):
        scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1)
    return sorted(scores.items(), key=lambda x: -x[1])

融合后通常召回率高于任一单路。BM25 的实现rank_bm25 库、Elasticsearch/OpenSearch、或 Qdrant/Milvus 内置的 sparse 检索。

2. 查询改写(Query Rewriting)

用户的原始提问往往不适合直接检索(太口语、太短、缺实体)。三步常见改写:

改写类型示例作用
补全"那家店退款呢" → "XX 店的退款政策是什么"补充缺失实体
多路改写一句话变 2~3 个检索 query增加召回面
拆解"对比 A 和 B 的区别" → 两个子查询分而治之
python
# 查询改写:一个最小实现(LLM 生成多路查询)
def rewrite_queries(question: str) -> list[str]:
    prompt = f"""把下面的问题改写成 2~3 个更适合检索的查询。
要求:补全缺失实体、口语转书面、覆盖不同表达角度。只输出查询,每行一个。
问题:{question}"""
    text = call_llm(prompt)
    return [line.strip("- ").strip() for line in text.splitlines() if line.strip()]

# 检索时:原问题 + 改写查询一起召回,再融合去重
all_results = []
for q in [question] + rewrite_queries(question):
    all_results += search(index, q, k=10)   # search 用上文的向量/BM25 检索
# 然后走 reciprocal rank fusion(见混合检索)合并去重

3. HyDE:让"假设答案"去检索

HyDE(Hypothetical Document Embeddings)的思想:先让 LLM 根据问题凭空写一段"假设答案",再用这段文字的向量去检索。因为"答案形态的文本"比"问题形态的文本"在向量空间里离真实文档更近,能显著提升召回(参考资料)。

python
def hyde_rewrite(question: str) -> str:
    """让 LLM 生成一段假设答案,作为检索的查询文本"""
    prompt = f"""请根据下面的问题,写一段 100 字左右的假设性回答。
即使你不确定事实,也写出一段通顺、结构完整的"看起来像百科条目"的文字。
问题:{question}
假设回答:"""
    return call_llm(prompt)   # 返回这段文字,用它 encode 后去检索

q_vec = model.encode(hyde_rewrite(question))   # 检索用 HyDE 向量

HyDE 的成本与风险

HyDE 每次查询多一次 LLM 调用(延迟 + 成本),且在"模型幻觉会误导检索"的任务上可能帮倒忙。先用不引入额外调用成本的改写试,再决定是否上 HyDE

五、重排序(Reranking)

检索返回 Top-50,重排序模型(cross-encoder)再精排取 Top-5。cross-encoder 把"问题 + 文档块"成对打分,精度远高于向量相似度,是目前 RAG 性价比最高的精度杠杆之一:

python
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-base")      # 中文重排序模型
pairs = [(question, chunk) for chunk in candidate_chunks]
scores = reranker.predict(pairs)                        # 成对打分
top5 = [c for _, c in sorted(zip(scores, candidate_chunks), reverse=True)[:5]]
阶段检索类型输入精度速度
召回(Retrieve)bi-encoder 向量 / BM25单边编码极快(毫秒级)
精排(Rerank)cross-encoder问题+文档对慢(只对候选集做)

六、生成:提示组装

检索到的内容要组装成"可回答"的提示。生成阶段有三个被反复验证的纪律:

text
你是内部知识库助手。只根据提供的资料回答。
规则:
1. 资料不足时,明确说"资料中没有相关信息",不要编造
2. 引用来源编号,如 [1][2]
3. 答案只使用资料内容,不超过资料范围

【资料】
[1](来源:销售手册 v3 第 5 页)……
[2](来源:退款政策 2025 版 第 2 条)……

【问题】
客户问:退款多久到账?

【回答】
  • "只根据资料回答" 是防幻觉的第一道闸(机制分析见幻觉:成因与缓解)。
  • 给资料编号并强制引用:答案可溯源,也方便下游校验。
  • "资料不足就明说":把"不知道"变成合法输出,而不是逼模型编。

上下文别硬塞

把所有检索结果(哪怕 20 块)全塞进提示,会稀释注意力且浪费 token。重排序后只喂 Top-3~5 块;与长上下文的权衡见上下文与长文本

七、RAG 评测:三个维度缺一不可

RAG 的输出质量由检索质量生成质量共同决定,评测必须分层:

维度指标含义评测对象
检索召回Recall@k / Hit@k相关文档是否进了 Top-k检索链路
忠实度(Faithfulness)回答是否都能从资料中找到依据防幻觉生成链路
答案相关(Relevance)回答是否针对问题、不跑题端到端整体
python
# 用 RAGAS 快速量化(忠实度/相关性需要 LLM 打分)
pip install ragas

from ragas.metrics import faithfulness, answer_relevancy
from ragas import evaluate
# 构造 dataset(question / answer / contexts / ground_truth)
result = evaluate(dataset, metrics=[faithfulness, answer_relevancy])
print(result)   # 输出各项 0~1 分数

评测体系的完整方法论(golden set、回归、LLM-as-a-judge 的偏差)见评估实战

八、建立 RAG golden set:评测的前提

RAG 评测的前提是带标准答案的测试集(问题 + 期望答案)。构造要点:

要点说明
问题来源线上真实日志、客服记录,而非自编
答案形态二选一:(a) 期望命中的 chunk id;(b) 期望答案文本
难度分层简单(直接可查)/ 中等(需多块拼合)/ 困难(需跨文档推理)
数量起步 50~100 条即可,持续追加

golden set 建好后,每次改动 chunk / 检索 / 重排 / 提示都重跑一遍——这就是 RAG 的回归测试。完整方法(分层评测、judge、成本控制)见评估实战

九、常见失败模式表

失败现象根因环节排查与修复
检索到的东西"像但不相关"embedding 领域漂移换领域微调 embedding;加查询改写;调融合权重
精确编号/型号搜不到纯向量检索弱于关键词上 BM25 混合检索;必要时正则抽取后精确匹配
答案答非所问chunk 过大/切碎,信息割裂调 chunk 大小;加重叠;用重排序精排
明显编造(资料里没有)生成阶段约束不足强化"只根据资料回答";加引用编号;降低温度
老版本文档覆盖新版本索引未版本化元数据带版本号;检索过滤最新版本
长文档全局问题答不好单块信息不足分块摘要 + 二次检索;或换长上下文模型
新文档上线后不生效索引没重建/缓存建立文档变更驱动的增量索引流程

十、工具链选型

主流选项选型要点
编排框架LangChain / LlamaIndex / 自研原型快用框架,生产考虑自研或轻量封装(见框架与工具选型
向量库FAISS / pgvector / Qdrant / Milvus见第三节决策表
EmbeddingOpenAI embeddings / BGE / E5中文场景优先中文模型
重排序bge-reranker / Cohere Rerank精度杠杆,强烈建议加
评测RAGAS / 自建 golden set见第七节

从最小可行到生产

第一版 RAG 用「PDF 解析 + RecursiveCharacterTextSplitter + 一个开源 embedding + FAISS + 混合检索 + bge-reranker + 3 条提示纪律」即可上线。先把这条链路跑通并建立评测集,再逐环替换更强的组件——每一步升级都用评测分数验证,而不是"感觉更好"。这条"别被框架绑架"的原则同样见框架与工具选型

延伸阅读

参考资料