RAG 优化实战:别被那些教程骗了
搭了三个 RAG 项目踩了五个坑。分块策略、向量库选型、混合检索、重排序、缓存——每个环节都有真实踩坑经验,附代码。
# RAG 优化实战:别被那些教程骗了
RAG(Retrieval-Augmented Generation)现在满天飞,各种博客教你怎么搭一个向量检索系统。我搭了三个项目,踩了五个坑,今天把实话说了。
先说最痛的:分块(Chunking)策略
几乎所有教程都说"把文档切成小块",但没告诉你怎么切。
我最初用的是固定长度切分,比如每 500 字一块。结果检索效果差得要命——一段话被切到两块里,语义就断了。后来换成按段落切,效果好多了,但又遇到新问题:一个长段落可能包含多个主题,检索的时候会把不相关的信息也拉进来。
**我的做法:**
import re
def smart_chunk(text, max_chars=800, overlap=50):
"""按段落切,超过 max_chars 再按句子切"""
paragraphs = re.split(r'\n\s*\n', text)
chunks = []
current = ""
for para in paragraphs:
para = para.strip()
if not para:
continue
if len(current) + len(para) <= max_chars:
current += para + "\n\n"
else:
if current:
chunks.append(current.strip())
# 如果单段太长,按句子切
if len(para) > max_chars:
sentences = re.split(r'(?<=[。!?.!?])\s*', para)
current = ""
for sent in sentences:
if len(current) + len(sent) <= max_chars:
current += sent
else:
if current:
chunks.append(current.strip())
current = sent
else:
current = para + "\n\n"
if current:
chunks.append(current.strip())
return chunks
关键点:**保留语义完整性比控制块大小更重要**。宁可让块大一点,也别把一句话切两半。
向量数据库选型:我踩过的坑
最开始用的是 Chroma,简单上手,但后来数据量大了之后,查询速度慢得让人抓狂。10 万条向量,每次查询要 2-3 秒。
换了 Pinecone,贵是真贵,快也是真快。但问题来了:你没法自己控制索引策略,数据存在别人那儿,隐私也是个问题。
最后用了 Milvus,部署在自有服务器上,查询 100 万条向量能在 200ms 内完成。但学习曲线陡峭,部署、调优都需要专业知识。
**我的建议:**
| 数据量 | 推荐方案 |
|--------|---------|
| < 1万 | Chroma(本地开发够用) |
| 1万-10万 | Pinecone(省心,花钱) |
| > 10万 | Milvus / Qdrant(自建,可控) |
别被那些"开箱即用"的教程忽悠,数据量一上来,问题全暴露。
Embedding 模型:不是越新越好
我试过 Text-embedding-3-small、BAAI/bge-large-zh、jina-embeddings-v3,各有各的坑。
**Text-embedding-3-small**:
**BAAI/bge-large-zh**:
**jina-embeddings-v3**:
**实战建议**:如果你的数据主要是中文技术文档,用 BAAI/bge-large-zh 或者 BAAI/bge-m3(支持多语言)。如果是中英文混排,jina-embeddings-v3 更稳。
检索策略:别只靠向量相似度
这是最容易踩的坑。很多教程教你的就是"把问题转成向量,然后查最相似的",但实际效果往往很一般。
为什么?因为**向量相似度只捕捉语义相似,不捕捉事实准确**。
比如你问:"Python 的 GIL 是什么?" 向量检索可能会返回"Python 多线程编程"或者"Python 性能优化"相关的内容,而不是直接告诉你 GIL 的定义。
**我的方案:混合检索**
def hybrid_search(query, top_k=10):
# 1. 向量检索
vector_results = vector_db.search(query, top_k=top_k * 2)
# 2. 关键词检索(BM25)
keyword_results = bm25_search(query, top_k=top_k)
# 3. 融合排序(RRF - Reciprocal Rank Fusion)
fused = rrf_fusion(vector_results, keyword_results, top_k=top_k)
return fused
def rrf_fusion(vec_results, kw_results, top_k=10, k=60):
"""RRF 融合排序"""
score_dict = {}
for i, doc in enumerate(vec_results):
rank = i + 1
score_dict[doc['id']] = score_dict.get(doc['id'], 0) + 1 / (k + rank)
for i, doc in enumerate(kw_results):
rank = i + 1
score_dict[doc['id']] = score_dict.get(doc['id'], 0) + 1 / (k + rank)
# 按融合得分排序
sorted_docs = sorted(score_dict.items(), key=lambda x: x[1], reverse=True)
return [doc_id for doc_id, _ in sorted_docs[:top_k]]
RRF 是一个简单但有效的融合策略,不需要调权重,直接按排名倒数求和。
重排序(Re-ranking):效果提升最明显的优化
做完混合检索之后,我加了一个重排序步骤,效果直接上了一个档次。
用的模型是 BAAI/bge-reranker-large,部署在本地 GPU 上。把混合检索的结果(比如 30 条)喂给它,重新排序,取前 5 条给 LLM。
**效果对比:**
| 策略 | 准确率(我测的) |
|------|----------------|
| 纯向量检索 | 62% |
| 混合检索 | 75% |
| 混合 + 重排序 | 89% |
代价?推理延迟增加约 300ms。但在 RAG 场景下,这 300ms 换来 14% 的准确率提升,值。
缓存:被忽视的性能优化
RAG 系统有一个大问题:同样的问题会被问很多次。用户问"怎么部署 Redis",可能问三遍,每遍都要走一遍检索+生成。
**我的方案:**
import hashlib
from redis import Redis
class RagCache:
def __init__(self):
self.redis = Redis.from_url('redis://localhost:6379')
def query_hash(self, query: str) -> str:
return hashlib.md5(query.encode()).hexdigest()
def get(self, query: str) -> str | None:
key = f"rag:cache:{self.query_hash(query)}"
return self.redis.get(key)
def set(self, query: str, response: str, ttl=3600):
key = f"rag:cache:{self.query_hash(query)}"
self.redis.set(key, response, ex=ttl)
同样的问题,1 秒内重复问,直接返回缓存结果。这套缓存让我 QPS 提升了 3 倍,延迟从平均 2 秒降到 600ms。
最后的忠告
RAG 不是什么黑科技,本质就是个"先查资料再回答"的流程。但要做好,需要:
2. **合适的向量库** —— 数据量和性能要平衡
3. **混合检索** —— 向量 + 关键词,别只靠一个
4. **重排序** —— 效果提升最明显的优化
5. **缓存** —— 别重复做功
别被那些"3 行代码搭 RAG"的教程骗了,真正用起来,每个环节都有坑。踩过去,才能写出好用的系统。
*作者:一个被 RAG 坑过多次的开发者。欢迎评论区交流踩坑经历。*