RAG 系统踩坑全景:从文档切分到检索排序
RAG 系统踩坑全景:从文档切分到检索排序
基础设施不需要漂亮话。
RAG(Retrieval-Augmented Generation)看起来简单:把文档切分、向量化、检索、喂给大模型。上线三个月后,你会发现每个环节都有坑。我们团队做了两个 RAG 项目,第一个上线后检索准确率只有 42%,第二个花了两个月重构才做到 78%。这篇文章把从文档切分到检索排序的踩坑经验全部列出来,每个坑都附带了原因分析和修复方案。
一、背景:RAG 系统为什么容易踩坑
RAG 的核心链路是:文档摄入 → 切分 → Embedding → 索引构建 → 检索 → 排序 → Prompt 组装 → LLM 生成。这条链路看起来线性清晰,但每个节点都有隐藏的失败模式。切分策略决定了检索粒度,Embedding 模型决定了语义空间质量,检索和排序决定了最终回答的素材质量。一个环节出问题,后面的环节再怎么优化也补不回来。
二、文档切分:四个最常见的坑
坑1:固定长度切分丢失语义边界
最直觉的做法是按 512 token 或 1000 字符切分。问题在于:一个完整的段落可能被切断,后半段缺少上下文变成无法理解的信息碎片。我们实测发现,固定长度切分的检索命中率比语义边界切分低 23%。
修复方案:优先按段落和章节切分,然后对超长段落做二次切分,保留 overlap 区域(通常 10%~20%)。overlap 不是万能的——它增加存储成本和检索噪声,需要根据文档类型调整比例。
# 按语义边界切分,超长段落二次切分带overlap def semantic_chunk(doc, max_tokens=512, overlap_ratio=0.15): paragraphs = doc.split('\n\n') chunks = [] for para in paragraphs: if token_count(para) <= max_tokens: chunks.append(para) else: # 二次切分:按句子边界,带overlap sentences = para.split('。') sub_chunks = sliding_window(sentences, max_tokens, overlap_ratio) chunks.extend(sub_chunks) return chunks坑2:表格和代码被切碎
表格的每一行单独切分后失去结构信息,代码块被切断后变量引用丢失。这两个场景在技术文档中极其常见。
修复方案:对表格和代码块做特殊标记,切分时将其作为不可分割单元整体保留。如果表格过长超过限制,按行组切分但保留表头作为每个子块的元信息。
坑3:切分粒度一刀切
不同类型的文档需要不同粒度:FAQ 适合按问答对切分,技术手册适合按章节切分,日志类文档适合按事件切分。用同一套切分策略处理所有文档类型,效果必然打折。
修复方案:按文档类型配置不同的切分策略,切分策略的参数(长度、overlap、边界规则)要做 A/B 测试对比检索命中率。
坑4:元信息没有随切分保留
文档的来源、版本、时间戳、章节层级这些元信息在切分后经常丢失。检索时拿到了内容,却不知道它来自哪个版本的文档,甚至不知道它的上下文是什么。
修复方案:每个 chunk 必须携带元信息字典,至少包含 source、version、section_title、chunk_index。这些信息在排序和 Prompt 组装时都要用到。
三、Embedding 和索引:三个关键坑
坁5:中文 Embedding 模型选错
很多人直接用多语言 Embedding 模型(如 multilingual-e5-large)处理中文文档,认为"多语言"就够用。实测发现,在中文场景下,专用中文模型(如 bge-large-zh)的检索准确率比多语言模型高 15%~20%。
修复方案:中文为主的应用优先选中文专用模型;混合语言场景用多语言模型,但检索时需要加语言过滤逻辑。模型选型要在真实数据集上评测,不要只看排行榜。
坁6:混合语言文档向量漂移
一篇文档同时包含中文和英文(比如技术文档),Embedding 向量会在两种语言的语义空间之间漂移。检索时用中文 query,可能召回英文 chunk;用英文 query,可能召回中文 chunk。
修复方案:对混合语言文档做语言检测,不同语言段落用对应模型生成向量,检索时匹配语言标签。或者统一用一个多语言模型,但在检索结果中加语言一致性权重。
坁7:增量索引没有重建
新文档加入后直接追加向量,不重建索引。这会导致索引结构退化——HNSW 图的连通性变差,检索路径变长,延迟上升。我们观察到索引追加 30% 数据后,检索延迟增加 40%。
修复方案:设定重建阈值——当新增向量超过总量的 15%~20% 时触发全量重建。重建期间用双索引策略:旧索引继续服务,新索引构建完成后一次性切换。
四、检索和排序:四个最致命的坑
坁8:纯向量检索忽略关键词匹配
向量检索擅长语义相似,但处理精确匹配(产品型号、错误码、版本号)的能力很差。用户问 "K8s 1.28 的 CVE-2023-xxxx 怎么处理",纯向量检索可能召回一堆 K8s 安全相关的通用文档,而不是精确命中那个 CVE。
修复方案:采用混合检索——向量检索负责语义匹配,关键词检索(BM25)负责精确匹配,两路结果融合排序。融合权重需要根据 query 类型动态调整:精确查询关键词权重高,语义查询向量权重高。
坁9:Top-K 固定不动态调整
不管 query 是简单还是复杂,一律取 Top-5 或 Top-10。简单问题("公司电话是多少")Top-3 就够了,多取的只会引入噪声;复杂问题("部署流程和回滚方案是什么")可能需要 Top-20 才能覆盖完整信息。
修复方案:根据 query 复杂度动态调整 Top-K。简单判断标准:query 长度和包含的实体数量。或者用两阶段检索:先取 Top-50,再用重排序模型筛选 Top-K。
坁10:没有重排序环节
向量检索的相似度分数和 BM25 的分数不在同一个尺度,直接融合效果不稳定。更关键的是,向量检索的排名顺序和最终回答需要的质量排序经常不一致——语义最相似的文档不一定是最有用的文档。
修复方案:加重排序模型(如 bge-reranker-v2-m3 或 Cohere Rerank)。重排序模型用交叉编码器对 query-document 对做精细评分,比双塔模型的向量点积准确率高 10%~15%。代价是延迟增加(每对 query-document 需要单独推理),需要控制候选数量。
坁11:相似度阈值一刀切
设一个固定阈值(如 cosine similarity > 0.7)过滤检索结果。问题是不同领域、不同 query 类型的合理阈值差别很大:技术文档的阈值可能偏高(术语精确匹配),政策文档的阈值可能偏低(表述模糊但语义相关)。
修复方案:不要设全局阈值。改用百分位阈值——取当前 query 的检索结果中相似度分布的 Top 百分位,或者用重排序分数的自然断点做过滤。
五、避坑全景图和总结
| 坑编号 | 坑描述 | 影响程度 | 修复优先级 |
|---|---|---|---|
| 8 | 纯向量检索忽略关键词 | 高 | P0 |
| 10 | 没有重排序环节 | 高 | P0 |
| 1 | 固定长度切分丢失语义 | 高 | P0 |
| 5 | 中文Embedding选错 | 中 | P1 |
| 9 | Top-K固定不调整 | 中 | P1 |
| 11 | 阈值一刀切 | 中 | P1 |
| 3 | 切分粒度一刀切 | 中 | P2 |
| 7 | 增量索引不重建 | 中 | P2 |
| 2 | 表格代码被切碎 | 低 | P2 |
| 4 | 元信息未保留 | 低 | P2 |
| 6 | 混合语言向量漂移 | 低 | P3 |
RAG 系统踩坑的核心规律:链路越长,每个环节的小问题会在下游被放大。切分阶段丢掉的语义信息,检索阶段不可能补回来;检索阶段召回的噪声,排序阶段只能部分过滤。优化顺序应该是:先修切分(数据质量是根基),再修检索策略(召回率是前提),最后修排序(精准度是锦上添花)。不要在切分还是固定长度的时候就急着调 Embedding 模型——那是在错误的地基上建高楼。