“RAG烂大街”这句话我同意一半。烂大街的是RAG的流水线——加载文档、切分、embedding、塞向量库、TopK检索、拼prompt给大模型,这套东西随便一个框架拖出来,三小时就能跑通demo,Dify里拖拖拽拽也能攒一套,GitHub上几千个star的项目用起来都差不多。但如果你真把一个RAG系统扔到生产环境里,让业务方拿着真实问题来打,你很快就会明白,流水线只是入场券,真正的分水岭全在那六处:文档解析、知识组织、检索质量、上下文构建、评测体系、工程化。这篇文章我不写第八百零一份“RAG入门”,而是把做生产级RAG踩过的坑、验证过的方法、反复改过的配置,一条条拆给你看。
1. 分水岭一:文档解析——同一份PDF,有人读出精华,有人读出乱码
1.1 大部分流水线都倒在了文档解析这一关
很多人以为RAG的第一步是embedding,其实第一步是读文档。你别笑,我见过太多次这样的场面:项目demo跑得好好的,一换真实业务文档,回答质量断崖式下跌。定位问题查了半天,最后发现不是检索不行,不是prompt不行,而是最开始的文档解析就没把内容读对。
PDF这种格式大家最常用,但也最坑。有些PDF看着是文字,实际是扫描图片,没有文本层;有些PDF排版复杂,文字、表格、图片混排;有些是从PPT转出来的,文本框错位、顺序颠倒。PyPDF2这类工具提取出来的不是文档内容,而是“文字碎块”——段落被拆得七零八落,表格变成一串数字和文字挤在一起,引用关系完全丢失。这时候你做切片、做向量化,等于在垃圾堆里淘金,后面的一切优化都是在错误的基础上放大错误。
所以我现在判断一个RAG项目能不能成,第一步不看向量库选型,而是先看它的文档解析链路。文档解析的目标不是把PDF变成文本,而是把PDF变成“结构化程度足够高、语义信息足够完整”的内容。PDF、Word、Markdown、HTML、扫描件、表格、图片,每一种来源都有自己的脾气,统一走一套“文本抽取”必然出事。
1.2 解析方案选型:从PyMuPDF到PaddleOCR再到Marker
先说结论:解析没有银弹,只有按文档类型混着用。
| 工具/方案 | 适用场景 | 优点 | 踩坑点 |
|---|---|---|---|
| PyMuPDF(fitz) | 数字原生PDF,带文本层 | 快、轻、安装方便 | 对复杂版式容易乱序,表格会变成纯文本 |
| PDFPlumber | 需要提取表格结构和坐标信息 | 能拿到字符坐标,适合做规则处理 | 慢,输出需要二次清洗 |
| PaddleOCR / Tesseract | 扫描件、图片型PDF | 解决“没有文本层”的死局 | 识别精度依赖画质,中文小字容易错 |
| Unstructured | 多种格式统一入口,能识别标题、列表 | 调用简单,按文档元素切分 | 处理速度慢,深水区还是要自己调 |
| Marker | 把PDF转成Markdown(含表格、公式) | 输出效果干净,适合喂给LLM | 依赖模型推理,首次运行要拉模型权重 |
我自己的经验是:如果一份文档有文本层,优先用PyMuPDF + PDFPlumber组合,先抽文本再抽表格,用坐标信息把版面拼回来;如果是扫描件,老老实实上PaddleOCR,别指望任何轻量方案能白嫖;如果文档需要高保真转Markdown给大模型看,比如技术手册、论文,Marker效果最稳,但为了速度建议只在离线构建知识库时用。
注意:解析完成后千万要做“人眼抽检”,别信自动化输出的正确率。抽样10页人工看一遍,表格有没有错位、段落有没有断行、标题层级有没有乱,这些直接决定后面切片质量。
1.3 实操:最稳的PDF到Markdown处理路径
我给你一个可以直接抄作业的离线解析组合,以一份混合型PDF为例:
- 先用PyMuPDF做预处理,判断PDF是否带文本层:
import fitz doc = fitz.open("sample.pdf") total_text_len = sum(len(page.get_text()) for page in doc) print("文本层字符数:", total_text_len)字符数为0或者极低,基本可以判断是扫描件,直接跳到PaddleOCR;如果字符数正常,继续走文本层方案。
- 文本层方案里,用PDFPlumber提取表格区域,遇到表格则按行列转为Markdown格式,普通段落保持自然段顺序:
import pdfplumber with pdfplumber.open("sample.pdf") as pdf: for page in pdf.pages: tables = page.extract_tables() text = page.extract_text() # 这里需要自己写规则,把表格块和文本块按坐标切开- 如果是扫描件,装好PaddleOCR后,把PDF逐页渲染成图片再OCR,输出带上坐标的JSON,然后用坐标重建阅读顺序。
这套链路跑下来,复杂版式PDF基本能变成像样的Markdown。核心思路是“按需组合,不要一个工具打天下”。
1.4 这个环节最容易踩的坑
- 千万别对扫描件跑纯文本抽取,出来的内容会让检索系统直接“瞎掉”。
- 表格不能被简单拍平成纯文本,否则“2023年营收 1000万”和“2022年营收 800万”这类关系全丢,语义检索根本匹配不到。
- 切分逻辑一定要和解析逻辑对齐。我见过有人用LangChain的RecursiveCharacterTextSplitter,按字符数硬切,结果一句话从中间被劈成两半,语义彻底断裂。
- 本地文本拆解工具建议选能在CPU上跑的,PDFPlumber和PyMuPDF就够,不一定要上重型OCR服务。
2. 分水岭二:知识组织——从“碎纸机”到“知识网络”
2.1 向量库不是知识库,它只是“内存”
我常跟团队成员说一句话:“向量库是内存,不是硬盘。”你把一堆文本切好、embedding、扔进向量库,这只能叫“可检索的文本仓库”,不叫知识库。因为切片之间没有关系,没有层级,没有来源约束,语义检索只能做“词语层面的相似匹配”,遇到需要逻辑、约束、跨文档归纳的问题就直接歇菜。
举一个真实场景:企业HR知识库里既有《薪酬管理制度》,又有《绩效管理办法》,还有各种年度的补充通知。新人问“今年销售岗的绩效奖金怎么算”,正确答案需要把三份文档里的内容串起来,还要注意“今年”这个时间约束。普通流水线会把三份文档的切片都召回来,然后让LLM自己挑,结果往往把不同年份的政策混在一起答,看似流畅实则错得离谱。
这就是知识组织的价值。你需要在文档进知识库之前,就想清楚:这份文档属于哪个业务域、适用于哪个部门、生效时间是哪年、和哪些文档是配套关系。这些元数据先做好,检索阶段才能用“过滤器”把不相关的切片挡在门外,而不是让LLM在雾里看花。
2.2 Ontology RAG:用实体和关系给检索装上地图
最近热词里“ontology rag”热度很高,它的核心其实不是新引入一个大模型,而是把“知识本体”的概念拿进RAG流程里。简单说,就是在知识库里预先定义好实体类型(员工、部门、岗位、政策、时间)和关系(属于、适用于、生效于、替换),然后把文档切片的元数据挂到这些实体关系上。
这样做的最大好处是把“语义检索”变成了“检索+推理”。用户问“销售部今年出差报销标准变了没有”,系统先通过实体识别确定“销售部”是部门实体,“今年”是时间实体,“报销标准”是政策实体,然后沿着本体关系找到最相关的几张切片,再交给LLM生成答案。
但我不建议一上来就建复杂的知识图谱。体感是:80%的团队连元数据都没做好,直接上图谱会很痛苦,因为图谱构建、维护、对齐的工程成本很高。更务实的路径是分两步走:第一步先把元数据体系建设起来,第二步当文档规模和跨文档关系复杂到一定阈值后,再引入图谱类方案。
2.3 先做好元数据,再谈图谱
元数据怎么设计?我提供一个通用的最小集,你可以按行业扩展:
| 字段 | 含义 | 示例 |
|---|---|---|
| document_id | 文档唯一ID | DOC-2024001 |
| title | 文档标题 | 销售部2024年差旅报销标准 |
| department | 适用部门 | 销售部 |
| doc_type | 文档类型 | 制度/通知/流程/FAQ |
| effective_date | 生效日期 | 2024-01-01 |
| expiry_date | 失效日期 | 2024-12-31 |
| tags | 业务标签 | 报销,差旅,标准 |
这套东西在Dify、LangChain4j里都能直接挂到metadata字段上,检索时用filter先粗筛一遍,效果立竿见影。比单纯靠向量相似度硬扛,响应准确率提升不是一点半点。
2.4 数据飞轮:让知识库越用越准
知识组织还有一个容易被忽略的维度:反馈闭环。RAG系统上线之后,用户的每一次追问、纠错、回答点赞/点踩,都是宝贵的数据。把这些数据捞回来,分析哪些问题经常回答错,错的原因是检索不到还是解析丢失,然后定向修文档、修切片、修元数据,这就是数据飞轮。
很多团队把RAG当成一个“一次性构建、永久使用”的系统,这是最大的认知错误。业务文档每个月都在变,政策会更新,产品会迭代,知识库不维护就是一座逐渐腐烂的资料库。我的建议很朴素:每周固定花时间看bad case,每个月迭代一次文档解析配置。这个小习惯,比任何技术选型都能显著提升回答质量。
3. 分水岭三:检索质量——Query改写、混合召回与Rerank
3.1 问题不在TopK,在于用户根本不按你写的剧本说话
流水线选手最爱的配置是:embedding模型选一个,向量库里TopK=5,然后拼prompt。这个配置不能说是错的,但它默认了一个前提:用户的问题表达方式,和知识库里文档的表达方式是相似的。
现实很骨感。用户会问“你们那个报销新规到底调了多少”,而文档里写的是“根据不同城市级别调整差旅住宿限额标准”。用户会问“被裁员了怎么办”,而文档里根本没有“裁员”这两个字,只有“劳动合同解除”和“经济补偿”。这时纯向量检索基本抓瞎,因为字面匹配度和语义相似度都不理想。
这就是Query改写对绝大多数RAG系统生死攸关的原因。它解决的不是“检索算法不够好”,而是“用户问题和文档语言之间存在系统性的表达鸿沟”。
3.2 Query改写与HyDE:先把用户问题想明白
Query改写的思路非常直白:用大模型把用户的原话,转换成更接近知识库文档风格的检索词。比如:
- 原问:“被裁员了怎么办”
- 改写后:“劳动合同解除流程、经济补偿标准”
- 原问:“报销新规到底调了多少”
- 改写后:“差旅报销标准调整幅度、2024年版本”
实际操作中有一个很有效的技巧叫HyDE(Hypothetical Document Embeddings),思路是先用LLM根据用户问题生成一个“假设的理想答案”,然后用这个假设答案去做向量检索,而不是直接用用户原话去检索。为什么效果好?因为假设答案的用词和知识库文档的用词更接近,embedding空间里的距离更近。
另外还可以做多路Query:一个用户问题,让LLM生成2-3个不同角度的检索词,分别去召回,最后合并结果。这个策略在“用户问题信息量少、但意图跨度大”的场景特别好用。缺点是牺牲一点延迟和成本,建议只在用户query比较模糊时启用。
3.3 混合召回:向量不是万能的,BM25也得要
向量检索擅长语义相似,但不擅长精确匹配。比如文档里有产品型号“A3-2024-CN”,你让向量检索找这个型号,它可能把一堆“A3”“2024”“CN”的词向量搅在一起,精度反而不如最朴素的BM25关键词匹配。
所以我做生产级RAG,几乎从不用单一召回通道,最少也是“向量召回+BM25召回”双通道,最后用RRF(Reciprocal Rank Fusion)合并结果。原理不复杂:两个通道各自返回一个带排名的候选列表,然后按排名倒数加权合并。哪边把真正的答案排得靠前,合并后它的综合排名就会明显领先。
一个最小可用的混合检索配置参考:
# 伪代码风格的双通道召回 vector_results = vector_store.search(query_embedding, top_k=10) bm25_results = bm25_index.search(query_rewritten, top_k=10) fused = {} for rank, doc_id in enumerate(vector_results): fused[doc_id] = fused.get(doc_id, 0) + 1 / (60 + rank + 1) for rank, doc_id in enumerate(bm25_results): fused[doc_id] = fused.get(doc_id, 0) + 1 / (60 + rank + 1) final_order = sorted(fused, key=fused.get, reverse=True)RRF的常数60是经验值,影响不大,你可以自己在验证集上调。重要的是把“语义”和“字面”两条腿都立起来。
3.4 Rerank:生成之前的最后一道闸门
双通道召回之后,TopN里面依然混杂着大量“看着相关、实则不对”的切片。这一步通常用一个Rerank模型来精排,常见的有BGE-Reranker、Cohere Rerank这些。Rerank和向量检索的区别在于,向量检索是“全局匹配”,Rerank是“用户query和每个候选切片做强交互匹配”,精度高一个档次。
实操上,我习惯先召回50到100条候选,用Rerank取前5到10条进上下文。这一步能让最终生成质量稳定提升,代价是增加几十到几百毫秒的计算时间。如果你的系统对延迟极其敏感,至少也要做到“不加Rerank就是裸奔跑生产”的觉悟。
3.5 一个可供复制的检索配置参考
- 召回阶段:向量召回(使用bge-m3或text-embedding-3-small)+ BM25召回,分别取20-50条
- 合并阶段:RRF合并
- 精排阶段:Rerank模型取前3-10条
- Query改写:糟糕提问/指代不清时启用HyDE或多路改写
- 元数据过滤:department、effective_date等字段先行过滤
这套配置跑下来的效果,和“直接TopK=5”的流水线相比,在同一条评测集上答案相关性能提高20个点以上,这不是夸张,是实测。
4. 分水岭四:上下文构建——信息密度比“塞得更多”重要
4.1 长上下文掩盖了RAG生成环节的瓶颈
现在很多大模型支持128K甚至更长的上下文,于是有人想:那我干脆把召回的一堆片段全塞进去,让模型自己挑。听起来很省事,但实际效果并不好。业界对这个现象有一个比较形象的说法叫“Lost in the Middle”——模型对于长上下文中间位置的信息,注意力权重会明显下降。你塞得越多,关键信息处在“注意力盲区”的概率就越大。
而且上下文越长,Token成本越高、生成延迟越长、幻觉风险越大。RAG生成环节真正的瓶颈不是“塞不下”,而是“塞进去了但模型没看见”。所以上下文构建的核心不是“尽量多塞”,而是“尽量精准”。
4.2 上下文压缩三件套:剪枝、摘要、去重
我在生产项目里常用的上下文组织策略有三个:
第一是剪枝。对每条召回片段,不是整段给LLM,而是提炼出和用户问题最相关的从句或句子组。这一步可以用LLM做一次轻量提取,也可以用规则匹配。别小看这个操作,上下文中无关信息越少,模型被带偏的概率就越低。
第二是摘要。如果一个召回片段本身很长,但它承载的关键信息分散在多个段落,与其把它完整塞进去,不如让LLM先“针对用户问题做一个100字以内的摘要”。代价是多一次小模型调用,但换来的是上下文更紧凑、答案更聚焦。
第三是去重。双通道召回很容易产生高度重叠的片段。你在拼上下文之前先做一次相似度过滤或者BM25得分合并,把重复内容去掉。否则模型看到三遍几乎一样的内容,容易对这段内容“过度自信”,反而忽略其他角度的信息。
4.3 知识库能存图片吗?能,但要看你的模型怎么读
这个热词问题很典型:“RAG知识库能存图片吗?”答案是能,但要分情况。
情况一:图片本身是答案主体。比如用户问“这个产品的结构图是什么样的”,你指望文本描述没用,必须把图片本身交给多模态大模型才能回答。这时你的解析链路就要保留图片,切片时把“图片文件路径/图片内容”作为一个独立的知识单元,检索命中后直接传给支持视觉的模型。
情况二:图片只是文本的配图。比如一份政策文件里有个组织结构图,但文字已经把关系写清楚了。这种场景没必要求多模态,把图片转成文字摘要(OCR+人工标注图注)塞进文本切片就行,成本低、效果好。
很多团队在这事上犯的错是“一刀切”:要么所有图片都不存,要么所有图片都存。合理做法是在解析阶段对图片做分类和可读性判断,再决定走哪条路。本地做多模态检索的工程成本不低,非必要不建议盲目上。
4.4 引用溯源:让答案可以被核验
上下文构建还有一个很容易被忽略的维度:引用溯源。生产场景里,业务方不会因为模型“回答得流畅”就信任它,他们要看到“这句话出自哪份文档的哪一页”。所以每一条进上下文的切片,都必须带上doc_id、页码、原文位置。
这样有两个好处。一是用户能自行核验,对错一目了然,减少无意义的争论;二是当答案出错时,你能快速回溯是检索环节还是生成环节的问题。我见过不少团队上线RAG后回答错了都不知道往哪查,就是因为没做引用溯源,整个系统像个黑盒。
5. 分水岭五:评测体系——没有评测集的RAG,都是玄学
5.1 RAG“改一版感觉好了”是最危险的信号
有些团队优化RAG的方式是“凭感觉”:今天换了个embedding模型,觉得回答好像更顺了;明天调了下prompt,感觉引用好像更准了。但你要问他“顺了多少”“准了多少”,他答不上来。
这非常危险。RAG系统是一个多环节流水线,任何一个环节改动都可能让一部分问题变好、另一部分问题变差。没有评测集,你根本无法判断一次改动到底是在进步还是退步,最终只能陷入“谁嗓门大谁说了算”的泥潭。
5.2 三个核心指标:忠实度、答案相关性、上下文相关性
这里我建议所有团队先建立三个底线指标,用RAGAS这类开源框架或LLM-as-judge都可以,指标口径要保持一致:
| 指标 | 英文名 | 一句话解释 | 对应哪个环节 |
|---|---|---|---|
| 忠实度 | Faithfulness | 答案是否完全基于上下文,有没有幻觉 | 生成环节 |
| 答案相关性 | Answer Relevancy | 答案有没有针对用户问题,别答非所问 | 生成环节 |
| 上下文相关性 | Context Relevance | 召回的上下文里有没有包含答案所需信息 | 检索环节 |
如果你连这三个指标都没跑过,那优化RAG就是闭眼开车。上下文相关性低,说明检索/知识组织有瓶颈;忠实度低,说明prompt或上下文组织有瓶颈;答案相关性低,可能是Query理解、可能是生成策略。指标先帮你定位“故障域”,再谈具体优化。
5.3 实操:一个月内搭起最小评测集
不用追求评测集规模多大,我建议从30到50条真实的用户问题开始,一个月内就能建起来:
- 从线上日志捞真实用户问题,挑高频的和典型的bad case,别自己凭空编问题。
- 对每个问题标注标准答案,可以从权威文档里抄,也可以让业务专家写。
- 跑评测时,把RAG对每个问题的“回答、召回上下文、引用来源”三个输出全部记录下来。
- 用LLM-as-judge批量打分,人工抽检20%保证打分可靠。
这30到50条问题就是你的“底线防线”。以后任何改动,先跑一遍评测集,指标不下滑才准上线,指标提升了就记录成优化经验。长期坚持下去,这套评测集就是团队最宝贵的资产之一。
5.4 离线评测与在线反馈的口径对齐
离线评测做得再好,也不能代替在线的真实反馈。我建议在系统里埋点,记录用户追问、点赞、点踩、复制行为。离线评测告诉你“历史上最好的版本是哪个”,在线反馈告诉你“当前版本在真实用户手里表现如何”。两者口径必须一致,否则你离线优化了半天,上线效果还是玄学。
这里有一个小技巧:定期把在线新产生的bad case,回流到离线评测集里。这样离线评测集就会越来越贴近真实业务,形成一套不断生长的“真问题库”。
6. 分水岭六:工程化与规模化——从能跑Demo到能跑生产
6.1 延迟和成本:先算一笔账
很多人做RAG demo的时候根本不关心延迟,因为数据量小、并发低。一到生产环境,第一个问号就来了:一次完整的RAG请求,链路里有多少次网络调用、多少次模型推理?
我粗略拆过一条请求:Query改写一次LLM调用、向量检索一次、BM25一次、Rerank一次、生成一次。如果每步都走LLM或者大模型,总延迟轻松上10秒,成本也高得让你怀疑人生。所以生产环境的第一个原则是:能用规则解决的,别动用模型;能用小模型解决的,别动用大模型。
比如Query改写不一定每次都要LLM来做。很多高频问题可以走模板匹配、关键词规则,命中规则就直接走固定检索流程,命不中再调LLM兜底。
6.2 缓存是RAG的第一步优化
RAG生产优化里性价比最高的永远是缓存,没有之一。同样的用户问题,为什么每次都要重新检索、重新生成?把“问题-答案-引用来源”缓存起来,命中后直接返回,延迟能降到原来的十分之一。
缓存分两层。第一层是精确缓存,用户问题完全一样就直接返回;第二层是语义缓存,把新问题和缓存里的历史问题做相似度匹配,相似超过阈值就复用答案。第二层效果更好,但要注意误命中风险,两个看似相似的问题答案可能截然相反。
6.3 RAG智能体:确定性优先,Agent化要克制
“RAG智能体”最近很火,很多团队想一步到位做成自动规划、自动调工具、自动多轮追问的Agent。我的态度很明确:先从确定性流程做起,Agent化要克制。
原因很简单,Agent的自由度越高,行为越不可控。生产系统里答案错了还可以通过引用溯源推责,但如果检索路径本身就是Agent临时“编”出来的,你连排查入口都找不到。我建议先跑固定的RAG流水线,把文档解析、Query改写、检索、重排、生成每一环都做成可观测、可回放的服务;等流程跑顺了,再逐步给特定的问题类型开放Agent能力,比如复杂跨文档推理问题,才值得用Agent去多步规划。
6.4 可观测性:每一个回答都要有据可查
生产RAG系统的可观测性,不是看服务器CPU、内存就算完事,而是要能回答这几个灵魂拷问:
- 用户这个问题,命中了哪几条切片?
- 这几条切片分别来自哪份文档的哪一段?
- 重排后哪些切片进了上下文?
- 大模型生成答案时,prompt里到底有没有包含原始资料?
- 用户最后对这个答案是点了赞还是踩了?
我把这些信息全量打到日志里,存成可检索的结构化记录。出现bad case时,翻出日志还原全过程,用不了五分钟就能定位是哪个环节出了问题。没有这套可观测性,RAG系统就是一台失控的提词机器。
6.5 RAG与微调的正确分工
最后聊一个绕不开的话题:有些知识高频、稳定、且对表达风格有要求,是不是直接微调模型更合适?
我的经验是:RAG管“动态知识”,微调管“稳定能力”。业务政策、产品文档、新闻资讯这类随时会变的内容,放RAG是最佳选择,因为更新知识库比重新训练模型便宜太多;而像“客服回复风格”“合同条款的固定表达”“一个企业内部的行话术语”这类长期稳定、需要模型牢牢记住的东西,微调的效果确实更好。两者不是互斥方案,生产系统完全可以“RAG+微调”共存:一个管实时检索,一个管底层能力。
说到底,RAG流水线只是脚手架,真正决定系统上限的,是脚手架背后这六项工程细节。它们不性感,甚至有点脏活累活的味道,但每一处都值得花时间打磨。
如果让我给一个最朴素的建议:别急着追最新模型、最新框架,先把自己手上这条流水线从头到尾走一遍,看看文档解析有没有丢信息,元数据有没有建好,检索召回是不是只有单通道,上下文是不是一股脑全塞,有没有评测集,有没有日志可查。把这些问题一个个补上,你会看到一个肉眼可见的质变。按我个人这几年的体会,这六处分水岭没有一项是能靠“换个大模型”一步到位的,但每补上一个坑,你的RAG系统才真正向“生产可用”前进了一步。