
RAG 确实烂大街了。从各大技术社区到培训机构只要是和 LLM 沾边的分享几乎都在讲同一套东西加载文档、切分、向量化、存向量库、检索、拼 Prompt、灌给大模型。我甚至见过把这段流程包装成“企业级智能问答系统”的合集课程封面写着“一天搭建 RAG 知识库零基础也能学会”。这套流程本身没有错它就是 RAG 的流水线问题在于流水线只是入场券真到了生产环境决定一个 RAG 项目生死存亡的是流水线上那些被一笔带过的细节。因为工作关系我最近一年多密集接触过合同问答、产品手册问答、政策咨询等多个 RAG 落地项目也踩过不少坑。如果非要把经验浓缩成一句话那就是RAG 烂大街不假但烂掉的是同质化的教学 Demo真正拉开项目效果差距的从来不是框架和流程而是以下这六处细节——文档解析、分块策略、嵌入模型选型、混合召回、重排、评估闭环。这篇文章就把这六处挨个说透每一处都有我踩过的坑也有可以直接抄走的作业。1. 文档清洗与解析RAG 的第一个分水岭很多人做 RAG 的第一步是把 PDF 直接丢给 LangChain。这个动作放在 Demo 里没问题放在真实业务里就是灾难。一份真实的 PDF 可能是扫描件、可能是双栏排版、可能带页眉页脚、可能含表格和图片甚至可能某些页面是图片整体导入的。如果你不先处理这些原始文档后续分块、向量化、召回全都会被污染。1.1 “喂进去就能答”是最大的误区我记得有一个合同审查项目一开始用 PyMuPDF 直接抽文本效果还行但用户很快反馈很多答案背不到原文引用出处都是一句“保密协议”。排查了很久才发现合同每一页页眉都印着“保密协议”四个字这些文本被切进了上下文检索时频繁命中页眉导致大模型以为找到了答案。后来我们把页眉区域、页脚区域、页码这些噪声先剪掉检索命中率立刻提了一截。这种问题教科书里不会讲但几乎每个真实文档集都会遇到。另外还有一个经常被问到的热词问题“RAG 知识库能存图片吗”我的回答是向量库当然能存图片向量但纯视觉向量很难支撑文本形式的问答。多数落地知识库的核心资产是文字信息图片如果不能被解析成文字或结构化描述那它在 RAG 里的价值就非常有限。正确的做法是走多模态解析把图片交给视觉模型或 OCR 识别抽成文字摘要或结构化数据再和图片路径一起存入 chunk 的 metadata。如果整套链路没有多模态能力至少要做到“以图转文”否则存向量只是自嗨。1.2 一个让准确率明显提升的文档预处理流程我从一个踩过多次坑的项目里总结了一套流程可以拿来直接用格式识别先看文件能否直接抽出文本。用 pdfplumber 或 PyMuPDF 尝试提取如果抽出来的是乱码或空文本判定为扫描版走 OCR。OCR 处理推荐 PaddleOCR 或 Tesseract。中文扫描件用 PaddleOCR 更稳缺点是慢所以只对无文本层的文档启用 OCR。版面分析把标题层级、段落边界、表格区域、图片位置识别出来。这里可以用开源工具做版面分析也可以用视觉大模型辅助。噪声清理删除页眉、页脚、页码、水印、重复分隔线等无用内容。结构规整把清洗后的内容转成规范的 Markdown 或 JSON 结构保留标题层级方便下一层分块。这套流程看着简单但每步都有门道。比如 OCR 后经常会多出来一些乱码字符要配合规则清洗再比如表格如果被 OCR 成一行行散文本检索时根本没法回答“第三列平均值是多少”这类问题。所以我习惯对表格做单独处理用表格结构识别工具把表格转成 Markdown 表格让字段和值的关系保留下来。能做到这一步RAG 的第一处分水岭就算迈过去了。注意不要盲目对每份文档都跑 OCR。OCR 速度慢、噪声多而且会引入额外错误。先检测文本层发现没有文本层再 OCR可以节省大量时间准确率也更可控。2. 分块策略同一个文档有人切出金子有人切出垃圾分块是 RAG 流水线上最容易被一带而过的环节。很多教程会直接告诉你“用固定长度切每块 500 个字符重叠 50 个字符”然后就没有然后了。这种固定长度切分在内容结构复杂的真实文档上会产生大量语义断裂的残片之后的检索和生成都会跟着遭殃。2.1 固定 500 字的分块害了多少 RAG为什么固定分块不好本质原因很简单语义是有边界的字符数没有。一个 800 字的段落中间有一个转折你把它切成两半前半段讲背景后半段讲结论用户问“结论是什么”能召回后半段倒还好但如果结论里省略了背景信息只凭半段就很难形成准确答案。更常见的是表格被人为切开、代码块被拦腰截断、列表项四分五裂。这种问题不是靠调大 chunk size 就能解决的。我一直强调一个观点没有“最优 chunk size”只有“适配文档结构的分块边界”。你要做的不是选择一个数字而是先理解你的文档单元是什么。合同文档的单元是条款和子条款产品手册的单元是功能模块和章节博客文章的单元是标题下的段落组。以单元为边界才能让每个 chunk 成为可以被独立阅读和理解的内容块。2.2 按文档结构语义分块才是可复用的方案我现在最常用的分块方案可以概括为“结构树优先长度兜底”。具体分三步走第一步基于前面解析出的标题层级构建一棵文档结构树。每个标题节点后面跟着的正文都归属到对应节点下。 第二步从根节点向下遍历把每个节点下的内容作为候选 chunk。如果某个候选内容太长比如超过模型 token 上限就递归切分切分时优先选择段落边界、句子边界而不是在字中间硬切。 第三步给每个 chunk 打上丰富的 metadata文档名、一级标题、二级标题、页码、段落序号。这些 metadata 不只是给人看的留着后面组装 Prompt 和生成引用时会非常有用。做的时候可以用 LangChain 的 RecursiveCharacterTextSplitter但要把 separators 按语言和文档类型定制。对于中文文档我的 separators 通常是这样设计的separators [\n\n, \n, 。, , , , , , ]意思是优先在段落间切其次是句子结尾再次是标点最后才是空格实在不行才按字符。这样能最大程度保留语义完整性。分完块之后如果你发现某些短段落单独成块太单薄可以把它和父标题合并成一个 chunk比如“服务条款”这个标题下只有一句话那就变成“服务条款公司对用户数据安全负责”检索时也更容易命中。顺带说一下“ontology RAG”。如果你们的领域知识有很强的实体关系比如病历里的诊断、症状、用药之间的关系或者制造领域设备、参数、故障之间的关系可以引入轻量级本体知识来辅助分块和检索。你不用搞一个大而全的知识图谱只要在 metadata 里维护实体关系回答跨文档问题时就能通过关系找到关联 chunk这也是一种低成本升级方式。2.3 表格、图片和代码块怎么切除了正文段落真实文档里还有三类特殊内容需要单独处理。表格千万不要切开。一个 20 行的表格如果被切进三个 chunk每个 chunk 都只有残列大模型很难拼出原表。要么整表作为一个 chunk要么把它转成 Markdown 后作为一个单元存放。图片的常见做法是把图片说明文字和图片路径绑进同一个 chunk如果已经有视觉模型解析结果就把解析文本一并放进去保证图文信息不分离。代码块则按函数、类或者独立补丁块切分不要按行数硬切否则每个代码块都缺上下文回答时经常出现变量未定义的问题。世界上的文档类型很多所以没有万能分块规则。但核心逻辑是固定的先解析结构再按语义边界分块最后用长度兜底。存疑时宁可用标题重复拼接也不要把一个语义单元拆散。3. 嵌入模型选型与向量化别让向量化成为隐形的天花板分块之后文本会被向量化。这一步看似简单——调一个 embedding 接口而已但实际上embedding 模型的选择直接影响召回效果。很多团队用 OpenAI Embedding 用惯了一换中文文档就感觉不对劲也有人在 BGE 和 M3 之间反复横跳最后也没搞明白差别在哪。3.1 别被 embedding 排行榜带偏排行榜上的分数高不代表你的业务场景一定好用。原因很简单通用排行榜测的是语义相关度而你的场景可能包含大量专有名词、缩写、编号这些内容在向量空间里可能是稀疏的。比如“查询订单 RT-2025-001 的状态”通用 embedding 模型对“RT-2025-001”这种序列号并不敏感你换更贵的模型也未必能解决因为问题不在“模型不够强”而在“这类精确信息需要精确匹配”。所以在选 embedding 时我会先跑一个小实验准备 20 条具有代表性的问题从知识库里找标准答案做一次 Top-5 召回测试对比三到四个候选模型。用这个测试结果作为选型依据而不是只看模型分数。尤其是中文场景我建议试一下 BGE 系列、GTE 系列以及本地 ollama 能跑通的轻量级模型。条件允许时本地模型 量化版本和云端模型再各跑一轮看看精度损失能不能接受。3.2 本地还是云端我的选择逻辑数据敏感型项目我基本都会选择本地部署 embedding。本地跑用 ollama 其实很方便下载一个嵌入模型就能作为服务使用比如ollama pull bge-m3 curl http://localhost:11434/api/embed -d {model: bge-m3, input: 查询订单状态}ollama 之后把这个地址接到 LangChain/LangChain4j 的 embedding 配置里就行。需要注意的是本地模型对资源占用有个底线BGE-M3 这种中大规模模型在 CPU 上推理会很慢建议用 GPU 或者调小序列长度。这里我踩过一个坑默认 max length 很大导致向量化耗时爆炸后来根据文档长度把 max length 限制在 512 甚至 256速度立刻上来了效果损失却很有限。云端模型的优势是生态完善、效果稳定、不用担心中间件。在非敏感场景直接用云 API 是性价比最高的选择。但不管用哪种都建议把 embedding 层单独封装方便后续切换。因为当你发现换一个 embedding 模型能让整个项目效果提升 15% 时如果之前代码里 embedding 接口到处都是切换成本会让你崩溃。3.3 “RAG 知识库能存图片吗”真正答案我们再回到那个热搜问题RAG 知识库能不能存图片技术上说可以。你可以用多模态 embedding 模型把图片编码成向量存进向量库然后用文本查询去检索图片。但实际落地时除非你要做的是“以图搜图”或“图文问答”并且整套链路都支持多模态否则不建议直接把图片当一等公民存向量。绝大多数企业的知识管理需求是“从资料里查答案”答案通常以文字表达图片只作为证据或附注存在。所以我的通用建议是文案为主、图片为辅。图片经过视觉解析后把识别文本、图片路径、位置信息放进 metadata既不影响检索主流程又能在答案展示时调出原图。这个思路看起来保守但在十几个项目里都验证过知识库能存图片吗当然能但真正有效的存法是让图片“闭嘴说话”而不是把一张图孤零零地丢进向量空间。4. 混合检索与召回策略向量检索不是万能的到了检索这一步分水岭就更明显了。很多人跑通 Demo 之后发现怎么换模型、怎么调参数答案还是不准这时候大概率不是生成问题而是召回问题。RAG 里最常被讨论的“瓶颈”往往就是从“检索不到”开始的。4.1 我见过的高频 RAG 瓶颈多数死在召回纯向量检索的本质是语义匹配它对“意思相近但表述不同”的文本很有效但对“精确匹配”极度不友好。你问“服务条款第三条写的是什么”向量检索可能召回一堆和“服务条款”相关的 chunk但不包含“第三条”这个编号你问“RT-2025-001 订单”向量检索可能召回一堆“订单管理”的说明却漏掉了唯一的订单号。这种问题不是加训练数据能解决的因为你没法把每个编号、每个缩写都训练进模型。更麻烦的是用户提问往往很短“退换货规则是什么”这句话放到向量空间里和“规则”“退换”“是什么”都相关召回结果自然发散。如果只有一路向量召回你就是在赌 embedding 模型恰好能理解这个短句的真实意图这个赌注风险太高。最好的办法是引入第二路检索BM25 全文检索。BM25 是经典的稀疏检索算法擅长精确关键词匹配能快速锁定包含“条款三”“RT-2025-001”“退换货”这些字眼的文档。把 BM25 结果和向量结果合并就构成了“混合检索”这也是目前工业界应对 RAG 召回问题最实在的方法没有之一。如果项目用的是 Elasticsearch、OpenSearch或者向量库本身支持稀疏向量都可以同时挂上实现成本并不高。4.2 实操两路召回、并发查询与结果合并混合检索的落地流程不复杂核心是“两路召回归一化”。我先在向量库查 Top-50 个相似 chunk再用全文检索引擎查 Top-50 个关键词命中 chunk最后把两批结果合并。合并时不要用简单的加权平均因为两路结果的分数分布不同权重很难调。业界最常用的方法是 RRFReciprocal Rank Fusion公式是score Σ 1/(k rank)其中 rank 是该文档在单路召回中的名次k 取经验值 60。这个公式的意思是一个文档在向量召回中排第 2在 BM25 排第 5那它的分就是 1/(602) 1/(605)。RRF 的优势是只依赖排名不依赖分数的绝对值稳健得多。在代码里我用 Python 做的大致逻辑是这样def rrf_fusion(dense_hits, sparse_hits, k60): scores {} for rank, doc_id in enumerate(dense_hits): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(sparse_hits): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)除了混合检索还可以在召回层加“多路改写”。用户问题简短时先用一个小模型把问题重写成一个更适合检索的查询比如把“你们退货怎么弄”改写成“退货流程 退货条件 退货申请”再用改写后的查询同时发往两路召回效果会有肉眼可见的提升。这个方案很便宜值得优先做。4.3 用户问题重写比换模型更划算在真实知识库里用户问题往往不是干净的搜索式表达而是口语化短句。比如“我想知道工资卡变更要怎么搞”直接拿去检索命中率很低。所以我习惯在 RAG pipeline 的最前面加一个 query rewrite 模块把口语问句标准化成检索式。它可以用 LLM 做也可以用专门的 rewrite 模型成本都不高。我试过最轻量的做法是在系统 Prompt 里要求 LLM 输出三个改写候选再让三段候选分别去检索最后合并去重。这么做对多跳问答特别有帮助。比如用户问“今年版本相比去年功能增强在哪”一次检索可能只找到“今年版本说明”找不到“去年版本说明”如果把问题重写成“今年版本新增功能”“去年版本功能列表”两个子查询就能同时召回两年资料。这一招在复杂问答场景里效果很明显简单说就是“一次检索不行就用多次检索覆盖”。5. 重排与精排把 Top-50 变成真正有用的 Top-4有了混合召回之后你会得到一堆候选 chunk但模型上下文窗口有限不可能全部塞进 LLM。这时候就需要重排rerank。很多项目直接把向量召回的 Top-4 喂给大模型这是非常偷懒的做法效果也很不稳定。5.1 为什么不能把向量检索的 Top-K 直接喂给 LLM向量检索和重排模型是两类不同的算法。召回阶段使用的通常是双塔模型也叫 bi-encoder它把 query 和 document 分别编码成向量然后算余弦相似度。这个过程本质牺牲了精读能力无法让文档和 query 充分交互。而重排模型用的是 cross-encoder把 query 和 document 拼在一起进入模型计算它们之间细粒度的匹配程度。一个字一个字地交叉关注相关性判断远优于双塔代价是计算开销大。所以工业界标准做法是“粗召回 精重排”先用轻量模型从整个知识库里捞出 50 条候选再用重排模型把 50 条按真实相关性重新排序最后挑前 4 条进入 Prompt。这样既保证了召回覆盖面又提升了最终给模型的内容质量。国内可以直接用 BGE-Reranker 系列效果不错部署也不复杂。我用 Transformers 跑重排的代码大致长这样from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch model_name BAAI/bge-reranker-base tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name) def rerank(query, docs): pairs [[query, doc] for doc in docs] inputs tokenizer(pairs, paddingTrue, truncationTrue, return_tensorspt, max_length512) with torch.no_grad(): scores model(**inputs).logits.view(-1).tolist() return sorted(zip(docs, scores), keylambda x: x[1], reverseTrue)注意一点如果你用的是英文重排模型去处理中文文档效果会差很多。中文知识库请一定选在中文语料上微调过的模型。重排阶段不需要追求超大模型base 版已经能在绝大多数场景满足需求large 版资源占用大不少但提升有限建议先用 base 跑一轮看结果。5.2 重排之后Prompt 组装也有讲究重排完成之后下一步不是直接丢进 Prompt而是要做两件小事。第一是去重。混合召回的两路结果以及多次查询的结果经常会有高度重叠的 chunk。如果不去重LLM 会被一段重复信息刷屏不仅浪费 token还容易把问题带偏。第二是顺序调整。LLM 对上下文尾部的内容更敏感我会把重排后最相关的 chunk 放在 Prompt 靠后的位置让模型在生成答案时更容易把注意力放在关键证据上。此外要在每个 chunk 前面拼上它的 metadata。比如[来源服务条款_第三章_第2节] [页码第12页]这样组合出来的 Prompt模型不仅能给出答案还能在生成时引用来源。很多 RAG 项目被吐槽“答案没有依据”其实不是生成模型不行而是你在组装 Prompt 时把证据链丢了。重排不是终点组装才是。这个细节做好上线后的用户信任度会高很多。6. 评估与优化闭环RAG 项目的最后分水岭前面五处都能做好的团队已经能跑出一个不错的系统了。但真正决定一个 RAG 项目能否持续迭代、长期可用的是第六处评估与优化闭环。没有评估的 RAG 项目就像在暗房里拍照全靠手感你永远不知道是哪一步出了问题。6.1 没有评测集的 RAG都是在碰运气我在项目初期一定会构建一个“最小可用的评测集”。具体做法是从真实用户提问里收集 50 到 100 条问题然后把标准答案和答案对应的 source chunk 都标注出来。如果完全没有人工标注资源也可以先用 LLM 根据文档自动生成问答对再人工抽检修正。自动生成有风险但作为冷启动足够了。评测集的数据结构我一般是这样设计的[ { question: 服务条款第三条是什么, answer: 服务条款第三条说明用户数据不会被出售给第三方。, source_chunks: [合同_服务条款_第3条_原文片段] }, { question: 订单 RT-2025-001 的状态是什么, answer: 已发货物流编号待更新。, source_chunks: [订单表_RT-2025-001] } ]这个评测集的价值在于每次调整解析、分块、召回、重排都可以跑一遍对比召回命中率和答案质量而不是靠一两个案例“感觉变好了”。RAG 系统调试过程最大的坑就是某个案例变好了、另一个案例变坏了你却完全察觉不到。评测集就是照妖镜。6.2 指标怎么定从 RAGAS 到业务可感评估 RAG 有现成的框架和指标比如 RAGAS 里的忠实度、答案相关性、上下文相关性。这些指标适合做自动回归测试但它们的实现普遍依赖 LLM 打分往往不够稳定只能看趋势不能当成绝对答案。所以我还建议定义几个业务可见的指标。第一是检索命中率RecallK评测问题中正确答案对应的 chunk 是否出现在重排后的 Top-4 里。这个指标最硬如果低于 80%后面怎么调 Prompt 都没用。第二是引用可溯源率生成的答案是否真的引用了知识库中的 chunk而不是模型脑补。第三是用户侧指标比如“答案有帮助”的点击率、二次追问率。这些业务指标短期看不出来但长期最值得看。建立一个简单的回归脚本每次改动后自动跑一遍评测集输出 Recall4、忠实度、答案相关性的变化。相信我这一套流程做下来你在项目汇报时会比绝大多数团队更有底气。6.3 调优优先级先召回、再重排、最后才是 Prompt很多人在 RAG 效果不好时第一反应是调 Prompt第二反应是换更大模型。但实际上大多数 RAG 问题出现在更靠前的位置。我排查问题的固定顺序是先看召回结果如果正确 chunk 没被召回问题多半在文档解析、分块策略、embedding 选型或检索方式再看重排结果如果正确 chunk 召回了但被排到后面问题在重排模型或排序阈值最后才看 Prompt 和生成策略如果证据都在但答案依然跑偏那才是生成环节的问题。这里整理一个速查表拿去就能用现象优先排查方向常见原因答案经常找不到依据召回环节文档没解析好、分块切碎了语义检索结果包含大量无关内容分块和召回策略chunk 粒度太大、没做混合检索正确内容召回了但排不进 Top-4重排环节重排模型选错或阈值过高证据有但答案乱编Prompt 组装chunk 顺序不对、metadata 丢失换一个用户问题效果波动大评测集没有做系统回归全在碰运气最后再提一句“RAG 智能体”。如果你的业务需要多轮追问、多跳检索比如用户问“这个产品 A 版本的缺货原因和 B 版本的补偿方案是什么”一次检索不够需要模型自己决定先查库存再查补偿策略这时候可以在前面六处的基础上为 RAG 套一层 Agent 编排。让模型自主规划检索路径而不是每次都死板地做一次单轮检索。这个方向是 RAG 的延伸但地基依然是上面这六处地基不稳Agent 只会放大错误。我个人在实际项目里最大的体会是RAG 没有银弹。那些表现稳定的项目并不是用了多前沿的模型而是把文档解析、分块、检索、重排、评估这条链路中的每一个环节都当成工程问题来对待。尤其是第一处和第六处一个在最前面决定了数据进场的质量一个在最后面决定了你是否有能力持续改进。这两处最容易被人忽略也恰恰是最能拉开项目水平的分水岭。如果你正准备做一个新的 RAG 知识库我的建议是别急着写代码先把文档解析方案定了把评测集搭起来再从流水线上一步步填细节这样踩坑的代价会小很多。