前几年提起RAG,大家的第一反应还是“检索增强生成”这个新名词。到了今年,情况已经变成:随便一个团队,拉上模型API加向量数据库,三天就能把问答demo跑起来。于是有了那句很流行的吐槽——RAG烂大街了。但我做了这么多RAG实战项目之后,反而觉得烂大街的只是那条流水线:导入文档、切块、向量化、召回、拼接、生成,翻来覆去就这几步,谁都会搭。真正把项目水平和Demo拉开差距的,是流水线之外那些很少被画在架构图里的细节。我总结下来,分水岭基本集中在六个地方:文档解析、分块策略、召回组合、查询改写、知识库结构、评测闭环。这篇文章就把每一处容易踩的坑和能落地的做法都摊开讲一遍。如果你正准备做RAG实战,或者已经搭好一条流水线但总觉得效果不理想,这六个点值得逐条对照检查。
1. 第一处分水岭:文档解析与流程清洗,源头脏一切都白搭
1.1 大多数RAG项目在第一步就埋了雷
很多团队习惯把文档直接丢给框架自带的Loader,PDF扔进去,文本抽出来,向量化,完事。Demo阶段看不出问题,真到了生产环境就会发现:用户问的问题明明在文档里,模型却答不上来。这时候排查链路会非常长,但根因往往是解析阶段已经丢了关键信息。
我见过最多的一个场景是PDF里的多栏排版。技术手册、行业报告、招标文件特别喜欢双栏甚至三栏布局,通用解析器按阅读顺序逐行抽取时,经常把左栏的下半段直接接到右栏的上半段,一句话被拦腰裁成两半,原本的含义全部错乱。另一种更隐蔽的问题是表格。很多解析器在提取PDF表格时,要么把单元格合并成一行乱码,要么直接跳过表格内容。而合同、报价单、参数表恰恰是问答场景里最常被查询的对象。我的经验是:如果文档里有大量表格,不要在解析器层面指望通用方案,优先考虑专门的表格抽取工具或者结构化预处理。
[示例:用pdfplumber抽取表格并保留页码]
import pdfplumber with pdfplumber.open("合同模板.pdf") as pdf: for page_idx, page in enumerate(pdf.pages): tables = page.extract_tables() for table in tables: for row in table: clean_row = [str(cell).replace("\n", " ").strip() if cell else "" for cell in row] # 把表格行转成带页码标记的文本,后续随向量一起入库 print(f"[p{page_idx + 1}]", " | ".join(clean_row))这段脚本输出的每一行都带上页码,这个习惯非常重要。将来做答复溯源、按原文引用、按页定位,全指望这里保留的页码信息。
1.2 本地可用的文档拆解工具链怎么选
从热搜里能看到不少人直接搜“有没有本地的rag文本拆解工具”,说明大家已经意识到,在线解析服务一方面上传敏感资料不放心,另一方面格式限制也多。本地工具链其实很成熟,关键是按文件类型组合使用:
- 扫描版PDF/图片型PDF:先用OCR成文字,再走文本解析。PaddleOCR和Tesseract都能跑在本地,前者对中文版面支持更好。
- 原生PDF(文字可选中的那一类):pdfplumber和PyMuPDF各有侧重,pdfplumber擅长表格坐标定位,PyMuPDF胜在速度和结构化信息提取。
- Word文档:python-docx可以抽取段落和表格,注意不要用简单的文本抽取把docx当txt读,样式层级会丢。
- Markdown/HTML:这反而是我最推荐的知识库源格式,自带标题层级和结构语义,切块时可以当成天然锚点使用。
有一个关键认知要提前建立:不要追求一个工具处理所有文件。用多工具组合,每种文档走最适合它的管线,比在一个库里反复调参高效得多。我自己的知识库管线会写一个简单的路由函数,根据文件扩展名和PDF是否含文本层来决定调用哪条解析链路,实测下来各种混杂文档的解析成功率比单一通用工具高不少。
1.3 解析阶段最容易丢的三类内容
第一类是页眉页脚和页码。这些噪声如果不清除,会被当成正文切进向量里,检索时产生大量无关片段。第二类是脚注和侧边注释,学术PDF尤其明显,正文与脚注混淆会让模型引用错误结论。第三类是图片里的文字,包括截图、流程图、带文字的架构图。普通文本解析对它们毫无办法,需要在预处理阶段把这类图片单独提取出来做OCR,或者把图片文件本身交给多模态模型处理。
我踩过一次很深的坑:把一份设备操作手册直接喂给解析器,流程图里的“应急停机按钮位置”这段文字全部被吞掉了,因为它在图片里。结果用户连问三遍“急停按钮在哪”,系统都答不出准确位置。后来我在流程里加了图片提取与OCR环节,把图片内的文字单独成块入库,这类问题才算根治。所以说一句很实在的话:做RAG不要先扑向向量模型和Prompt,先把手里的源文档处理干净,这一步的收益远超后面任何调参。
2. 第二处分水岭:分块策略,从“按字符砍”到“按语义切”
2.1 固定窗口分块的隐蔽缺陷
很多人第一次做RAG,分块逻辑就是“每隔500个字符切一刀”。这种固定窗口切法在纯说明性文本上勉强可用,但实际语料里到处都是它会切碎语义的反例:一个完整的操作步骤被从中间切断,前半句在上一块,后半句在下一块;一段结论和它的前置条件被分隔开,召回时只拿到一半;表格刚刚解析成文本,又被横着一刀切成碎片。
常见做法是加overlap,每两个相邻块重叠50到100个字符,试图缓解切断问题。但overlap治标不治本,它只是降低了切在语义关键位置的概率,并没有真正解决语义断裂。更麻烦的是,overlap会让一个信息点在多个块里重复出现,召回时可能出现同一内容反复命中,挤占了本应返回其他相关内容的名额。
2.2 四种常见分块策略的取舍
务实一点看,工程上值得认真考虑的其实是下面这几种:
| 策略 | 实现思路 | 适合场景 | 主要风险 |
|---|---|---|---|
| 固定窗口+overlap | 按字符数切块,块间重叠 | 纯文本、日志、格式统一的说明文档 | 切断语义单元 |
| 结构分块 | 按Markdown标题、章节、段落边界切 | Wiki、帮助中心、规范文档 | 依赖源文档结构完整 |
| 语义分块 | 按embedding相似度变化点切分 | 长文档、主题切换明显的资料 | 计算开销大,边界不稳定 |
| 父子分块 | 子块用于检索,父块用于生成 | 需要回答完整上下文的场景 | 存储复杂,召回后需二次取块 |
从“Wiki和RAG”这类热搜也能看出来,很多人是在给企业知识库搭问答系统,源文档多数是写好的Wiki页面,这种情况下第一选择应该是结构分块而非固定窗口。Markdown的标题天然就是分块的锚点,按二级标题或三级标题把页面切开,得到的基本就是独立成章的语义单元。
2.3 我实测过的分块参数经验
关于块大小,建议不要听网上一句话就锁死某个数值。块大小的最优解高度依赖内容形态,也依赖你选的embedding模型支持的最大输入长度。我的实测习惯是:先按结构分块,观察每个块的字符数分布,再决定是否需要对过长块做二次拆分;检索质量差时优先检查是不是块粒度与用户问题粒度过大或过小。
举个我调过的例子。某产品知识库里描述安装步骤的段落,平均每个步骤约300字,完整安装流程约2000字。一开始用512字符固定窗口切,经常把“步骤3”和“步骤4”切进不同块,用户问“安装时需要注意什么”,模型只召回步骤3的细节,丢了步骤4的警告信息。改成父子分块之后,子块按步骤粒度切、父块保留完整安装流程,检索命中的是步骤块,送入生成的却带上完整流程上下文,答复质量一下子改善了。这类调整没有银弹,唯一靠谱的方法是把几种策略都放进自己的评测集里跑一遍对比。
3. 第三处分水岭:召回不是单选一个向量库那么简单
3.1 纯向量检索救不了专有名词
这是RAG项目里最常见的返工点:第一版用纯向量检索,效果看起来不错,一上线就被真实用户的专有名词问崩。原因不复杂——向量检索捕捉的是语义相似,遇到产品型号、工单编号、设备名称这类“精确匹配才有效”的查询,它反而力不从心。
举个例子:用户问你“HL-2090 后面板接口怎么接”,如果知识库里写的是“HL-2090 型设备后面板包含电源、网口、USB口”,向量检索可能召回一段相近的“HL-2080 接口说明”,因为语义上太接近了。但正确答案根本不是那个型号。这种情况在售后知识库、设备维保、政策法规场景里很常见。我在本地知识库项目里就反复吃过这类亏,后来才意识到:召回阶段不能只靠一个向量索引。
3.2 混合检索的落地顺序与权重融合
最稳的做法是混合检索:向量检索负责语义相关,BM25/关键词检索负责精确匹配,最后把两路结果合并。工程实现上有两条路:
第一是自建ES或Meilisearch,配合向量插件做混合索引。这条路线适合需要重度自定义、已有搜索基础设施的团队,但运维成本高。
第二是轻量的本地组合。例如用SQLite加vec插件存向量,同时用倒排索引做关键词检索,代码量不大,适合本地知识库或中小规模场景。做RAG实战时我倾向于先用轻量组合跑通,再按需升级,而不是一开始就上重型组件。
两路结果合并时,最简单的办法是RRF(Reciprocal Rank Fusion)——对多路结果按排名倒数求和,重新排序。这个算法屏蔽了不同召回模块分数不可比的问题,几行代码就能实现:
[示例:RRF合并召回结果]
def rrf_fusion(results_by_query, k=60): score = {} for result_list in results_by_query: for rank, doc_id in enumerate(result_list): score[doc_id] = score.get(doc_id, 0) + 1.0 / (k + rank) return sorted(score.items(), key=lambda x: x[1], reverse=True) # 用法:分别拿到向量检索和关键词检索的top结果,合并打分 # fused = rrf_fusion([vector_top_ids, bm25_top_ids])实际项目中,混合检索能把“型号精确匹配”“同义改写”“模糊语义召回”这几类问题同时兜住,是性价比最高的性能提升手段。
3.3 重排序:多花几十毫秒,质量上一个台阶
即使做了混合召回,top候选里仍难免混入不相关片段。这时候需要重排序模型对候选集再做一次精排。重排模型的应用方式是:把用户问题和每个候选块拼接成一对输入,让模型打一个相关性分,然后按分重新排序。因为重排阶段只看有限个候选,不像向量检索要面对整个库,计算成本可控。
我自己的经验是:加不加重排,体验差异非常明显。尤其当知识库里存在大量相似文档时,比如多个版本的产品手册,向量召回前三名可能全是同一型号的近似页面,真正对应的旧版参数反而排在后面。重排模型能把“旧版X”和“新版X”的区别识别出来,把最匹配的那一个顶上来。本地部署场景下,目前的中小型重排模型在CPU上耗时大约几十到一二百毫秒,换取的回答准确率提升非常划算。
3.4 本地部署场景的轻量组合
热词里有“ollama + 简易本地RAG知识库”和“langchain4j easy rag”,说明本地化部署需求很大。本地环境通常没有GPU集群,甚至embedding和生成模型都跑在CPU上,资源约束明显。我搭过的本地方案大致是:embedding模型用中小尺寸的文本向量模型,检索用上面说的SQLite向量插件加BM25组合,重排用轻量cross-encoder模型,生成端接Ollama加载的本地模型。整条链路没有外部API调用,数据全部留在本机,隐私友好,启动也快。
要提醒的是,本地模型对知识库质量的敏感度比大模型API更高。大模型API有强大的补全和改写能力,即使检索片段有些瑕疵也能强答;本地小模型没有这个冗余,检索不准,答得就离谱。所以在本地RAG项目里,更要把前面的解析、分块、召回一个个抠到位。这个道理放在所有RAG项目上都成立。
4. 第四处分水岭:查询改写与多轮上下文,别让用户替你凑问题
4.1 用户提问的三个典型问题
真实用户的提问和评测集里的标准问法差距很大。第一个典型问题是指代,多轮对话里用户直接问“它的价格是多少”“那后来呢”,如果不处理指代,检索到的内容必然偏。第二个典型问题是省略,用户只给几个关键词:“A项目 延期 原因”,这种碎片化查询直接去检索,匹配质量不稳定。第三个典型问题是口语化,用户的问题带了很多语气词和背景叙述,堆在一起向量化后中心反而被稀释。
很多团队不去处理这些,直接把用户原文塞给检索模块,然后怪模型不行。实际上,用户根本不会按照embedding模型的偏好来组织语言,让系统适应人,还是让人适应系统,这是RAG项目的一道真正的分水岭。
4.2 改写逻辑:从规则到模型
查询改写不一定非要上大模型。工程上推荐按复杂度递增的顺序来做:
- 第一步:规则改写。处理人称指代、缩写展开、停用词过滤。“它”“该设备”“这个”这类代词,结合对话历史里最近一次出现的实体名替换。写几十行正则就能覆盖大部分情况。
- 第二步:模板改写。针对缺失主语的碎片式问题,用“用户问的是[业务模块]下的[实体],想了解的是[意图]”这类模板补全。
- 第三步:模型改写。用大模型根据对话历史重写当前问题,这也是RAG智能体项目里常见的做法。但要注意控制,改写提示词里要明确“不要添加原文没有的信息”,否则模型自由发挥,改写出来的问题和原意相去甚远。
我实测过一种很有效的折中方案:用轻量规则做第一次改写,把问题规整到“实体+关系+属性”的基本结构;只有当规则改写置信度低时,才调用模型兜底。这样既控制了成本,也避免了每轮对话都走模型带来的改写漂移。
4.3 改写和检索的配合边界
改写不是越“完整”越好。有一次我把用户问题“测试环境登录报错”改写成“测试环境的登录页面出现401身份验证错误”,看着更完整了,结果检索出来的全是关于身份验证机制的原理文档,离用户想要的“报错排查步骤”越来越远。后来我才反应过来,改写补全的是用户缺失的指代和背景,不是替用户扩大语义范围。正确的做法是:改写结果先自检一遍,如果改写答案里出现了原问题没有限定的条件,比如环境、版本、模块,就要谨慎。
这里的另一个心得是:改写后的文本最好保留原始查询作为并列输入,向量检索用改写后的,关键词检索用原始的和改写后的各跑一路,最后一起融合。这样即使改写出了问题,原始关键词那一路还能兜住部分召回。
5. 第五处分水岭:知识库结构化与扩展能力,决定“回答对”还是“答得全”
5.1 元数据:检索过滤的隐形骨架
很多人把知识库理解成“一堆文本切成的向量”,这视角太窄了。真正支撑生产级RAG的是附着在文本上的元数据:文档来源、作者、部门、发布日期、版本号、适用范围、权限标签。这些元数据首先是用来过滤的。没有它们,库里如果混着2021年和2024年的两份制度文件,用户问“最新报销标准是多少”,系统只能靠向量语义硬猜,很容易被旧版本带偏。
更常见的需求是按范围过滤:只搜当前产品线的手册、只搜销售部门的资料、只搜某个项目维度的文档。这必须依赖元数据过滤,而不是靠语义理解去猜。我的习惯是在解析入库阶段就为每个块建立统一的元数据模板,至少要包含source(来源路径)、title(文档标题)、page(页码)、doc_type(文档类型)、department(所属部门)、version(版本号)、timestamp(入库时间)。这些字段就像数据库里的索引,决定了RAG系统能不能处理“精确圈定范围”类查询。
5.2 本体/ontology:从词汇匹配到概念关联
为什么有人开始关注ontology RAG?因为纯向量匹配只能做词汇或者语义相似度,无法表达概念之间的关系。比如“心绞痛”和“冠心病”语义相似度不高,但医学上关系密切;“违约金比例”和“合同解除条件”看起来只共享少数字词,法律场景里却高度相关。向量检索对这种关联无能为力,Ontology则通过显式的概念层级和关系边把它们串起来。
在垂直领域项目里,构建一个最小体量的本体是很有价值的步骤。不一定要做完整知识图谱,先把核心实体类型、实体间关系、同义词表建好,检索时把用户问题里的实体映射到本体节点上,再用关系做一次扩展召回。我做过的一个设备维护知识库里,建立了“设备型号-部件-故障现象-维修操作”的四类本体关系,用户说“主机异响”,系统能关联到“风扇”“轴承”“转轴”这些相关部件,再把这些关联词的检索结果并入候选集,回答覆盖率明显提升。
5.3 图片与多模态内容能不能进知识库
“RAG知识库能存储图片嘛”这个热搜问得相当准。很多业务流程里的知识确实以图片形式存在:截图、流程图、界面示意、设备外观图。我的答案是:能,但要用正确的方式。直接拿图片做检索会面临两个问题——当前主流embedding模型对纯图片并不友好,用户用文字描述图片内容时跨模态匹配极不稳定。
比较务实的处理路径有两种。第一种是图文分离:图片提取后先用本地OCR或多模态模型生成文字描述,描述部分进入文本向量库,图片文件本身存到对象存储里,在回答时引用其路径。第二种是视觉向量化:如果项目预算和资源允许,用支持图片输入的多模态embedding模型单独建一路图片向量索引,与文本向量做多路召回。大部分知识库场景用第一种就够了,因为用户关心的是图片里的信息,而不只是图片本身。把图里的字抽出来、把图示逻辑用文字备份一段,问答体验提升非常直接。
5.4 权限隔离:知识库落地的硬需求
企业内部用RAG,权限隔离躲不开。不同角色的人登录系统,问同一个问题,应该只得到各自权限范围内的资料。这个需求如果在分块阶段不考虑,后期硬做会非常痛苦。
正确做法是在元数据阶段就打好权限标签,检索阶段先按元数据过滤再做语义匹配。千万别想着“模型能理解谁能看什么”——大模型没有权限意识,只能靠检索管道硬隔离。这和水表同理,先圈定范围再在水表范围内计算,才是确定的。权限标签可以是部门、职级、密级中的一种或多种组合,过滤逻辑必须在进入向量检索之前完成,在结果排序之后才做过滤很容易泄露信息。这一点在RAG项目上线评审时几乎一定会被问到,早点设计能省大量返工。
6. 第六处分水岭:评测与优化闭环,没有评估的RAG只能瞎调
6.1 为什么你觉得“差不多”其实根本没法上线
我见过不少团队在RAG项目上的工作方式:搭好流水线,问几个自己拟的问题,看着答案“差不多”,就准备上线了。这个流程的最大问题是,你测的这几个问题恰好命中了你写接口时脑子里预想的场景,而真实用户不会按你的预想来问。没有一套评测体系和基线数据,后续任何优化都无从谈起。你改了分块大小,没法量化说“提升多少”;你说换embedding模型更好,拿不出对比数据。最后只能依赖直觉拍板,团队里每个人都按自己的体感做判断,项目就会陷入无休止的口头争论。
6.2 最小可行评测集的搭建方法
做RAG评测不要一上来追求庞大数据集。我的建议是建一套“黄金评测集”,控制在50到100条左右,包含真实用户问题、期望答案要点、期望命中的参考文档或内容片段。来源可以翻聊天记录、售后工单、内部群提问,凑够各种问法变体。每条问题标注会有几个维度:是否考到精确匹配、是否考到语义改写、是否考到多轮指代、是否考到跨文档关联。这样评测集不仅能量分,还能告诉你哪一类问题最弱。
评测指标上,检索层看命中率(Hit Rate)和MRR,生成层看答案相关性和引用准确率。我见过太多人只盯着生成答案好不好看,不查引用来源,结果模型胡编得丝滑流畅,这恰恰是最危险的。RAG系统的一个基本原则就是:模型回答的话可以信,引用来源必须和回答一致,否则就是幻觉。
6.3 调优顺序:先数据,再检索,最后才轮到提示词
搭好基线之后,调优顺序如果错了,会浪费大量时间。我最推荐的顺序是:
- 第一优先级:解析与数据质量。看看评测集里漏召回的case,是不是原文没解析进来,或者解析丢了表格图片。这一步堵住的是系统性缺口。
- 第二优先级:分块粒度与检索策略。检查召回列表里正确片段排名是否靠后,试试结构分块、父子分块、混合检索、重排。这一步解决的是“正确答案在库里但没捞上来”。
- 第三优先级:查询改写和对话策略。区分单轮问题和多轮指代,针对性加改写。这解决的是“用户提问方式绕过了检索入口”。
- 第四优先级:生成提示词和输出格式。要求模型严格基于给定片段回答,格式上限制“若上下文中没有相关信息,请直接说明不知道”,能极大减少幻觉。
每改一步,都拿黄金评测集跑一遍,记录分数变化。我强烈建议项目从一开始就把基线的评测脚本固化下来,任何配置改动都要求附评测结果。这看起来慢,实际是RAG项目能持续迭代不被推翻的核心保障。
6.4 线上反馈回收与失败case分析
评测集终究是离线快照,线上用户的问题永远会比评测集更刁钻。生产环境一定要设计反馈回收:回答下方放“有帮助/没帮助”按钮是一种方式,但更有效的是记录检索日志,包括用户原始问题、改写后问题、召回的top候选、模型最终答案、用户是否追问等。定期把“没帮助”和“追问”对应的case拉出来和评测集合并,重新分析失败原因。
分析失败case时,不要只看一个孤立问题。把同类型问题聚成一类,比如“都在问同一份旧手册里的参数”“都卡在缩写展开上”“都和某个表格解析错误有关”。这种聚类决定了下一个迭代周期该做数据修复还是检索升级。我常用的做法是拿二三十个失败case,人工标一遍失败环节——是召回没命中、排序靠后、引用错误还是模型没按上下文答,用帕累托图找出占比最高的两类问题,下一轮只集中修这两类。
这套方法论看起来比我那些“三天跑通demo”的教程重,但它对应的回报是系统真正可用、效果可追踪、迭代有方向。尤其在企业知识库场景里,RAG不是做一个演示,而是做一个长期运行、持续更新的生产系统。我个人的体会是:没必要一开始就把六处全做完美,可以先把流水线跑通,再按评测结果逐个强化;但架构和数据模型设计一定要从一开始就给这些能力留好位置——尤其是元数据规范和评测基线,补起来最疼。如果你正卡在“查得到但答不对”或者“看着能用又不敢上线”这类问题,回头逐条对照这六个环节,多半能定位到真正的分水岭在哪儿。