RAG 检索增强生成:为什么 "检索+生成" 是当前最务实的 LLM 落地方案
2023 年初,我跟一个做企业知识库产品的朋友聊天。他说:"我用 ChatGPT API 搭建了一个内部问答系统,准确率做到 60%,老板问我为什么不是 100%。"
我说:"因为你期望一个没见过你们公司数据的模型,回答你们公司的问题。"
这就是 RAG(Retrieval-Augmented Generation,检索增强生成)存在的全部理由。LLM 的知识截止于训练数据日期——你公司上个月发布的内部政策,它能知道才有鬼。RAG 是在 LLM 回答之前,先帮它 "查资料",然后基于资料回答问题。 听起来简单,但真正的 engineering 在细节里。
不是 "搜+答",是一个精密的信息供应链
RAG 的原始论文(Lewis et al., 2020,Meta AI(Facebook AI Research))提出的流程很清晰:
离线阶段:文档 → 分块 → 向量化(Embedding) → 存入向量数据库
在线阶段:用户 Query → 向量化 → 检索 Top-K 相关文档块 → [Query + 文档] → LLM 生成回答但在实际工程中,每一步都有无数细节会显著影响最终效果。我见过太多项目,embedding 用了 OpenAI 默认的 text-embedding-ada-002、chunk size 用了文档教程里的 500、vector db 用了 "GitHub star 最多" 的 Pinecone——结果准确率 50% 不到,然后开始怀疑 "RAG 是不是不适合我的场景"。
不是 RAG 不行,是你的 RAG 太糙。
分块(Chunking):检索质量的第一道关口
你有一本 500 页的用户手册,你不可能把整本书塞给 LLM——太长也太多无关信息。你需要把它切成小块。怎么切?这就是分块策略。
固定大小分块——最简单但最常用:每 512 个 token 一块,块之间重叠 64 个 token(重叠率 12.5%)。重叠是必须的——如果你恰好把一个关键概念切在两块的边界上(比如 "## 故障排除 第三章" 在块 A 的末尾,实际的故障排除步骤在块 B 的开头),没有重叠的话这两块谁都回答不了 "怎么排查网络故障"。
参数选择:chunk_size 通常在 256-1024 之间。太大——一块包含太多不相关信息,embedding 被平均化,检索噪音大。太小——一块的信息不足以支撑完整回答。我个人的经验值:对 Q&A 场景用 512,对需要上下文的摘要场景用 1024。
语义分块——比固定大小智能得多但实现成本高。做法:用一个小型模型(比如 Sentence-BERT)把文档拆成句子,每句生成一个 embedding,然后用 embedding 之间的相似度变化来找 "自然分割点"。当相邻两句的 embedding 相似度突然大幅下降时,说明话题变了——这里就该切一刀。
语义分块的效果在长文档、多主题文档上显著好于固定分块——因为一块一个主题,embedding 的表达了更集中、检索更精准。但代价是分块本身需要推理(虽然不是 LLM 推理但也费 GPU),对百万级文档以上的规模需要仔细考虑性价比。
还有两个容易被忽略的策略点:
- 父文档检索(Parent Document Retrieval):检索时用小块(语义清晰、匹配精确),但实际喂给 LLM 的是小块所属的大块(提供完整上下文)。实现方式是在 metadata 里标记 "parent_chunk_id"。
- 文档结构感知(Structure-aware chunking):如果你的文档是 Markdown 写的,不要用固定大小切——按
##标题切。标题本身带有强烈的语义信号,一个## 定价方案下的所有内容显然应该放在一块。
Embedding 模型:不是所有向量都一样
选 embedding 模型的时候,很多人直接上 OpenAI 的 text-embedding-3-large——因为它方便。但我做过的几个中文项目里,bge-large-zh(BAAI,2023)在中文检索表现上明显更好——毕竟它的训练语料就是中文为主。
| 模型 | 维度 | 最大输入 | 特点 | 适用场景 |
|---|---|---|---|---|
| text-embedding-3-large | 256-3072(可调) | 8191 token | 通用强、多语言、支持降维 | 英文为主的通用检索 |
| text-embedding-3-small | 512-1536 | 8191 token | 3-large 的廉价版,质量略差 | 成本敏感场景 |
| bge-large-zh-v1.5 | 1024 | 512 token | 中文场景领先 | 中文 FAQ、知识库 |
| bge-m3 | 1024 | 8192 token | BAAI 最新多语言,支持长文本 | 多语言、长文档 |
| jina-embeddings-v3 | 1024 | 8192 token | LoRA 适配器、支持任务特定微调 | 需要定制化的场景 |
text-embedding-3-large 有个骚操作:它的维度是可调的(通过 API 参数指定降到 256/512/1024/1536/3072)。更低维度 = 更少显存 + 更快检索 + 轻微精度损失。我一般在原型阶段用 3072 验证上限,上线后根据 latency 要求降到 1536 甚至 512。
不要低估 embedding 模型的重训练价值。 如果你有一个 10 万条的领域数据,在开源 embedding 模型上做 contrastive fine-tuning,召回率通常能提升 5-15%。具体操作:用 GPT-4 生成 (query, positive_doc, negative_doc) 三元组,用 InfoNCE loss 做对比学习。
向量数据库:HNSW 才是核心
市面上向量数据库很多——Pinecone、Milvus、Qdrant、Chroma、Weaviate——但其实它们底层的核心索引算法只有几个:HNSW(Hierarchical Navigable Small World)和 IVF(Inverted File Index)是两个主流。
HNSW 是一个极其精妙的数据结构: 它把数据点组织成一个多层图——顶层稀疏(少数 "跳板" 点做粗粒度导航),底层稠密(精细搜索)。搜索时从顶层的一个节点开始,贪心地往离查询向量更近的邻居移动,下到更低层后继续——最终在底层找到精确最近邻。时间复杂度 O(log N),空间复杂度 O(N·M)(M 是每个节点的最大连接数,通常 16-64)。
HNSW 为什么这么快?因为它本质上是一个 "skip list for high-dim vectors"——多层结构让搜索在 log 级复杂度下就能锁定大致区域,不需要扫描所有向量。
选库指南:
- 原型阶段:Chroma——纯 Python,一行 import 就能用,不需要起服务
- 生产环境(千万级文档):Qdrant,Rust 实现,高性能,支持复杂过滤条件(属性过滤 + 向量搜索同时进行),自带 REST API
- 超大规模(十亿级以上):Milvus——支持分布式部署、GPU 索引、混合查询,但运维复杂度高
- 不想运维:Pinecone——全托管,贵但是省心
一个小建议:除非你的数据量真的到了十亿级,否则 Qdrant 足够满足 99% 的场景——而且因为它是单机部署的,排错远比 Milvus 容易。
从 "搜到啥用啥" 到 "搜到最好的几个再挑一下"
最近几年 RAG 最关键的改进,不是在检索这一步,而是在 检索后的优化。
混合检索(Hybrid Search)
向量检索(dense retrieval)善于理解语义相似——"车" 能召回 "汽车"——但做不好精确的 keyword 匹配。"AAPL 2024 Q3 净利率" 这样的查询,向量检索可能会跑偏到 "苹果产品的利润率",因为语义上太接近了。这就是为什么需要 BM25(稀疏检索)——基于词频的经典算法,没有 "语义理解",但能精准匹配 "AAPL" 这个 ticker。
主流做法是同时跑 dense(向量)和 sparse(BM25),然后用 加权融合(weighted fusion)或 RRF(Reciprocal Rank Fusion) 合并结果。RRF 公式:
RRF_score(d) = Σ(1 / (k + rank_i(d))) # k 通常取 60每个检索器给每个文档一个排名,对排名取倒数求和——两个检索器都排名前几的文档会获得高分。经验值:dense 权重 0.7/sparse 权重 0.3 是一个不错的默认起点。
HyDE(Hypothetical Document Embeddings)
高德研究(Gao et al., 2022)提出一个天才的设计:在检索之前,先让 LLM 生成一个 "假设性回答",然后用这个回答去做 embedding 检索。
原理:用户的查询通常很短("量子计算是什么"),embedding 的表示很 sparse。但如果你先让 LLM 写一段 100 字的 "假设性回答"("量子计算是一种利用量子比特的叠加态和纠缠态..."),这段回答的 embedding 表面距离真实相关文档会更近——因为真实文档也是这种阐述性的语言风格。
实践中,HyDE 对抽象概念类的查询提升最明显(+10-15% 召回),对事实性的精确查询提升较小。
重排序(Re-ranking)
初检用向量检索(速度快),初检后用更强但更慢的模型重新排序 Top-K(比如 Top-50 中挑最好的 5 个)。
- BGE-Reranker-v2(BAAI):cross-encoder 结构——把 query-document 对一起喂进 BERT 大小的模型,输出一个相关性分数。比 embedding-based 的双塔模型准得多(因为 query 和 doc 在 Transformer 内部做了 full cross-attention),但慢(不能预制 embedding)。召回率提升 10-20%
- Cohere Rerank(商业 API):效果最好(他们在 reranking 领域下了重注),但每 1000 次搜索 $1-2
- LLM-based Reranking:直接让 GPT-4 评价文档相关性——最准也最贵,只在资金充裕的项目上见过有人做
一个实际的检索 pipeline(我的常用配置):
查询 → HyDE 生成 → 并行 dense+BM25 检索 → 各取 Top-100
→ RRF 融合 → 取 Top-50
→ BGE-Reranker 重排 → 取 Top-5
→ LLM 生成增加重排这一步的成本很小(一次推理几十 ms),但端到端准确率的提升非常可观。
GraphRAG:当平铺检索不够用的时候
微软在 2024 年发布的 GraphRAG 突破了传统 RAG 的一个核心局限:它只能检索 "片段",不能回答需要 "全局理解" 的结构性问题。
比如你问 "这篇文章讨论了哪几个主题,它们之间的关系是什么?"——传统 RAG 只能检索到包含 "主题"、"讨论" 关键词的片段,但没法告诉你整体的主题结构。GraphRAG 的做法是:首先用 LLM 从整个文档语料中抽取实体(Entity)和关系(Relation)构建一个知识图谱,然后在查询时对图谱做遍历——找到跟查询相关的实体,沿关系展开相关的社区(Community),用子图的信息辅助回答。
代价:构建图索引需要大量 LLM 调用——微软的原论文里,一个 1M token 的语料就要几千次 LLM 调用来抽取实体和关系。这对实时检索来说是沉重的前处理成本。GraphRAG 目前最适合 "对知识质量要求很高、更新频率很低" 的场景——比如一个学术论文库的深度分析、法律文献的跨文档关联。
常见翻车场景与排查方法
翻车 1:检索到了但答案不对
这通常不是检索的问题,是 chunk 的质量问题——检索找到了对的区域但上下文不充分。解决方案:增大 chunk size,或使用父文档检索。
翻车 2:检索不到任何相关内容
你的 embedding 向量和文档向量之间的距离太大了——它们在向量空间里不在同一片区域。常见原因:查询是中文但 embedding 模型针对英文训练、查询太口语化、文档充满领域黑话。解决方案:换 embedding 模型、对查询做 query expansion(让 LLM 把短查询扩展成详细描述)、做领域 fine-tuning。
翻车 3:找到了相关片段但 LLM 不 "信任" 它
这是 prompt 的问题——你的 prompt 里缺乏清晰的指令:"如果检索到的信息不充足,请告诉用户你无法回答,而不是编造。" 很多 RAG 系统在检索结果质量差时 LLM 就自信地胡说,因为你根本没告诉它 "检索可能失败"。
翻车 4:回答正确但丢失细节丢失
检索到的 chunk 包含了正确信息,但 LLM 在生成时 "概括化" 了,丢失了数字和公式等关键细节。这个问题没有银弹——需要反复调 prompt,强调 "保留原文中的数字、公式和精确表述"。
最后的话
RAG 是当前最实用的 LLM 落地范式——它在回答质量上能做到纯 LLM 做不到的事(知识及时性、事实准确度),在成本上远低于微调或重新训练。但它不是银弹——RAG 的质量上限取决于你的数据质量和 retrieval pipeline 的工程精细度。 好的 RAG 系统(分块策略、embedding 选型、混合检索、reranking)和 "搭了个 demo 能跑的 RAG" 之间,准确率可以差 30% 以上。工程细节决定成败。