一文彻底搞懂 RAG — Retrieval‑Augmented Generation
本文全面梳理了 RAG(Retrieval‑Augmented Generation) 的核心概念、技术细节与落地实践,适合作为 优快云 的科普/实战教程。阅读完毕后,读者将能够:
- 明确 RAG 的整体架构与关键模块;
- 基于 Elasticsearch 独立实现“向量检索 → 多路召回 → 重排序 → 生成问答”的端到端流程;
- 深入理解向量、相似度、知识图谱召回等技术原理,并掌握常见工程坑位及优化策略。
目录
-
- 关键词召回(BM25)
- 向量召回(语义检索)
- 知识图谱/规则召回
- Chunk 策略 与候选池合并
-
- 索引映射与相似度模型
- 数据写入脚本
knn
查询 vsscript_score
查询- 向量归一化与分数解释
1. 何为 RAG?解决什么问题?
Retrieval‑Augmented Generation 由 Meta AI 在 2020 年提出,旨在将检索和生成能力结合,以解决大语言模型在实时性、私有数据访问和长尾知识覆盖方面的不足。
2. RAG 总体流程拆解
- Query 预处理 → 分词、语言检测、实体识别。
- 多路召回 → 关键词、向量、知识图谱等并行召回,汇总候选池。
- 粗排(可选) → 简易向量或 BM25 分数过滤,保留 Top‑N(如 500)。
- 重排序 → Cross‑Encoder / Rank‑BERT 对 (query, doc) 对精排,输出最终 Top‑k(如 10)。
- Prompt 构造 → 将检索结果与用户问题拼接,送入 LLM。
- 生成答案 → LLM 基于外部知识生成响应。
3. 第一层:多路召回(Recall)
3.1 关键词召回(BM25)
基于倒排索引的精确匹配,适合短语义明确的查询,速度快、稳定。
3.2 向量召回(语义检索)
将文本切块 → Embedding → 存入 ES dense_vector
字段,使用 knn
查询找 Top‑K 最近邻。
Chunk 是否必要?
场景 | 是否分块 | 说明 |
---|---|---|
长网页 / PDF | ✅ 必须 | 512 token 限制,且匹配需更细粒度 |
FAQ / 标题 | ❌ 可以不分 | 文本短,可整体向量化 |
Chunk 示例(滑动窗口)
def split_to_chunks(text, max_len=150, stride=50): words = text.split() for i in range(0, len(words), stride): yield " ".join(words[i:i+max_len])
3.3 知识图谱/规则召回
通过实体→关系→实体多跳路径补全召回,示例:(高血压)-[:治疗方式]->(降压药)
。
Cypher 查询示例 (Neo4j)
MATCH p=(q:Entity {name:"高血压"})-[:治疗方式|包含*1..2]-(m)
RETURN m.name LIMIT 50;
3.4 候选池合并
多路结果 → 去重 → 打标签(来源、分数),形成统一候选池。
4. 第二层:重排序(Re‑ranking)
阶段 | 模型 | 优点 | 缺点 |
---|---|---|---|
首轮召回 | Bi‑Encoder 向量检索 | 毫秒级 | 误召、漏召 |
重排序 | Cross‑Encoder | 高精度 | 计算慢,仅能处理 Top‑N |
Cross‑Encoder 代码示例:
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
tokenizer = AutoTokenizer.from_pretrained("cross-encoder/ms-marco-MiniLM-L-6-v2")
model = AutoModelForSequenceClassification.from_pretrained(...)
def rerank(query, docs):
scores = []
for doc in docs:
inputs = tokenizer(query, doc, truncation=True, return_tensors="pt")
score = model(**inputs).logits.squeeze().item()
scores.append(score)
return [doc for doc,_ in sorted(zip(docs,scores), key=lambda x: x[1], reverse=True)]
5. Elasticsearch 向量检索实战
5.1 创建索引映射
PUT rag_chunks
{
"mappings": {
"properties": {
"text": {"type": "text"},
"vector": {
"type": "dense_vector",
"dims": 384,
"index": true,
"similarity": "cosine"
}
}
}
}
5.2 数据写入脚本
import requests, json, uuid
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
text = "高血压是一种常见慢性病…"
for chunk in split_to_chunks(text):
vec = model.encode(chunk, normalize_embeddings=True).tolist()
doc = {"text": chunk, "vector": vec}
requests.post("http://localhost:9200/rag_chunks/_doc/"+str(uuid.uuid4()), json=doc)
5.3 knn
查询 vs script_score
knn_query = {
"knn": {
"field": "vector",
"query_vector": query_vec.tolist(),
"k": 5,
"num_candidates": 100
}
}
- knn:ES 8.x 内置 HNSW,适合大规模;
- script_score:灵活,可自定义
cosineSimilarity()
,适合小数据量或混合打分场景。
5.4 向量归一化与 _score
解释
similarity: "cosine"
时_score∈[-1,1]
;l2_norm
时,ES 将距离转为1/(1+distance)
。
6. 深入理解向量相似度
6.1 余弦相似度
sim ( A , B ) = A ⋅ B ∥ A ∥ ∥ B ∥ \text{sim}(A,B)=\frac{A·B}{\|A\|\|B\|} sim(A,B)=∥A∥∥B∥A⋅B
6.2 欧氏距离
d ( A , B ) = ∑ ( a i − b i ) 2 d(A,B)=\sqrt{\sum(a_i-b_i)^2} d(A,B)=∑(ai−bi)2
6.3 点积
A ⋅ B = ∑ a i b i A·B=\sum a_ib_i A⋅B=∑aibi
6.4 Python 实验对比
vec1, vec2 = embed_text("你好"), embed_text("您好")
print(np.dot(vec1, vec2)) # cosine (已归一化)
print(1 - np.linalg.norm(vec1-vec2)/2) # 归一化后欧氏距离转相似度
7. 知识图谱召回:结构化语义跳跃
- 实体识别 + 链接 →
高血压
→ entityID; - 路径扩展:一到两跳获取关联实体;
- 反向检索文档:根据实体名称检索 chunk。
优点:补全召回、可解释;缺点:维护成本较高。
8. 端到端 RAG Demo(Python)
# 1. 生成查询向量
query = input("用户问题 > ")
q_vec = model.encode(query, normalize_embeddings=True)
# 2. Elasticsearch KNN 检索
hits = es.search(index="rag_chunks", knn={"field":"vector","query_vector":q_vec.tolist(),"k":20,"num_candidates":200})
chunks = [h['_source']['text'] for h in hits['hits']['