RAG 落地踩过的坑,比你想象的深
从 chunk 策略到向量检索,生产环境 RAG 系统踩坑实录,附解决方案。
# RAG 落地踩过的坑,比你想象的深
背景
我们公司的产品有个"智能问答"功能,本质就是个 RAG 系统:用户提问 → 检索相关文档 → 喂给 LLM 生成回答。
看起来很简单对吧?三个步骤,每个都有现成方案。我们花了三个月才把准确率从 35% 提到 72%。以下是踩过的坑,按严重程度排序。
坑一:Chunk 策略选错了
最初我们用的策略是"按段落切分,每段 500 字"。这在技术博客场景下还行,但我们的产品文档有大量表格、代码块、层级标题。按段落切分后,一个完整的"配置说明"可能被切成三段,语义断裂。
换了策略:先按 Markdown 标题分层,每层内再按语义完整性切分,确保每个 chunk 包含一个完整的信息单元。chunk 大小控制在 300-600 tokens,用 overlap=50 防止边界信息丢失。
效果:检索准确率从 41% 提到 58%。
坑二:向量检索不够用
纯向量检索有个问题:它对关键词匹配不敏感。用户搜"如何重置密码",但文档里写的是"忘记密码后的恢复流程",语义相近但关键词不重叠,向量相似度可能很低。
解决方案:混合检索。向量检索 + BM25 关键词检索,然后用 Reciprocal Rank Fusion 合并排序。
// 简化的 RRF 合并逻辑
function rrfRank(vectorResults, keywordResults, k = 60) {
const scores = {};
vectorResults.forEach((doc, i) => {
scores[doc.id] = (scores[doc.id] || 0) + 1 / (k + i + 1);
});
keywordResults.forEach((doc, i) => {
scores[doc.id] = (scores[doc.id] || 0) + 1 / (k + i + 1);
});
return Object.entries(scores)
.sort((a, b) => b[1] - a[1])
.map(([id]) => id);
}
效果:关键词敏感查询的准确率从 33% 提到 61%。
坑三:没做检索后的重排序
向量检索返回的前 K 个结果,顺序不一定最优。我们加了个 cross-encoder 重排序模型(bge-reranker-large),把 top-20 的候选 re-rank 到 top-5。
这个步骤增加了约 200ms 延迟,但回答质量明显提升——LLM 看到的上下文更精准,生成的回答相关性和幻觉率都改善了。
坑四:Prompt 里没给检索来源的元数据
最初我们直接把检索到的文本塞进 prompt:
基于以下文档内容回答问题:{retrieved_text}
问题:{user_question}
问题是,LLM 不知道这些文本的标题、作者、更新时间。用户问"最新的 API 版本是什么",检索结果里可能混入了旧版文档,LLM 无法判断哪个是最新的。
改后的 prompt 加入了元数据:
以下是从文档库中检索到的相关内容(按相关性排序):
{content}
请基于以上内容回答问题,优先引用更新、更权威来源的信息。
问题:{user_question}
效果:时效性相关问题的准确率提升明显。
坑五:没处理"我不知道"的情况
最严重的一个 bug:当检索结果完全不相关时,LLM 仍然会"自信地"编造答案。这是 RAG 系统最典型的问题——hallucination。
解决方案:加一个相关性阈值判断。如果最佳匹配的向量相似度低于 0.7(我们调过的值),直接返回"未在文档中找到相关内容",而不是让 LLM 瞎编。
const SIMILARITY_THRESHOLD = 0.7;
if (topResult.score < SIMILARITY_THRESHOLD) {
return '抱歉,在现有文档中未找到相关说明。';
}
效果:幻觉回答率从约 40% 降到 8%。
踩坑总结
| 问题 | 解决方案 | 效果提升 |
|------|----------|----------|
| Chunk 策略不当 | 按语义单元切分 + overlap | +17% |
| 纯向量检索 | 混合检索 + RRF | +28% |
| 排序不精确 | Cross-encoder 重排序 | +11% |
| 缺少元数据 | Prompt 加入来源信息 | +6% |
| 幻觉问题 | 相似度阈值兜底 | 幻觉率 -32% |
三个月,五个坑,每个都不大但叠加起来杀伤力惊人。RAG 看起来是"套娃",实际上调参的空间比想象中大多了。