RAG 烂大街了。这是我这半年在技术群和面试场合听到最多的一句话。说这话的人,手里多半有个用 LangChain 三行代码搭出来的知识库问答 demo——读文件、切文本、跑 embedding、塞进向量库、检索、拼 prompt、丢给 LLM,一套下来连半小时都用不了。确实,这套流水线已经普及到不能再普及,网上随便翻一份“RAG 教程”,80% 都是同一个套路,连 chunk_size 都是抄来抄去的那几个。
但我一直觉得,烂大街的只是那条流水线,不是 RAG 本身。恰恰相反,真正把 RAG 从 demo 推到生产环境的人,从来不在“装库跑通”这个层面较劲,他们的精力几乎全花在六个地方。这六处才是 RAG 工程真正的分水岭,也是我最近一年陆续接手、复盘了几个知识库项目之后最想分享的东西。
如果你正在搭 rag知识库,如果你觉得 rag实战 里“装上就跑”和“跑起来真能用”之间差了不止一个量级,或者你正被知识割裂、rag hit rate 上不去这类问题卡住,这篇内容应该能帮你把注意力从流水线挪到真正的攻坚点上。下面按我自己的优先级,把这六处分水岭挨个拆开讲,每处都会带具体参数、代码片段和踩坑记录。
1. 分水岭一:文档解析与切分——决定 RAG 上限的脏活累活
很多团队对“解析”的理解就是“读文本”。生产环境里哪来那么多干净的 TXT?几乎全是 PDF、Word、扫描件、网页、PPT。PDF 有单栏双栏、有表格、有页眉页脚、有各种流程图;扫描件要先 OCR;Word 里可能还挂着批注和修订。直接用 PyPDF2 把 PDF 抽成纯文本,双栏文章会抽成一整行乱序文字,表格数据全部搅在一起,这种输入到了检索阶段,效果能好才怪。
1.1 解析:从“读文本”到“理解版式”
我把解析分成四个层次,按成本从低到高排列:
- 文本抽取层:PyMuPDF 处理 PDF、python-docx 处理 Word,只拿纯文本和基本元数据。
- 布局分析层:用 pdfplumber 或 LayoutParser 先做版面识别,把一页拆成标题区、正文区、表格区、页眉页脚区。双栏文章这时候才能按栏读,而不是从左到右一锅端。
- 表格抽取层:数据密度高的表格直接进向量库是灾难。用 Camelot、TableTransformer 之类把表格结构还原成二维数据,再转成 Markdown 或多行键值对文本。
- OCR 层:扫描件和图片型 PDF,先走 PaddleOCR 或 Tesseract,识别出来的文字再进上面三层。这一步很多人嫌重,但你手里只要有一批扫描合同,跳过去就等着检索效果崩。
实操里最常见的坑是“文档体检没做”。我接手过一个项目,团队把一堆 PDF 丢进一个通用解析函数,结果其中有 30% 是扫描版,解析出来全是空行。排查了半天才发现不是检索模型的问题,是根本就没读到内容。所以动手之前先写个脚本统计一下:总页数、表格数量、每页抽出的字符数、疑似扫描页的数量。根据这个体检结果再决定解析管线怎么配,而不是一套函数吃天下。
1.2 切分:别再用固定窗口撞大运
切分是 RAG 里讨论最多、也最容易出错的一步。常见策略有这么几种:
| 切分策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定窗口切分 | 实现简单、参数可控 | 很容易切断语义,一句完整结论被劈成两半 | 格式统一的通用文档 |
| 父子块切分 | 检索用小块,生成用大块,兼顾精度和上下文 | 逻辑复杂,存储和查询都要多留一层 | FAQ 型知识库、答案需要上下文支撑的场景 |
| 语义切分 | 按语义边界断句,切出来的块更“自然” | 要额外算一遍 embedding 或分类,成本高 | 长文、逻辑性强的制度文档 |
| 结构感知切分 | 按 Markdown 标题、PDF 段落、列表层级切 | 依赖源文档结构足够规整 | 有清晰标题体系的文档、HTML 转文本 |
我见过最蠢但不罕见的模式:不管什么文档,一律 chunk_size=500、overlap=50 硬切。遇到一个长段落讲到三个不同主题,向量被平均稀释,用户问其中任何一个主题都检索不到精确内容。反过来,切太碎也不行——检索 Top-K 里全是“半句话”,拼给 LLM 看的都是上下文缺失的碎片,生成出来的答案东一句西一句,毫无依据。
真正的做法是把“检索粒度”和“生成粒度”分开想。检索用小块提高命中精度,生成用父块补充完整上下文,这就是父子块方案的核心逻辑:一个块是用于检索的子块,一个块是把它连同上下级结构一起包起来的父块,先把子块查出来,再拿父块的内容去喂 LLM。它比单纯调大 chunk_size 高明得多,因为大块只是“更长”,父块是“结构完整”。
1.3 切分配参的实战经验
关于参数,别直接抄别人的。chunk_size 跟你选的 embedding 模型强相关:text-embedding-3-small 支持到 8K token,但塞满 8K 并不意味着检索更准,反而可能引入大量无关信息把向量稀释。中文场景我自己的经验是单块 300-800 字比较稳,overlap 控制在 10%-20% 之间,主要目的是让跨块边界的关键句不会因为恰好被切开而失联。
判断切得好不好,别靠感觉。掏 50 个真实问题出来,查一下每条问题的“正确答案到底落在哪个块里”,看看是不是每次都能落到同一个语义完整的块上。如果答案是“正确答案总被劈成两半”,那问题就出在切分策略上,换个切法比换个 embedding 模型管用得多。解析和切分属于 RAG 的底座工程,这块不扎实,后面所有分水岭都无从谈起。
2. 分水岭二:检索质量——Hit Rate 是一切的地基
2.1 先认识 Hit Rate 这个数字
先定义一下 hit rate:在检索返回的 Top-K 结果里,“包含正确答案”的比例是多少。它只关心正确答案有没有被捞上来,不关心答案最后生成得好不好。很多团队一上来就死磕 prompt 模板、换大模型,生成效果却始终不行,原因往往不是生成环节弱,而是检索环节压根没把对的资料捞出来。我在一个订单文档问答项目里,初始 hit rate 只有 46%,无论怎么优化 prompt,答案质量都上不去;后来把 hit rate 干到 89%,生成质量才真正迎来质变。
检索失败通常有三种典型原因:
- 词表鸿沟:用户问“报销额度上限”,文档里写的是“可报销金额最高为”。两句话语义相同,但字面完全不一样,纯向量检索对这种情况的覆盖率有限。
- 语义漂移:一个 chunk 里塞了三个不同主题,embedding 把整段向量往中间地带推,用户查任何一个主题都差一点。
- 领域术语:在医疗、法律、制造这些领域,通用 embedding 模型对专有词根本没有概念,查“GB 4706.1 标准”这种编号时,向量空间里完全抓瞎。
2.2 三件套:混合检索、重排、Query 改写
应对词表鸿沟和领域术语,最立竿见影的手段是混合检索:BM25 负责关键词精确匹配,向量检索负责语义匹配,两边结果做融合。BM25 吃“报销额度上限”这种字面词,向量吃它和“可报销金额最高为”的语义关系,两者一叠加,覆盖率立刻不一样。
融合算法我推荐 RRF(Reciprocal Rank Fusion),因为它不用校准两类检索各自的分数,直接用排名位置做计算。实现起来十几行:
def reciprocal_rank_fusion(results_list, k=60): fused_scores = {} for results in results_list: for rank, doc_id in enumerate(results, start=1): fused_scores[doc_id] = fused_scores.get(doc_id, 0) + 1 / (k + rank) return sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)重排是第二件套。先用粗召回模型一次性拉回 50 到 100 条,再用 Cross-Encoder 精排取 Top-5。Cross-Encoder 同时把 query 和 document 放进一个模型,让注意力在两者之间交叉,精度远高于向量检索阶段的 Bi-Encoder。代价是速度慢,所以它只适合做粗召回之后的小规模精排。
第三件套是 Query 改写。当用户问“去年西南地区电力项目验收周期平均多长”这种组合问题时,直接用原始 query 去检索容易漏,可以先让 LLM 把问题拆成“西南地区电力项目有哪些”“每个项目验收周期多久”“平均周期计算”几个子查询,分别检索再聚合。改写这一步会让单次请求延迟增加几百毫秒,但复杂问题上的命中率提升非常明显。
2.3 Embedding 选型与微调
选 embedding 模型时,别盲目追新。中文场景里 bge-m3、text-embedding-v3 这类支持长文本和多语言的模型是稳妥选择;如果预算和时间允许,用领域语料把 embedding 微调一版,收益通常比换更大参数量的模型还大。微调不需要海量数据,几百到一两千条与你的知识库真实查询高度相关的(query, passage)对就够跑起来。注意一定要做负样本挖掘,不然模型只会学到“什么都相关”,微调完指标反而下降。
至于 RAG 还有没有别的检索技巧,比如按元数据过滤、分库检索,这些属于场景定制,核心思想是一致的:在把问题交给 LLM 之前,先保证候选资料的质量。没有 hit rate 作为地基,生成环节再折腾也补不了窟窿。
3. 分水岭三:知识组织——从向量堆到本体与图谱
3.1 知识割裂的问题到底出在哪
不少人搭建知识库时,把文档全部切碎后一股脑塞进向量库,没有任何结构。这种“所有东西都往里堆”的玩法,遇到“我们公司哪两个产品线营收占比最高”或者“A 项目负责人和 B 项目的客户是不是同一家公司”这类跨文档问题,立刻露怯。原因就是文档之间的关联没有被建模,每个 chunk 都变成了一座孤岛,这就是热词里提到的“知识割裂”。
我早期踩过最狠的坑:给一家制造业客户搭知识库,单跳问题答得很好,一到“这个设备的供应商以前还供过哪些料”这种涉及关系链路的问题,答案就开始胡编。单纯增加文档量没用,因为信息不在同一块文本里躺着,而在彼此的关系中。RAG 系统不是只存储文本,它得能表达“设备和供应商”“项目和负责人”之间的连接。
3.2 Ontology RAG:用本体做知识约束
本体(Ontology)RAG 的思路是先把领域的实体类型和关系定义成 schema,比如“研发项目”“产品线”“供应商”“里程碑”,再规定它们之间的属性关系。然后做文档解析时,用 LLM 把实体和关系抽出来,存成三元组。查询时不只是在向量库里盲搜,而是可以先做本体过滤,把检索范围限定到相关的实体分支上,再去精准捞内容。
举个例子,用户问“XX 项目负责人是谁”。传统 RAG 是拿整句去向量库碰运气;ontology RAG 会先解析出“XX 项目”这个实体,沿“项目→负责人”这个关系路径去定位,再去指定文档片段里确认细节。命中率更高,也更容易解释结果是怎么来的。
本体设计的原则是“先轻后重”。别一上来就定义几十个实体、上百种关系,你会被自己的 schema 拖死。先划出 5 到 8 个核心实体,把最关键的关系链建起来,跑一批真实问题看哪些检索还失败,再逐步扩展。
3.3 GraphRAG:图谱与社区摘要
GraphRAG 是另一个方向,核心做法是构建文档的实体关系图谱,然后在图上跑社区检测,把关系紧密的实体聚成一个社区,再对每个社区生成一层摘要。查询时先定位相关社区,拿社区摘要做全局层面的理解,再回到底层图谱检索具体证据。它在全局性问题上尤其有优势,比如“我们公司的全部业务范围有哪些”,这种问题散落在几百篇文档里,传统向量检索很难聚合全,但图谱社区摘要可以快速给出全局视角。
值得注意的是,GraphRAG 不是银弹。构建图谱的抽取环节依赖 LLM,成本非常高,而且抽出来的三元组质量参差不齐。我接手过一个项目,图谱里近三成三元组是错的,社区摘要于是全部建立在错误信息上,修正成本极高,最后不得不推倒重建。如果数据量不大,问题又偏“单文档内查询”,完全没必要上 GraphRAG,先做 ontology 过滤或者干脆把文档分组管理,性价比高得多。
3.4 场景判断:到底要不要上图谱
我给个相对可执行的判断标准:
- 知识库少于 500 个文档,问题大多是“这份文档里怎么说”的单跳查询:不需要图谱。
- 问题开始出现明确关系链,比如“某个分支机构隶属于哪条业务线、负责人是谁”:先考虑 ontology 做轻量建模。
- 问题的确需要跨大量文档做全局聚合,且预算充裕、有专门人力维护图谱质量:GraphRAG 才值得上。
知识组织这个分水岭,本质是让 RAG 从“文本堆”进化成“知识网”。它解决的不是“找得到一句话”,而是“找得到一句话和另一句话之间的关系”。这一步做扎实了,很多看起来需要换模型的问题,实际上改改结构就解决了。
4. 分水岭四:Agentic RAG——从单轮到多步推理
4.1 单轮检索解决不了的问题
传统 RAG 的默认流程是“一次检索、一次生成”,这个模式对单跳问题很合适,但遇到“去年我们在西南地区的电力项目有多少个?”这类问题就吃力了——它要先拆出“西南地区有哪些电力项目”,再逐个确认年份和状态,最后数个数。一次检索就算抓回一堆文档,里面混着不同项目、不同年份的信息,LLM 很难从中准确数出结果。
Agentic RAG 的思路是把检索过程交给一个智能体来控制。它不再“问一次就答”,而是先分析问题,判断要不要检索、要怎么检索,再拆成一系列动作:先做一个粗查询,看结果缺什么,再补一个过滤查询,必要时调外部工具算平均值,最后收齐证据才生成回答。你可以把普通 RAG 想象成“搜一下就交卷”,把他想象成“带着检索计划逐步找齐线索再作答”的调研员。
4.2 Agent 的决策循环长什么样
实现上最通用的是类 ReAct 模式:thought(分析现状)→ action(选一个工具)→ observation(观察工具返回)→ 重复,直到信息足够。工具列表通常包括:向量搜索、图谱查询、文档结构查询、计算器、以及最终回答器。
用 LangGraph 这类状态机框架落地时,核心是定义好状态转移。伪代码结构是这样的:
from langgraph.graph import StateGraph def analyze(state): # LLM 决定下一步动作,比如是否需要额外检索 return {"next_action": "search", "sub_queries": ["西南地区电力项目有哪些"]} def search(state): # 执行一次检索,把结果追加进已有的上下文 return {"context": state["context"] + retrieved_chunks} def finalize(state): # 所有信息收齐,生成最终答案 return {"answer": llm.generate(state["context"], state["question"])}每一轮循环都是一次 LLM 调用,所以 agent 流程的延迟和成本都会成倍上升。我见过没做控制的项目,一个简单问题跑了 9 轮工具调用,用户都等睡着了。合理做法是加一个门槛:先让一个轻量判断模型或者简单规则决定“这个问题需不需要 agent”,如果是单跳 FAQ,直接走传统单轮检索;只有命中“需要多次查询”“跨主题对比”“信息不足”这几个信号,才启用 agent 循环。
4.3 成本、延迟与可观测性
Agentic RAG 上线前,你必须把每一步决策都暴露出来。每个循环里的 thought、action、observation 都要记录到日志,否则问题来了你根本不知道是哪一轮检索误导了模型。我项目里最典型的 case:agent 在第二轮引用了一份过期版本的制度文档,后续所有推理都基于错误前提,最后答案自然全错。没有日志,我只会看到“生成的答案不对”,有了日志,一眼定位是检索优先级排序问题。
另外,工具调用要考虑失败重试和超时。向量库临时不可用、图谱查询返回空结果,这些情况都要 agent 能感知到并做出备用选择,否则就会卡死在一个失败的循环里反复消耗 token。成本控制上,我习惯给 agent 设置最大轮次上限,3 到 5 轮是常见阈值,超过直接转人工兜底或者放弃回答。
5. 分水岭五:多模态——知识库不只是文本
5.1 图片到底能不能进知识库
几乎每个做知识库的人都会遇到这个问题:客户的资料里一半信息在图片里,操作手册有截图,PPT 有架构图,PDF 里有数据图表。经典的 RAG 管线对图片是完全无感的——要么丢弃,要么只保留一个毫无信息量的文件名。于是用户问“这个设备的面板指示灯含义”,系统答不上来,因为那张截图压根没被入库。这的确是个大问题,但解决方案并不玄乎,按成本和效果分几个层次来选。
5.2 分级的实现方案
- 最轻量:OCR 转文本。截图、扫描件里的文字先用 OCR 识别出来,以文本形式入库。面板截图上的“电源”“故障”“运行”这几个字,跟文档正文放在一起,检索到文字就等于定位到了对应的图。这个方案零模型成本,实现最快。
- 常见做法:图片生成描述文本再入库。用多模态模型给每张图生成一段 caption,比如“图中展示了设备背板接口布局,从左到右依次为电源接口、网口、串口”,把这段描述作为图中的检索单元。LLM 生成答案时看到描述文本,就能知道图里有什么。需要给用户呈现原图时,在描述文本旁边存好图片路径或 ID,检索到描述就把它对应的图片链接一并返回。
- 进阶方案:图像向量直接入库。用 CLIP、SigLIP 或 Qwen-VL 这类多模态模型,把图片编码成向量,放进和文本相同的向量空间。查询时用文本 query 直接匹配图片向量,做到“文字搜图”。回答阶段如果 LLM 本身具备视觉能力,比如 GPT-4o、Qwen-VL,就把图片直接传给它;如果 LLM 只能读文本,还是把图片描述拼进 prompt。
- 专项方案:图表结构化。数据图表不要只转图片,最好把图表背后的数据抽出来,转成 Markdown 表格或 JSON,让 LLM 看到精确数值而不是看图猜数。这一步对“检索发票金额”“分析季度趋势”这类问题特别关键。
5.3 表格与图表的专项处理
表格在 RAG 里的尴尬程度仅次于图片。整页塞进一个 chunk,表头和正文混在一起,检索和生成都会困惑;拆碎了又丢了表头字段含义。我的做法是:每张表单独成一个 chunk,表头信息和通用上下文作为这个 chunk 的引文前缀,然后再把表格转成 Markdown 或键值对文本。数据密集型表格优先走 Camelot 或 TableTransformer 做结构化抽取,别把整页 PDF 拿 OCR 硬扫。
多模态这一关不一定要上多重的方案。我的经验是,80% 的图片需求靠“OCR + 描述文本入库”就能满足,先别急着上图像向量检索,成本和维护难度都高出一个量级。只有当你真实遇到“用户拿文字描述找一张没文字的架构图”这种场景时,图像 embedding 才真正值得投。
6. 分水岭六:评测与优化闭环——没有评测就是盲人摸象
6.1 三层指标:别只盯着一个数字
很多人问 RAG 的瓶颈在哪,我见过最多的瓶颈不在模型,在“没有评测体系”。优化全靠感觉,今天调个 chunk 大小好像变好了,明天换个 embedding 又说变差了,没有一个客观标准来裁决,整个项目就在反复试错的迷雾里打转。要搭评测闭环,至少要盯三层指标:
- 检索层:Hit Rate@K、MRR、NDCG。回答“正确答案有没有被捞回来、排得够不够靠前”。
- 生成层:忠实度(faithfulness)、答案相关度(answer relevance)、上下文相关度(context relevance)。回答“答案是否忠于给定资料、是否真的回应了问题”。
- 端到端层:人工匿名评分或者 LLM-as-judge 打分。回答“用户体感到底怎么样”。
每一层都要独立看。我惯用的排查套路是:答案不对,先看 Hit Rate。如果 hit rate 低,那就是检索层的问题,去改解析、切分、混合检索、重排;如果 hit rate 已经很高但答案还是不对,那才是生成层的问题,去改 prompt、换生成模型、加幻觉检测。这个先后顺序能帮你避免最无效的优化操作——明明是检索漏了,却花一整天调 prompt。
6.2 用 Ragas 搭建评测脚手架
市面上有不少评测工具,Ragas 是比较好上手的一个。构造一批带标准答案的问题集,跑一遍评测脚本就能得到基础指标:
from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_relevancy result = evaluate( dataset=eval_dataset, metrics=[faithfulness, answer_relevancy, context_relevancy] ) df = result.to_pandas()评测集不是随便攒 20 条问题就完了。我建议至少覆盖这六类:单跳问答、多跳聚合、否定类问题(“哪些项目没有通过验收”)、术语改写表述、跨文档查询、无答案问题(知识库里根本没这信息,系统应该诚实说不清楚而不是编)。这六类覆盖不住,评测结果会严重失真。我见过只测 30 条简单单跳问题的项目,指标高得吓人,一上真实用户问题立刻现原形。
6.3 建立 Badcase 回归机制
评测不是为了跑一次拿个漂亮数字,而是为了把每一轮的优化结果固化下来。我建议把每次用户反馈的失败案例做成 Badcase 库,每一条记录三样东西:原始问题、模型给出的回答、期望的正确答案来源(指出错在哪一步)。然后每一版优化之后,都把 Badcase 库全量回归一遍,观察哪些问题被修复了、哪些还是错的、有没有引入新的问题。
用 LLM 做自动评测时要注意顺序偏差。同一个答案,排在另一个答案前面和后面的得分可能不一样,评测脚本里最好做顺序随机化,并且定期抽样人工复核,纯靠自动打分容易被系统性的偏差带偏。我在一个项目里发现自动评分 90 分,人工一看答案里引用了完全不存在的文件号,忠实度这一项必须结合实体级校验来做,不能完全信任大模型自评。
6.4 从指标反推优化路径
评测闭环建起来之后,优化路径就清晰了:
- Hit Rate 低到无法接受 → 先查解析是否完整,再审视切分粒度,然后加混合检索和重排,必要时微调 embedding。
- Hit Rate 合格但忠实度低 → 生成模型在自由发挥了,收紧 prompt 的“只能使用给定资料”约束,压低 temperature,或引入“找不到就明说”的兜底逻辑。
- 检索结果里全是相似但不相关的 chunk → 多半是 embedding 模型区分度不够,或者知识库文档之间存在大量重复表达,考虑做去重和聚类。
RAG 优化是一个循环,不是一次性交付。评测体系决定你在这个循环里走得多快、多准。没有它,所有调参都是盲人摸象;有了它,每一次改动都能用数字说清楚“到底变好了没有”。
写到这里,我最想表达的是:RAG 的入门门槛确实低到烂大街了,但真正把一个 rag知识库 做成能扛业务压力的系统,分水岭从来都在细节里——解析切分有没有认真做、hit rate 有没有被量化、知识有没有被组织成结构、控制流够不够灵活、多模态有没有被遗漏、评测闭环有没有建立起来。这六处每补一处,你都会感受到明显的质变;补到后面,你会发现很多前期架构上的问题会反过来逼迫你重构——这不是坏事,说明你正在从流水线思维走向真正的工程化思维。
按我个人的习惯,如果你现在要新起一个 RAG 项目,我不会让你先选模型或者搭框架。我的建议是:先收集 100 道贴近真实业务的问题,做出第一版可以跑通的基线,把 hit rate 打出来,再沿着这六处逐项排查、逐项优化。等这六个地方都有了量化数据,你对整个系统的掌控力就完全不一样了——到那时候,再有人跟你说“RAG 烂大街”,你大概也只能笑笑:烂大街的是流水线,分水岭从来都在这些没人愿意细抠的地方。