RAG 体系
相关笔记:Elasticsearch
RAG 看起来就五步:切块、向量化、存、检索、拼。但真正决定效果的,全在细节里 —— 90% 的 RAG 效果问题,都出在「切块」和「召回」,而不是模型。
1. 什么时候该用 RAG
| 场景 | 选择 |
|---|---|
| 模型不知道的私有知识,且要及时更新 | RAG |
| 模型能力/风格不对(不会说行话、格式总错) | 微调 |
| 只是没说清 | 改 Prompt |
| 知识量很小(几页),且不常变 | 直接塞进上下文 |
| 需要跨文档多跳推理 | RAG + 查询改写/多轮检索 |
判断口诀:「不知道」用 RAG,「不会说」才微调。
2. 离线管道:解析与切块
2.1 解析(最脏最累的一步)
PDF 是 RAG 的第一大坑:表格丢结构、双栏串行、扫描件没文字。
| 内容类型 | 处理 |
|---|---|
| 普通 PDF | 优先用结构化解析(PyMuPDF / pdfplumber),别用粗略抽取 |
| 扫描件 | OCR(PaddleOCR / 云服务),注意版面还原 |
| 表格 | 保留为 Markdown/HTML 表格,别拆成一段文字 |
| Word/HTML | 按标题层级解析,天然有结构 |
| 图片 | 多模态模型生成描述,或直接走多模态检索 |
一句经验:输入脏,后面全白搭。清洗(去页眉页脚、去乱码、合并断行)值得花掉整个项目 1/3 的时间。
2.2 切块(chunking)策略
| 策略 | 做法 | 适合 |
|---|---|---|
| 定长 + 重叠 | 512 token,overlap 64 | 通用基线,简单可靠 |
| 按结构切 | 按标题层级/段落切,保留章节路径 | 文档类首选 |
| 语义切块 | 按句子嵌入的相似度找断点 | 长篇连续叙述 |
| 父子块(small-to-big) | 用小块检索,命中后返回其父块 | 兼顾精度与上下文 |
| 句窗(sentence window) | 命中句子后向前后扩 N 句 | 问答型语料 |
三条硬经验:
- 一定要有 overlap(10%~20%),否则答案正好跨在边界上就丢了;
- 每块带上「来源 + 章节标题」(元数据),既提升检索,又方便引用;
- 块大小和问题粒度匹配:问「年假几天」用 200~500 token;问「这个方案怎么做」要大一些。
3. Embedding
| 关注点 | 说明 |
|---|---|
| 模型选择 | 中文场景优先中文/多语言模型(如 bge-m3、text-embedding-3、qwen-embedding) |
| 维度 | 768 / 1024 / 1536,维度越高越贵越慢 |
| 归一化 | 用余弦相似度前要确认模型输出是否已归一化 |
| 对称 vs 非对称 | 问题和文档不同分布,有的模型要求加 query: / passage: 前缀,不加会明显掉点 |
| 一致性 | 索引和查询必须用同一个模型,换模型要全量重建 |
注意:换了 embedding 模型 = 重建整个库。所以选型要在项目早期定,别中途换。
4. 索引与向量库
| 方案 | 适合 | 备注 |
|---|---|---|
| Milvus | 百万~亿级 | 专业向量库,运维成本中高 |
| pgvector | 十万级、已有 PG | 一库两用,最省心 |
| ES 8.x kNN | 已有 ES | 天然支持 BM25 + 向量混合 |
| Qdrant / Weaviate | 中小规模 | 过滤能力强 |
| Redis / FAISS | 小规模 / 离线 | Redis 复用现有栈,FAISS 不是服务 |
HNSW 参数(最常被问):
| 参数 | 作用 | 调大 |
|---|---|---|
M | 每个节点的邻居数 | 召回↑、内存↑、建索引慢 |
ef_construction | 建索引时的候选集 | 精度↑、建索引慢 |
ef_search | 查询时的候选集 | 召回↑、延迟↑ |
没有通用参数,必须用自己的数据集画 recall-latency 曲线。
5. 在线链路:召回才是命门
用户问题
↓ Query 改写(多轮/口语化 → 完整查询)
↓ 并行两路:向量检索(语义)+ BM25(关键词)
↓ RRF 融合
↓ 元数据过滤(租户/时间/权限)
↓ Rerank 精排(cross-encoder,取 top 5)
↓ 拼 Prompt(带编号 + 引用要求)
↓ 模型生成 + 拒答兜底用户问题
↓ Query 改写(多轮/口语化 → 完整查询)
↓ 并行两路:向量检索(语义)+ BM25(关键词)
↓ RRF 融合
↓ 元数据过滤(租户/时间/权限)
↓ Rerank 精排(cross-encoder,取 top 5)
↓ 拼 Prompt(带编号 + 引用要求)
↓ 模型生成 + 拒答兜底5.1 Query 改写
多轮对话里「它多少钱」必须改写为「XX 套餐多少钱」才能召回。手段:用模型改写(多一次小调用)、或用规则拼接上一轮实体。
5.2 混合检索与 RRF
纯向量对专有名词/型号/编号很弱(语义相近但完全不同),所以要加关键词通道。
RRF(Reciprocal Rank Fusion) 是把两路排名融合的标准做法,不需要归一化分数:
score(d) = Σ 1 / (k + rank_i(d)) k 常取 60score(d) = Σ 1 / (k + rank_i(d)) k 常取 60它只看名次不看分数,所以对两路分数量纲不一致的情况特别稳。
5.3 Rerank
- 粗排(向量/BM25)召回 50~100 条;
- 用 Cross-Encoder(如 bge-reranker)逐条打分,取 top 3~5;
- 代价是慢(要过一遍模型),所以只对少量候选做。
实测经验:加 Rerank 通常是性价比最高的一步,尤其候选池大、问题口语化时。
5.4 元数据过滤与权限
- 一定要在检索阶段做租户/权限过滤,不能检索完再筛(会泄露、也会污染 topK);
- 时间过滤对「最新政策」类问题很关键。
6. 生成:拼 Prompt 与拒答
text
资料(编号):
[1] ...
[2] ...
要求:
- 只依据资料回答,资料没有就说「资料中未提及」;
- 每个结论标注来源编号,如 [1];
- 不要编造资料里没有的数字。资料(编号):
[1] ...
[2] ...
要求:
- 只依据资料回答,资料没有就说「资料中未提及」;
- 每个结论标注来源编号,如 [1];
- 不要编造资料里没有的数字。拒答策略比「努力作答」更重要:空召回或相似度低于阈值时,直接说「没找到」,不要硬编。
7. 评估:分两段看
| 层次 | 指标 | 怎么测 |
|---|---|---|
| 检索 | Recall@K、MRR、NDCG、空召回率 | 人工标注「标准答案在第几块」 |
| 生成 | 忠实度(是否来自资料)、答案相关性、引用正确率 | 规则 + LLM-as-judge + 抽检 |
| 端到端 | 通过率、人工评分 | 固定评测集回归 |
建评测集的正确姿势:从真实日志里抽 50~200 条问题 → 标注答案所在文档/片段 → 每次改动跑一遍。
记住顺序:先修检索,再修生成。召不回,模型再强也是编。
8. 进阶
| 方向 | 解决什么 |
|---|---|
| GraphRAG | 跨文档多跳、全局总结(实体关系图) |
| Agentic RAG | 让 Agent 自己决定「检索几次、换什么查询」 |
| 多向量 / ColBERT | 更细粒度匹配(token 级),但存储成本高 |
| 增量更新与删除 | 文档变更时按 docId 删旧块再写新块,别只追加 |
| 缓存 | 相同问题的检索结果缓存,省钱省延迟 |
9. 常见失败模式对照
| 现象 | 大概率原因 | 先查什么 |
|---|---|---|
| 总是答「未提及」 | 空召回 | 切块粒度、embedding 前缀、相似度阈值 |
| 答得对但引用错 | 编号与片段对应错位 | 拼 Prompt 时的编号是否稳定 |
| 关键数字错 | 表格被拆坏 | 解析阶段 |
| 多轮对话越聊越偏 | 没做 Query 改写 | 改写链路 |
| 召回内容相关但不解决问题 | 缺 Rerank | 加 cross-encoder |
| 老文档还在被引用 | 没做删除/覆盖 | 增量更新逻辑 |
10. 速查表
| 问题 | 一句话 |
|---|---|
| 什么时候用 RAG | 私有知识、要更新、不想微调 |
| 切块多大 | 200~500 token,overlap 10%~20%,按结构切优先 |
| 换 embedding 模型要做什么 | 全量重建索引 |
| 为什么加 BM25 | 专有名词/编号类查询向量检索不准 |
| RRF 是什么 | 按名次融合,Σ 1/(k+rank),k≈60 |
| Rerank 值不值 | 通常最划算的一步,粗排 50 取 5 |
| 怎么评估 RAG | 检索看 Recall@K,生成看忠实度 |
| 先优化哪 | 先检索后生成,空召回优先修 |
| 拒答重要吗 | 重要,空召回必须说「未提及」,别硬编 |
Cao Note