RAG 检索的三个隐藏坑
最近搞了个内部知识库问答系统,用的就是那套经典 RAG 架构:文档切片 → 向量化 → 检索 → 喂给 LLM 生成回答。
听起来很简单对吧?我一开始也这么想的。结果上线之后,用户反馈「回答经常跑偏」「有时候答非所问」,我一看日志,好家伙,检索出来的 chunks 跟用户问题根本不在一个频道上。
折腾了两周,总结出三个最容易被忽视的坑,分享给大家。
最直觉的做法就是按固定长度切片,比如每 500 字一段,overlap 50 字。我一开始就是这么干的。
问题是,知识库里的文档不是流水账,它们有结构——章节、段落、列表。你一刀切下去,很可能把一个完整的概念拆成了两半。
举个例子,文档里有一段讲「Redis 持久化的两种方式」:
Redis 持久化有两种方式:
- RDB:定时快照,恢复快,但可能丢数据
- AOF:追加日志,数据更安全,但恢复慢
固定切片可能把「选择建议」那句话切到下一个 chunk 里。用户问「Redis 该选哪种持久化」,检索到的 chunk 只有前两行,没有选择建议——LLM 只能瞎编或者含糊回答。
- 按语义边界切片,而不是固定长度。用段落、标题、列表项作为天然切分点。
- 如果必须固定长度,overlap 要足够大(建议 20%~30%),确保关键上下文不会断在两段之间。
- 更好的方案:用 LLM 做智能切片,让模型帮你识别语义完整的段落边界。
一个简单的语义切片思路
def semantic_chunk(doc: str, max_len=500, overlap_ratio=0.25):
paragraphs = doc.split('\n\n') # 段落作为天然边界
chunks = []
current = ''
for para in paragraphs:
if len(current) + len(para) > max_len and current:
chunks.append(current)
# overlap: 保留最后一段
current = current.split('\n\n')[-1] + '\n\n' + para
else:
current += '\n\n' + para
if current:
chunks.append(current)
return chunks
## 坑二:只靠向量相似度检索,精度不够
标准 RAG 流程里,检索就是拿用户 query 的向量去跟 chunk 向量做 cosine similarity,取 top-k。
听起来合理,但实际问题是:**用户的问题往往很短,而文档 chunk 很长,语义空间里的距离并不总是靠谱**。
比如用户问「怎么部署 Kubernetes」,你的知识库里有「K8s 集群搭建指南」和「容器编排原理概述」。前者才是用户想要的,但后者的向量可能跟「Kubernetes」这个词更近(因为全文在讲概念,词频更高)。
混合检索:向量检索 + 关键词检索(BM25),双路召回再合并排序。这是目前业界公认最稳的方案。
混合检索示意(伪代码)
results_vector = vector_store.search(query_embedding, top_k=10)
results_bm25 = bm25_index.search(query_keywords, top_k=10)
# 合并:简单加权或 RRF (Reciprocal Rank Fusion)
merged = reciprocal_rank_fusion([results_vector, results_bm25], k=60)
final = merged[:5] # 取最终 top-5
Query 改写**:在检索前,先让 LLM 把用户的简短问题扩展成一个更丰富的检索语句,增加命中概率。
比如:「怎么部署 K8s」 → 「Kubernetes 集群部署步骤、环境准备、kubectl 配置、节点管理」
这一步花钱不多,但检索质量提升很明显。
坑三:检索出来的内容没做精排,噪声拉低回答质量
Top-k 检索回来的 chunk 里,经常混着一些「有点关系但不直接相关」的内容。如果不做二次筛选,LLM 就会被这些噪声干扰,生成含糊甚至错误的回答。
我就遇到过:用户问「Python GIL 是什么」,检索 top-5 里居然混进去一段讲「Python 装饰器」的——因为那篇文章标题是「Python 性能相关的几个话题」,向量相似度 0.72,排到了第三。
LLM 看到这段装饰器的内容,回答里就扯上了装饰器,用户一脸懵。
怎么改?
加一轮精排(reranking):用 Cross-Encoder 或轻量级重排模型,对检索结果做 query-document 的精细匹配评分。
python
使用 sentence-transformers 的 CrossEncoder 做精排
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
scores = reranker.predict([(query, chunk) for chunk in retrieved_chunks])
按分数重排
ranked = sorted(zip(retrieved_chunks, scores), key=lambda x: x[1], reverse=True)
final_chunks = [c for c, s in ranked[:3]] # 只取最相关的 3 段
设阈值:不是所有 top-k 都值得喂给 LLM,相似度低于某个阈值就丢弃,宁可少给别给垃圾。
上下文压缩:对检索到的长 chunk,先让 LLM 提炼出跟问题直接相关的部分,再喂进生成环节,减少 token 浪费和噪声。
总结:RAG 不是拼积木
很多人以为 RAG = 切片 + 向量 + 检索 + 生成,拼起来就完事了。但实际上每一个环节都有细节陷阱,拼起来能跑 ≠ 拼起来能用。
我的经验浓缩成一句话:**检索质量决定了 RAG 的天花板,生成只是锦上添花**。
如果检索回来的内容就是歪的,再强的 LLM 也救不回来。所以别光盯着模型参数和 Prompt 调优,先回头看看你的检索链路——切片是不是太粗暴?检索是不是只靠向量?精排是不是没做?
这三步改完,你的 RAG 系统大概率会有肉眼可见的提升。

浙公网安备 33010602011771号