← 返回博客
·AI技术

RAG 向量检索调优:把准确率从 40% 拉到 78% 的路是怎么踩出来的

从 chunking 策略、embedding 模型选择、到重排序和缓存优化,分享一个生产级 RAG 系统的调参经验和踩坑记录。

#RAG#向量检索#LLM#Embedding#语义搜索

# RAG 向量检索调优:把准确率从 40% 拉到 78% 的路

先说好:这不是教程,是踩坑日记

上个月我们团队把一个企业知识库的 RAG 问答系统从 v1 干到 v3,recall 从 0.4 升到了 0.78,precision 从 0.31 升到了 0.65。过程中踩了至少 12 个大坑——包括把 embedding 维度从 1536 降到 768 导致召回率断崖式下跌这种蠢事。

先说结论:RAG 的调优没有银弹,只有层层叠加的微调。下面每一层都是真金白银买教训换来的。

Chunk 策略:别再用固定长度切分了

v1 版本我们用的是最简单的逻辑:每 500 token 切一刀。效果很差——因为文档的结构边界被暴力切断。比如一份合同,"第四条 违约责任"后面紧跟着具体条款,fixed cut 可能刚好在"违约"两个字中间劈开,语义完整性直接崩了。

后来改成**基于语义边界的 chunking**,核心思路是用 embedding 相似度检测段落边界:

from sentence_transformers import SentenceTransformer

import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')

def smart_chunk(text, sentences=None, similarity_threshold=0.85):

if not sentences:

import re

sentences = re.split(r'(?<=[。!?])', text)

sentences = [s for s in sentences if s.strip()]

chunks = []

current_chunk = []

current_emb = None

for sent in sentences:

emb = model.encode(sent).reshape(1, -1)

if current_emb is None:

current_chunk.append(sent)

current_emb = emb

continue

sim = np.dot(current_emb, emb.T)[0][0]

if sim < similarity_threshold:

chunks.append(''.join(current_chunk))

current_chunk = [sent]

current_emb = emb

else:

current_chunk.append(sent)

current_emb = (current_emb * len(current_chunk) + emb) / (len(current_chunk) + 1)

if current_chunk:

chunks.append(''.join(current_chunk))

return chunks

这段代码跑在我们 PDF 解析后的文本上,chunk 数减少了约 40%,但 recall 直接涨了 12 个百分点。代价是 chunking 的时间成本增加了——从 O(n) 变成了 O(n²) 的逐句比较。对于超大型文档,建议加一个并行预处理。

Embedding 模型的选择:越大越好?不一定。

一开始我们用的 openai/text-embedding-3-large(3072 维),效果好但贵。后来尝试了开源方案,发现 bge-m3 在中文场景下出奇地能打:

| 模型 | 中文 MTEB | API 成本/1K tokens | 推理延迟 |

|------|-----------|-------------------|----------|

| text-embedding-3-large | 68.3 | $0.00013 | ~200ms |

| bge-m3 | 72.1 | $0 (自部署) | ~150ms |

| jina-embeddings-v3 | 65.8 | $0.00001 | ~100ms |

| text-embedding-3-small | 61.2 | $0.00002 | ~80ms |

bge-m3 的 multilingual 能力确实强,而且支持 sparse + dense hybrid retrieval。我们后来切到 bge-m3 后,整体检索精度反而比 openai 还高 2-3 个点——主要归功于它在中文法律文档上的 fine-tune 数据质量高。

Hybrid Search:Dense + Sparse 是必须的

单靠 dense embedding 有个致命问题:专业术语和领域缩写搜不到。比如用户问"CQRS 模式怎么用在订单系统",dense embedding 可能会匹配到讲 "cqrs" 概念定义的文档,但匹配不到实际写 CQRS 实现方案的文档。

引入 BM25 做 sparse retrieval,然后把两者的结果用 **Reciprocal Rank Fusion (RRF)** 合并:

def rrf_fuse(dense_results, sparse_results, k=60):

rank_scores = {}

for i, (doc_id, _) in enumerate(dense_results):

rank_scores[doc_id] = rank_scores.get(doc_id, 0) + 1.0 / (k + i + 1)

for i, (doc_id, _) in enumerate(sparse_results):

rank_scores[doc_id] = rank_scores.get(doc_id, 0) + 1.0 / (k + i + 1)

return sorted(rank_scores.items(), key=lambda x: x[1], reverse=True)

这个方案的工程成本极低,但收益惊人——我们把 query 里的专业术语召回率提了 15 个百分点。关键参数是 k,默认 60 就行,别改太大。

Re-ranking:最后的临门一脚

检索回来 top-K 候选之后,用一个 cross-encoder 做二次排序。这一步虽然贵,但值得。

我们用的是 bge-reranker-v2-m3,把 top-20 向量检索结果重新打分,取前 5 个喂给 LLM。实测效果:

  • 没 rerank:llm 答错的概率约 35%
  • rerank after vector search:llm 答错率降到约 12%
  • from FlagEmbedding import FlagReranker

    reranker = FlagReranker('BAAI/bge-reranker-v2-m3', use_fp16=True)

    pairs = [[query, doc] for doc in candidate_docs[:20]]

    scores = reranker.compute_score(pairs, normalize=True)

    ranked_docs = sorted(zip(candidate_docs[:20], scores), key=lambda x: x[1], reverse=True)

    top_chunks = [d[0] for d in ranked_docs[:5]]

    注意 use_fp16=True——这能让推理速度提升约 40%,显存占用减半。我们用 A10G 就能扛住 200 QPS 的 reranking 请求。

    缓存层:别让每个请求都跑一遍 pipeline

    RAG 最怕的就是重复查询。用户问"密码重置的流程是什么"和"忘记怎么改密码怎么办",本质上是同一个知识块。我们在 retrieval 之后加了一层 **semantic cache**:

    cache = {}

    cache_ttl = 3600

    def cached_rag(query, top_k=5):

    q_emb = model.encode(query).tobytes()

    if q_emb in cache:

    return cache[q_emb]['result']

    result = perform_full_rag_pipeline(query, top_k)

    cache[q_emb] = {'result': result, 'timestamp': time.time()}

    return result

    加上缓存后,我们 30% 的请求是直接命中缓存的——这些请求的 P99 延迟从 2.3s 降到了 45ms。当然缓存也有风险:如果知识库更新了,旧缓存会返回过期结果。所以我们加了个版本号,每次文档库有变更就 invalidate 全部缓存。

    经验教训总结

  • **chunking 是 RAG 的地基**——地基打歪了,后面所有优化都是在沙堆上盖楼
  • 2. **Hybrid Search 是最便宜的增益**,几乎零成本换 10-15% 的提升

    3. **Re-ranking 别省**,它是最后的质量守门员

    4. **缓存要带 TTL 和版本号**,否则你会收到一堆过时答案

    5. **永远用真实用户的 query 做评估**,不要用 synthetic data 骗自己

    RAG 调优是一个持续的过程,没有"完成了"这一天。每次知识库更新、每次 query 分布变化,都可能需要重新评估 chunk 策略和模型选择。记住:你优化的不是系统,而是用户对系统的体感。