RAG 检索增强生成
RAG 检索增强生成
RAG 是企业 AI 落地最核心的技术路径,支撑知识库问答、智能客服、文档助手等高价值场景。掌握生产级 RAG 是与普通 API 调用者拉开差距的关键。
写在前面
阿里、腾讯、美团的 AI 工程师面试,RAG 是必考大题。面试官不会只问"RAG 是什么",而是会追问:分块策略怎么选?检索召回率低怎么优化?向量数据库为什么要用 HNSW 而不是暴力搜索?混合检索的 RRF 融合怎么算? 本文从生产实践角度系统梳理 RAG 核心技术栈。
RAG 原理与核心价值
三阶段架构
RAG(Retrieval-Augmented Generation,检索增强生成)分为三个阶段:
阶段一:Indexing(离线构建索引)
原始文档 → 文本提取 → 分块(Chunking)→ Embedding 向量化 → 存入向量数据库阶段二:Retrieval(在线检索)
用户问题 → Embedding 向量化 → 向量相似度搜索 → Top-K 相关文档片段阶段三:Generation(生成答案)
检索上下文 + 用户问题 → LLM → 基于上下文的准确答案下图直观展示了 RAG 三阶段的完整数据流转过程:
为什么不直接把文档塞进上下文?
| 方案 | 问题 |
|---|---|
| 把所有文档塞进上下文 | Token 爆炸(成本极高)、"Lost in Middle" 问题、无法处理海量文档 |
| 模型微调 | 成本高(时间+算力)、知识更新需重新微调、无法追溯答案来源 |
| RAG | 按需检索、可实时更新(改文档库即可)、答案可追溯来源、成本可控 |
文档分块策略(决定 RAG 质量的第一关)
分块策略直接影响检索效果:块太大 → 包含多个主题,检索噪声多;块太小 → 语义不完整,答案缺乏上下文。
策略一:固定大小分块(快速原型)
@Component
public class FixedSizeChunkingService {
public List<Document> chunk(List<Document> documents) {
TokenTextSplitter splitter = new TokenTextSplitter(
512, // chunkSize:每块 Token 数
50, // minChunkSizeChars
10, // minChunkLengthToEmbed
10000, // maxNumChunks
true // keepSeparator
);
return splitter.apply(documents);
}
}参数建议:
- chunk_size:512-1024 Token(中文对话类文档用 512,技术文档用 1024)
- overlap:chunk_size 的 10%-20%(保证语义连续性)
策略二:递归字符分割(推荐默认)
按段落 → 句子 → 字符的优先级递归分割,尽量保持语义完整。适合绝大多数文档类型。
策略三:父子分块(检索质量最高)
小块用于检索(精准),大块用于生成(上下文丰富):
@Service
public class ParentChildChunkingService {
private final VectorStore vectorStore;
public void indexWithParentChild(List<Document> documents) {
for (Document doc : documents) {
// 父块:512 Token,存储完整上下文
List<Document> parentChunks = splitIntoChunks(doc, 512, 50);
for (Document parent : parentChunks) {
String parentId = UUID.randomUUID().toString();
parent.getMetadata().put("chunk_type", "parent");
parent.getMetadata().put("parent_id", parentId);
// 子块:128 Token,用于向量检索(更精准)
List<Document> childChunks = splitIntoChunks(parent, 128, 20);
for (Document child : childChunks) {
child.getMetadata().put("chunk_type", "child");
child.getMetadata().put("parent_id", parentId); // 关联到父块
}
vectorStore.add(childChunks); // 只索引子块
parentStore.save(parent); // 父块存 Redis/DB
}
}
}
public List<String> retrieveWithParentExpansion(String query) {
// 1. 用子块检索(精准)
List<Document> childDocs = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(10)
.withFilterExpression("chunk_type == 'child'"));
// 2. 找对应父块(上下文丰富)
Set<String> parentIds = childDocs.stream()
.map(d -> (String) d.getMetadata().get("parent_id"))
.collect(Collectors.toSet());
return parentIds.stream()
.map(id -> parentStore.get(id))
.map(Document::getContent)
.collect(Collectors.toList());
}
}向量数据库选型
主流产品对比
| 数据库 | 特点 | 推荐场景 |
|---|---|---|
| Milvus | 专用向量库,支持十亿级,功能最全 | 大规模生产(>1000 万向量) |
| Qdrant | Rust 实现,高性能,API 优雅 | 中大规模,追求性能 |
| PGVector | PostgreSQL 扩展,SQL 可用 | 已有 PG、数据量 <500 万 |
| Chroma | 开发友好,本地易部署 | 开发测试、原型验证 |
| Redis Vector | 低延迟,与现有 Redis 整合 | 实时推荐、语义缓存 |
选型决策:
- 已有 PostgreSQL → PGVector(零新增依赖)
- 中小规模生产(<100 万向量)→ Qdrant
- 大规模生产(>1000 万向量)→ Milvus
- 开发/原型阶段 → Chroma 或 SimpleVectorStore(Spring AI 内置)
以下决策图帮助快速定位合适的向量数据库:
ANN 索引算法:为什么不用暴力搜索
100 万条 1536 维向量,暴力穷举计算余弦相似度需要 15 亿次浮点乘法,每次查询约 5 秒。
HNSW(分层小世界图)是目前最常用的 ANN 索引:
- 将向量组织为多层稀疏图,上层是"高速公路",下层是"本地道路"
- 搜索时从上层快速定位区域,再在下层精确查找
- 效果:100 万向量的查询时间从 5 秒降至 1-10 毫秒,精度损失 < 1%
Spring AI 统一 VectorStore API
@Service
public class KnowledgeBaseService {
private final VectorStore vectorStore; // 可换 Milvus/PGVector/Chroma
private final EmbeddingModel embeddingModel;
/**
* 文档入库
*/
public void ingestDocument(String content, Map<String, Object> metadata) {
Document doc = new Document(content, metadata);
vectorStore.add(List.of(doc));
}
/**
* 语义检索(带元数据过滤)
*/
public List<Document> search(String query, String tenantId, int topK) {
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(0.7) // 过滤低相关性结果
.withFilterExpression("tenant_id == '" + tenantId + "'")
);
}
/**
* RAG 问答
*/
public String answer(String question, String tenantId) {
List<Document> context = search(question, tenantId, 5);
if (context.isEmpty()) {
return "抱歉,知识库中没有找到相关信息。";
}
String contextText = context.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n---\n\n"));
return chatClient.prompt()
.system("""
你是知识库问答助手。请基于以下参考资料回答问题。
如果参考资料中没有相关信息,请明确说明"知识库中没有相关信息",不要编造。
参考资料:
""" + contextText)
.user(question)
.call()
.content();
}
}检索质量优化
混合检索(Hybrid Search)
单纯向量检索有时会漏掉关键词精确匹配的文档,混合检索结合向量检索(语义)和 BM25(关键词),用 RRF 融合排序:
@Service
public class HybridSearchService {
private final VectorStore vectorStore;
private final ElasticsearchClient esClient; // BM25 全文检索
public List<Document> hybridSearch(String query, int topK) {
// 1. 向量检索(语义理解)
List<Document> vectorResults = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(topK * 2));
// 2. BM25 关键词检索
List<Document> bm25Results = esClient.search(query, topK * 2);
// 3. RRF(Reciprocal Rank Fusion)融合排序
return rerankWithRRF(vectorResults, bm25Results, topK);
}
/**
* RRF 融合排序:score = Σ(1 / (k + rank_i)),k=60 为常数
*/
private List<Document> rerankWithRRF(List<Document> list1,
List<Document> list2,
int topK) {
Map<String, Double> rrfScores = new HashMap<>();
int k = 60;
for (int i = 0; i < list1.size(); i++) {
String id = list1.get(i).getId();
rrfScores.merge(id, 1.0 / (k + i + 1), Double::sum);
}
for (int i = 0; i < list2.size(); i++) {
String id = list2.get(i).getId();
rrfScores.merge(id, 1.0 / (k + i + 1), Double::sum);
}
// 按 RRF 分数排序,取 top-K
return rrfScores.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(topK)
.map(e -> findDocById(e.getKey(), list1, list2))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}Reranking 重排序
两阶段检索架构:第一阶段用 Embedding 双编码器快速召回大量候选,第二阶段用 Reranker 交叉编码器精细排序,兼顾效率与精度:
初步检索召回 20-50 个候选,用专门的 Reranker 模型(如 bge-reranker-v2-m3)对查询和文档进行精细排序,取 top-5 作为最终上下文:
@Service
public class RerankerService {
private final RestTemplate restTemplate;
public List<Document> rerank(String query, List<Document> candidates, int topK) {
// 调用 bge-reranker 模型 API
List<Map<String, String>> pairs = candidates.stream()
.map(doc -> Map.of("query", query, "passage", doc.getContent()))
.collect(Collectors.toList());
RerankResponse response = restTemplate.postForObject(
"http://reranker-service/rerank",
Map.of("pairs", pairs),
RerankResponse.class);
// 按 Reranker 分数重新排序
return IntStream.range(0, candidates.size())
.boxed()
.sorted((a, b) -> Double.compare(
response.getScores().get(b),
response.getScores().get(a)))
.limit(topK)
.map(candidates::get)
.collect(Collectors.toList());
}
}HyDE(假设文档扩展)
检索前先让 LLM 生成一个"假设的理想答案",用假设答案的向量去检索,比直接用问题检索效果更好(因为答案向量与文档向量分布更接近):
public List<Document> hydeSearch(String question, int topK) {
// 1. 生成假设文档
String hypotheticalAnswer = chatClient.prompt()
.system("请生成一段回答以下问题的假设性文档段落,不需要真实,只要风格和格式符合:")
.user(question)
.call()
.content();
// 2. 用假设答案检索(而非原始问题)
return vectorStore.similaritySearch(
SearchRequest.query(hypotheticalAnswer).withTopK(topK));
}嵌入模型选型
| 模型 | 维度 | 中文效果 | 部署方式 | 推荐场景 |
|---|---|---|---|---|
| text-embedding-3-large | 3072 | 良好 | API | 通用场景,质量最高 |
| BGE-M3 | 1024 | 极佳 | 本地/API | 中文为主的知识库 |
| bge-large-zh-v1.5 | 1024 | 极佳 | 本地 | 纯中文,私有化部署 |
| nomic-embed-text | 768 | 良好 | Ollama 本地 | 开发测试,零成本 |
实践建议:国内中文知识库优先用 BGE-M3,开发阶段用 nomic-embed-text(Ollama 本地运行,零费用)。
RAG 评估:RAGAS 框架
生产级 RAG 必须建立评估体系,不能只靠"感觉好像准确":
| 指标 | 含义 | 目标值 |
|---|---|---|
| Context Recall | 答案需要的信息有多少在检索结果中 | > 0.8 |
| Context Precision | 检索结果中有多少是真正相关的 | > 0.7 |
| Answer Faithfulness | 答案有多少内容有上下文支撑(抗幻觉) | > 0.9 |
| Answer Relevancy | 答案与问题的相关程度 | > 0.8 |
高频面试题
Q: RAG 和微调(Fine-tuning)各自适用什么场景?
RAG 适合:需要最新信息(LLM 知识有截止日期)、企业私有知识、答案需要来源追溯、知识频繁更新。微调适合:需要特定回复风格、垂直领域专业术语理解、提升特定任务准确率(如领域分类)。核心区别:RAG 改变 LLM 的输入(注入上下文),微调改变 LLM 的权重(改变模型本身)。实践原则:先用 RAG(快速低成本),效果不满意再考虑微调。
Q: 分块大小如何选择?
没有银弹,需要根据文档类型和查询模式决定:技术文档/代码用 512-1024 Token(内容密集);FAQ 类文档用 256-512 Token(每条相对独立);长篇报告用父子分块(子块 128 Token 用于检索,父块 512 Token 用于生成)。选定后必须在评测集上验证 Context Recall 指标,而不是凭感觉。
Q: 向量检索的相似度指标如何选?
三种主流相似度:余弦相似度(方向相似,不受向量模长影响,最常用)、欧氏距离(绝对距离,受向量模长影响)、内积(速度最快,但向量需归一化才等价于余弦)。实践中使用余弦相似度(大多数 Embedding 模型已对输出归一化,此时内积等价于余弦,Milvus/PGVector 默认都支持)。
Q: 混合检索(Hybrid Search)为什么比纯向量检索效果好?
向量检索擅长语义相似("苹果手机"能找到"iPhone 评测"),但对精确关键词(如人名、产品型号、代码片段)效果差。BM25 关键词检索正好相反。混合检索结合两者优势:用 RRF(倒数排名融合)将两个排名列表合并,实验证明混合检索通常比单一方法高 10-20% 的召回率。
Q: 什么是 Reranking?什么情况下需要用?
Reranking 是用精细的交叉编码器(Cross-Encoder)模型对初步检索结果重新排序。与 Embedding 检索(双编码器,分别编码查询和文档,速度快)不同,Reranker 同时输入查询和文档,理解两者的交互关系,排序质量更高。适合场景:对检索精度要求极高时(如法律/医疗问答)、召回后需要 Top-3 高精度时使用。不建议每次都用 Reranker,因为延迟增加约 100-500ms。
Q: RAG 系统中幻觉如何控制?
四个层面控制:1)提示词约束:"如果参考资料中没有相关信息,请明确说不知道,不要编造";2)相似度阈值过滤:检索结果相似度低于 0.7 时不注入上下文,直接回答"知识库中未找到";3)Answer Faithfulness 评估:用 RAGAS 定期评估答案是否有上下文支撑;4)引用来源:让 LLM 在回答中引用具体的文档片段,便于用户核实。
Q: PGVector 和 Milvus 如何选择?
PGVector:PostgreSQL 扩展,优势是无需新增基础设施(已有 PG 直接用),支持事务和 SQL 联合查询,适合 < 500 万向量的场景。Milvus:专用向量数据库,支持十亿级向量,查询延迟更低,功能更全(多种索引类型、GPU 加速),适合大规模生产。团队已有 PostgreSQL 且向量规模不大就用 PGVector;需要大规模且性能要求高用 Milvus。
知识星球深度内容
完整大厂面经(含详细答案、最新更新)、AI 项目源码、1v1 简历修改,扫码加入「AI 工程师加速社区」知识星球获取 👉 立即加入
