1. 被L7带偏的RAG讨论:为什么大家都在卷错方向
过去大半年,只要聊到RAG系统的效果优化,几乎所有人第一反应都是"检索层怎么调"。换更好的embedding模型、上rerank、调chunk size、加HyDE、搞多路召回……这些手段确实有用,但如果你真的把一个RAG系统从demo做到线上,就会发现一个很尴尬的事实:检索层再怎么优化,天花板也就那样,真正决定系统能不能用的,是L3和L4这两层。
先把话说清楚,这里的L7、L3、L4不是网络协议那套东西,而是我在做RAG系统时习惯用的一套分层视角。L7指的是最上层、最贴近用户感知的那一层——检索召回、排序、prompt拼装、答案生成,也就是大多数人理解的"RAG本体"。L3指的是文档理解层,也就是原始文档怎么被解析、切分、结构化。L4指的是知识组织层,也就是这些被解析出来的内容,怎么被组织成机器能真正"理解"和"推理"的结构。
为什么说L7不是护城河?因为L7这一层的东西,几乎全是公开的、可复制的。LangChain、LlamaIndex、各种开源框架把检索链路封装得明明白白,你换一个embedding模型,别人明天也能换。你加一个rerank,别人照着文档抄一遍就行。L7的优化是"工程调参",不是"能力壁垒"。
而L3和L4不一样。文档理解层决定了你的系统能不能读懂一份带复杂表格的PDF、能不能处理扫描件里的手写批注、能不能把一份合同里的条款关系抽出来。知识组织层决定了你的系统能不能回答"这个条款和那个条款冲突吗"这种需要跨文档推理的问题。这两层做不好,L7再怎么调,用户问一个稍微复杂点的问题,系统就开始胡说八道。
我见过太多团队,花三个月调检索,最后发现真正卡住他们的是:PDF解析出来全是乱码、表格被切得七零八落、章节层级全丢了。这时候你embedding再强也没用,因为喂进去的本来就是垃圾。
一句话总结:L7决定你的系统"能不能跑",L3和L4决定你的系统"能不能用"。
2. L3文档理解层:RAG系统真正的第一道分水岭
2.1 为什么文档解析是RAG最容易被低估的环节
大部分人做RAG的第一步是什么?找个PDF loader,把文档读进来,按固定长度切一刀,扔进向量库。这个流程在demo阶段完全没问题,因为demo用的文档通常是你自己精心挑选的、格式规整的、纯文本为主的文档。但真实场景里的文档是什么样的?
- 一份产品手册,里面有大量嵌套表格,表格里还有合并单元格
- 一份技术白皮书,双栏排版,中间穿插图表和脚注
- 一份合同,条款之间有引用关系,还有手写的修改痕迹
- 一份扫描版的老文档,OCR出来一堆错别字
这些文档你用固定长度切分,切出来的chunk是什么?是"上半句在讲A,下半句突然跳到B"的碎片。这种碎片喂给embedding模型,向量本身就是模糊的,检索出来的结果自然驴唇不对马嘴。
文档理解层的核心任务,不是"把文字读出来",而是"把文档的结构和语义关系保留下来"。这两件事的难度差了一个数量级。
2.2 从"读文字"到"读结构":文档解析的三个层次
我把文档解析分成三个层次,你可以对照看看自己的系统在哪一层:
| 层次 | 能力 | 典型做法 | 适用场景 |
|---|---|---|---|
| 第一层:文本提取 | 把文字读出来 | PyPDF2、pdfplumber基础用法 | 纯文本、格式简单的文档 |
| 第二层:结构还原 | 保留标题、段落、表格、列表的层级 | 版面分析模型 + 规则后处理 | 技术文档、报告、手册 |
| 第三层:语义抽取 | 抽取实体、关系、条款引用等语义信息 | LLM辅助抽取 + schema约束 | 合同、法规、知识密集型文档 |
大多数团队卡在第一层到第二层之间。他们能读出文字,但读不出结构。结果就是:一份有清晰章节层级的文档,被切成了几百个无差别的文本块,章节标题和正文混在一起,表格被拆成一行一行的碎片。
这里有个很关键的认知:chunk的质量不取决于你切多大,而取决于你切的时候知不知道自己在切什么。如果你知道这是"第三章第二节下面的第一个表格",你切出来的chunk就自带上下文;如果你只是按500字一刀切,切出来的就是无根之木。
2.3 表格和图片:文档理解里最难啃的两块骨头
先说表格。表格是RAG系统里最容易被处理坏的元素。一个典型的坑是:用普通的文本提取工具读PDF表格,读出来是这样的:
产品名称 规格 价格 库存 A产品 10cm 100 有货 B产品 20cm 200 缺货看起来还行?但如果表格有合并单元格、有跨页、有嵌套表头,读出来就完全乱了。更麻烦的是,表格里的信息往往是"结构化"的,你把它拍平成文本,就丢失了行列之间的对应关系。用户问"20cm的产品多少钱",系统如果只看到"20cm 200"这个碎片,根本不知道200是价格还是库存。
我的做法是:表格单独处理,不跟正文混在一起切。用专门的表格识别工具(比如基于版面分析的模型)把表格抽成结构化数据(JSON或Markdown表格),然后给每个表格加上"它属于哪个章节、它的表头是什么、它描述的是什么"这些元信息。检索的时候,表格作为一个独立的、带完整上下文的单元被召回。
再说图片。热词里有个问题特别典型:"rag知识库能存储图片嘛"。答案是能,但关键在于你怎么存。如果你只是把图片转成base64塞进向量库,那基本没用。图片在RAG里的正确用法是:
- 图片转文字描述:用多模态模型给图片生成详细的文字描述,把描述文本存进向量库
- 图片和上下文绑定:图片所在的章节、图片的标题、图片前后的正文,都要作为元信息绑定
- 图片单独索引:如果图片本身包含关键信息(比如流程图、架构图),要单独建索引,检索时能直接命中
我实测下来,图片处理做得好不好,对技术文档类RAG系统的效果影响能到30%以上。因为技术文档里大量关键信息就在图里,你只读文字,等于漏了一半。
2.4 一个真实的文档解析踩坑记录
说个我自己的经历。之前做一个内部技术文档的RAG系统,文档是Markdown格式的,我心想Markdown总好处理吧,结构清晰、格式规整。结果上线后发现,用户问"XX功能的配置参数有哪些",系统总是答不全。
排查过程是这样的:
第一步,我看检索出来的chunk。发现配置参数确实被检索到了,但只检索到了一部分。为什么?因为配置参数在原文里是一个表格,而我的切分逻辑是按标题层级切的,表格被当成了一个普通段落,切成了好几块。
第二步,我看切分逻辑。发现我用的Markdown切分器是按##和###切的,但表格在Markdown里是|开头的行,切分器不认识表格,就把表格当普通文本处理了。
第三步,我看embedding。发现表格被切成碎片后,每个碎片的向量都很模糊,因为表格碎片本身语义不完整。
修复方案:在切分之前,先把Markdown里的表格识别出来,单独抽成结构化数据,给表格加上"所属章节"的元信息,然后表格作为一个整体存入向量库。同时,在prompt里明确告诉模型:"如果检索到表格,请按行列关系理解。"
改完之后,配置参数类问题的回答完整度从60%左右提升到了90%以上。这个坑的本质是:切分器不懂文档结构,把结构化内容当成了非结构化内容处理。
3. L4知识组织层:从"能检索"到"能推理"的关键一跃
3.1 向量库的天花板:为什么相似度检索解决不了关系问题
向量库的核心能力是"语义相似度检索"。你问一个问题,它找最相似的文本块。这个能力在"事实型问答"上很好用,比如"XX功能的默认端口是多少",检索到相关段落就能答。
但用户的问题往往不是事实型的,而是关系型的:
- "A方案和B方案在性能上有什么区别?"
- "这个配置项改了之后会影响哪些其他功能?"
- "文档里提到的三个限制条件之间有没有冲突?"
这些问题需要的是跨文档、跨段落的关系推理,而向量相似度检索天然不擅长这个。因为相似度检索是"点对点"的,它找的是"和问题最像的文本块",而不是"和这个问题相关的所有信息以及它们之间的关系"。
这就是L4知识组织层要解决的问题:把散落在各个chunk里的知识,组织成一张有关系的网,让系统能沿着关系去推理。
3.2 知识图谱和RAG的结合:不是赶时髦,是刚需
热词里出现了"ontology rag"和"kg知识库",说明大家已经开始意识到这个问题了。知识图谱(KG)和RAG的结合,不是为了赶时髦,而是因为纯向量RAG在关系推理上确实有硬伤。
我举个具体的例子。假设你有一个产品文档库,里面有:
- 文档A:功能X的配置说明,提到"功能X依赖功能Y"
- 文档B:功能Y的配置说明,提到"功能Y在版本2.0后废弃"
- 文档C:版本升级说明,提到"版本2.0移除了功能Y"
用户问:"功能X在当前版本能用吗?"
纯向量RAG的做法:检索"功能X",找到文档A,回答"功能X依赖功能Y"。但它不会主动去查功能Y的状态,因为功能Y和问题"功能X能用吗"的语义相似度不高。
KG+RAG的做法:系统知道"功能X --依赖--> 功能Y --状态--> 已废弃",沿着这条关系链推理,得出"功能X在当前版本不可用"。
这就是L4的价值:它让系统从"检索相关文本"升级到"沿着关系推理"。
具体怎么落地?我的做法是分三步:
- 实体抽取:用LLM从每个chunk里抽取实体(功能名、配置项、版本号、限制条件等)和关系(依赖、冲突、替代、包含等)
- 图谱构建:把抽取出来的实体和关系存进图数据库(Neo4j、NebulaGraph都行),同时保留每个实体对应的原文chunk ID
- 混合检索:用户提问时,先做向量检索找到相关实体,然后沿着图谱关系扩展,把关联的实体和原文一起召回
这套方案听起来复杂,但落地成本没有想象中高。关键是schema设计要克制,不要一上来就搞几十种关系类型,先从最核心的几种开始(依赖、包含、冲突、替代),跑通了再扩展。
3.3 结构化知识库和向量知识库的分工
热词里有个问题问得很好:"rag知识库和结构知识库区分以及应用场景"。我的理解是,这两者不是替代关系,而是互补关系。
| 维度 | 向量知识库 | 结构化知识库(KG/表) |
|---|---|---|
| 擅长 | 模糊语义匹配、非结构化文本 | 精确关系查询、多跳推理 |
| 不擅长 | 关系推理、精确过滤 | 模糊语义、非结构化内容 |
| 典型场景 | "XX是什么意思" | "XX和YY有什么关系" |
| 更新成本 | 低,重新embedding即可 | 高,需要维护schema和关系 |
我的实践是:向量库做第一层召回,结构化知识库做第二层推理。用户提问后,先用向量库找到相关实体,然后去结构化知识库里查这些实体的关系,最后把两部分信息一起喂给LLM生成答案。
这样做的好处是,既保留了向量库处理非结构化文本的灵活性,又补上了关系推理的能力。而且结构化知识库可以单独维护、单独更新,不会因为文档更新就全部重建。
3.4 文档理解层和知识组织层的联动
L3和L4不是孤立的,它们之间有很强的联动关系。L3解析出来的结构信息(章节层级、表格结构、条款编号),是L4构建知识图谱的重要输入。
举个例子:如果L3能正确识别出"这是第三章第二节的表格,表格标题是'配置参数对照表'",那L4在抽取实体时,就能给每个参数加上"属于配置参数对照表、位于第三章第二节"这些上下文。这些上下文在后续检索和推理时非常有用。
反过来,如果L3解析得一塌糊涂,章节层级全丢了,表格被切碎了,那L4抽取出来的实体就是一堆没有上下文的孤立点,图谱建出来也是残缺的。
所以我的建议是:先把L3做扎实,再去做L4。不要跳过L3直接搞知识图谱,那样建出来的图谱质量很差,反而会拖累系统效果。
4. 从L7到L3&L4:一套可落地的分层优化路径
4.1 先诊断:你的RAG系统到底卡在哪一层
在动手优化之前,先做个诊断。我常用的诊断方法是"分层归因":
准备一组测试问题,覆盖事实型、关系型、推理型三类。然后对每个问题,看系统失败的原因是什么:
- 如果检索出来的chunk本身就是错的(没找到相关文档),问题在L7的检索层
- 如果检索出来的chunk是对的,但chunk本身语义不完整(比如表格被切碎),问题在L3
- 如果检索出来的chunk都对,但系统答不出关系型问题,问题在L4
这个诊断做完,你就知道该往哪投入资源了。我见过太多团队,明明是L3的问题,却一直在L7上使劲,结果就是投入产出比极低。
4.2 L3优化的具体动作清单
如果你诊断下来问题在L3,可以按这个清单来优化:
第一步:换掉默认的文档加载器。不要用框架自带的通用loader,针对你的文档类型选专门的工具。PDF用版面分析工具,Markdown用结构化解析器,HTML用DOM解析。
第二步:表格单独处理。把表格从正文里抽出来,转成结构化格式(Markdown表格或JSON),加上所属章节、表格标题等元信息,单独索引。
第三步:保留章节层级。切分的时候,把章节标题作为元信息附加到每个chunk上。这样检索出来的chunk自带上下文,LLM也更容易理解。
第四步:图片转描述。用多模态模型给图片生成文字描述,描述文本和图片元信息一起存入向量库。
第五步:加一层文档质量检查。解析完之后,抽样检查chunk的质量。如果发现大量chunk语义不完整,说明解析环节有问题,要回去调。
4.3 L4优化的最小可行方案
L4的落地不需要一上来就搞大而全的知识图谱。我推荐一个最小可行方案:
第一步:定义核心实体类型。根据你的业务场景,定义3-5种最核心的实体类型。比如技术文档场景,可以是"功能"、"配置项"、"版本"、"限制条件"。
第二步:用LLM做实体和关系抽取。对每个chunk,用LLM抽取实体和关系。prompt要写清楚schema,让LLM按固定格式输出。
第三步:存进图数据库。用Neo4j或NebulaGraph存实体和关系,同时保留每个实体对应的chunk ID。
第四步:混合检索。检索时,向量检索和图谱检索并行,结果合并后一起喂给LLM。
第五步:迭代schema。跑一段时间后,看哪些关系类型最常用、哪些实体类型缺失,逐步完善schema。
这个方案的好处是启动成本低,一两个星期就能跑通。跑通之后再逐步扩展,比一上来就设计完美schema要务实得多。
4.4 一个完整的优化案例:从60分到85分
说个我实际做过的优化案例。一个内部知识库RAG系统,初始版本用的是"PDF loader + 固定长度切分 + 向量检索"这套标准流程。上线后用户反馈"答非所问"、"答案不完整"。
诊断结果:
- 事实型问题准确率约70%
- 关系型问题准确率约40%
- 推理型问题准确率约20%
优化动作:
- L3优化:换用版面分析工具解析PDF,表格单独抽取,章节层级保留。chunk质量明显提升。
- L4优化:抽取"功能-依赖-功能"、"配置项-属于-功能"、"版本-废弃-功能"三类关系,建了一个轻量图谱。
- L7微调:在L3和L4优化后,重新调了chunk size和rerank策略。
优化后:
- 事实型问题准确率约90%
- 关系型问题准确率约75%
- 推理型问题准确率约60%
关键发现:L3和L4的优化贡献了大部分提升,L7的调整只是锦上添花。这印证了我一直以来的判断:L7不是护城河,L3和L4才是。
5. 那些没人告诉你的实操细节和避坑经验
5.1 文档解析的"脏数据"问题
真实文档里充满了"脏数据":页眉页脚、水印、扫描噪点、OCR错字、格式错乱。这些东西如果不处理,会严重污染向量库。
我的做法是加一层"清洗规则":
- 页眉页脚:按位置规则过滤(比如每页顶部和底部的固定区域)
- 水印:用图像处理去掉,或者用文本规则过滤
- OCR错字:用LLM做一遍纠错,或者建一个常见错字映射表
- 格式错乱:用规则+LLM结合的方式修复
这层清洗看起来不起眼,但对最终效果影响很大。我实测过,清洗前后,检索准确率能差15%以上。
5.2 chunk size不是拍脑袋定的
很多人问我chunk size设多少,我的回答永远是:取决于你的文档结构和问题类型。
- 如果文档结构清晰(有明确章节),按章节切,chunk size可以大一些(800-1200字)
- 如果文档结构模糊,按语义切,chunk size小一些(300-500字)
- 如果问题需要跨段落推理,chunk size要大一些,或者用"父文档-子文档"的层级切分
关键是不要一刀切。我现在的做法是:按文档结构动态决定chunk size,同时保留一个"父chunk"的引用,检索到子chunk时能把父chunk一起召回。
5.3 知识图谱不是建得越大越好
我见过一些团队,一上来就建了一个几万个节点的大图谱,结果检索时根本用不上,因为关系太稀疏了,沿着关系走两步就走到无关节点了。
我的经验是:图谱的价值在于"关系密度",不在于"节点数量"。与其建一个大而稀疏的图谱,不如建一个小而密的图谱,把核心实体之间的关系抽全、抽准。
具体做法:先聚焦最核心的20%实体,把这20%实体之间的关系抽全,跑通之后再逐步扩展。这样图谱的可用性会高很多。
5.4 别忘了评估环节
优化L3和L4的时候,一定要有评估环节。不然你改了一堆东西,不知道到底有没有效果。
我的评估方法是:
- 准备一组标注好的测试问题(50-100个),覆盖不同类型
- 每次优化后,跑一遍测试集,看准确率变化
- 对失败的case做归因分析,看是L3的问题还是L4的问题
这个评估环节看起来费时间,但能帮你避免"瞎优化"。我见过太多团队,改了一堆东西,结果效果反而下降了,就是因为没有评估环节。
5.5 一个容易被忽略的点:文档更新
真实场景里,文档是不断更新的。今天解析好的文档,明天可能就改版了。如果你的L3和L4没有考虑更新机制,那系统很快就会过时。
我的做法是:
- L3层:文档更新时,重新解析,对比新旧chunk,只更新变化的chunk
- L4层:文档更新时,重新抽取实体和关系,更新图谱
- 加一个"版本标记",检索时优先召回最新版本的chunk
这个更新机制看起来简单,但落地时有很多细节要处理。比如怎么判断"变化"、怎么处理"删除的chunk"、怎么保证图谱的一致性。这些都需要在系统设计时就考虑进去。
6. 回到标题:护城河到底在哪
说了这么多,回到标题的问题:真正的护城河在哪?
我的答案是:护城河不在L7的检索调参上,而在L3的文档理解能力和L4的知识组织能力上。
原因很简单:L7的东西是公开的、可复制的,你调一个参数,别人明天也能调。但L3和L4不一样,它们需要你深入理解自己的业务场景、自己的文档特点、自己的用户问题类型。这些东西是没法直接抄的,必须自己一点点打磨。
而且L3和L4的优化是"复利"的。你今天把文档解析做好了,明天所有基于这些文档的检索和推理都会受益。你今天把知识图谱建好了,明天所有关系型问题都能答得更好。这种复利效应,是L7调参给不了的。
我个人的体会是:做RAG系统,前期在L3和L4上多投入,后期在L7上少折腾。前期把文档理解和知识组织做扎实,后期检索层用默认配置就能跑得不错。反过来,如果前期偷懒,后期在L7上怎么调都补不回来。
最后分享一个小技巧:如果你不确定自己的系统卡在哪一层,就做一个简单的测试——把检索出来的chunk直接打印出来看。如果chunk本身就是乱的、碎的、语义不完整的,那问题一定在L3。如果chunk是好的,但系统答不出关系型问题,那问题一定在L4。这个测试花不了十分钟,但能帮你省下几个星期的瞎折腾。