
RAG 从入门到实战三PDF 解析与文本分块完全指南我前两篇聊了 RAG 的整体架构和向量化流程很多朋友看完后反馈说“原理懂了一到处理真实文档就卡住”。尤其是 PDF这玩意看着通用实际处理起来全是坑有的页面是文字层有的本质是图片有的表格混着多栏排版还有一堆扫描件等着你做 OCR。如果你正卡在这一步那这篇文章就是为你准备的。这篇我用一个完整的实战视角把 PDF 解析和文本分块这两件事彻底讲透。你会搞清楚为什么解析和分块的质量直接决定 RAG 效果的上限主流的 PDF 解析方案各自适合什么场景分块策略里 chunk size 和 overlap 到底怎么调以及我踩过的一些坑和对应的排查思路。所有代码示例都是可以直接拿去改的不是 PPT 级别的那种。1. 内容整体设计与思路拆解1.1 为什么“数据入口”决定了 RAG 的天花板先说一个很多人容易忽略的事实RAG 的上限由“检索质量”决定而检索质量的上限又由“索引数据质量”决定。你后面用多好的 embedding 模型、多强的重排模型都救不回一个在源头就切碎、切错、丢信息的文档。我见过太多项目在模型选型和向量库调参上花了大把时间最后效果不行一排查发现是 PDF 解析阶段出了问题——要么文字全挤在一起要么标题层级全丢了要么表格被切得七零八落。这些问题不会在 parsing 阶段报错它们是“静默失败”直到你查 answer 质量时才暴露出来。所以做 RAG第一步不是急着调模型而是先把文档解析和分块这层地基打牢。这个环节做的事情可以拆成四个递进层次内容提取完整性文字有没有缺漏、乱码页眉页脚有没有干扰正文表格内容是否保留。结构信息保留标题、段落、列表、表格的层级关系有没有被带下来这决定了后续分块能不能“按结构切”。语义边界还原不同主题的段落是否能自然分开而不是被硬生生拼在一个块里。可控性与稳定性同样的解析方案换一批文档结果是否稳定异常文件不会让整个 pipeline 崩溃。这四个层次前两个靠解析方案解决后两个靠分块策略解决。很多教程把两者混在一起讲实际工程中它们各有各的难点需要分开来设计和调优。1.2 解析和分块在整个 RAG 流程中的位置我习惯把 RAG 的离线数据构建流程画成一条流水线文档加载 → 格式解析 → 清洗与结构化 → 分块 → 向量化 → 入库。PDF 解析发生在“格式解析”这一步文本分块发生在“分块”这一步。两者看起来是相邻环节实际上有强依赖关系如果你解析阶段把文档结构破坏了分块阶段再怎么优化都只能“在垃圾上做精细切分”。反过来如果解析做得很好但分块策略粗糙比如一刀切固定 500 字那么再好的结构信息也会在切分时被浪费掉。所以这篇的实战思路是先把“解析”和“分块”当成一个整体来看待先确定你要处理什么类型的 PDF再决定用什么策略把解析结果变成适合 embedding 的文本块。这个顺序不能反很多人一上来就选 chunk size却对自己文档的类型和复杂度完全没有概念。1.3 选型背后的核心考量没有“最好”的方案只有“最合适”PDF 解析这个领域不存在一个通用的银弹方案。你选什么工具取决于你的文档从哪来、长什么样、要拿来做多高精度的检索。我自己的经验是先把文档分类再决定工具纯文本型 PDF比如论文预印本、网页导出的 PDF文件体积小复制文字无乱码这类用轻量库就能处理。复杂排版型 PDF比如公司年报、产品手册虽然带文字层但分栏、表格、页眉页脚混在一起轻量库处理效果很差需要用带版面分析的解析器。扫描件/图片型 PDF比如老书扫描版、传真件根本没有文字层只能 OCR。技术图纸/专业软件导出 PDF比如 CAD 导出、3GPP 协议文档这类往往包含大量特殊符号、公式或者固定排版需要专业方案或者自定义后处理。在后面的章节里我会分别给出这些场景的解析方案以及我自己用下来的真实感受。记住一个原则先用最便宜的方式探测文档类型再决定上什么方案不要一上来就堆重型工具。2. PDF 解析核心细节与实操要点2.1 纯文本型 PDF轻量库够用但要注意“伪文本”对于最常见的“从 Word 或 LaTeX 导出的 PDF”文字层是完整的直接用 Python 的几个轻量库就能提取。我最常用的三个PyPDF2 / pypdf、pdfplumber、PyMuPDFfitz。它们各有特点我列个表方便你选型。库提取速度版面还原度表格支持适用场景pypdfPyPDF2 继任者快差纯按流式文本无快速抽取文字不关心排版pdfplumber较慢较好基于坐标定位支持表格线检测需要提取表格或关注位置关系PyMuPDFfitz非常快较好可以拿块坐标一般需手写大批量高性能解析按块提取这三者我都用过分别说下实际体验。pypdf 的提取结果是一大段连续文本基本丢弃了版面信息。它的优点是代码简单一个 extract_text() 就能拿到全文。但遇到多栏排版会出大问题——两栏的文字会被交叉拼在一起左边一栏读到一半跳到右边一栏语义全乱。如果你只是处理单栏、顺序明确的文档pypdf 是性价比最高的选择。pdfplumber 是我最常用的定位工具。它的特点是“知道每个字符在哪里”可以拿到每个词的 x0、x1、top、bottom 坐标所以你可以非常精准地按坐标区域切文本。比如只提取页面左上角的正文区域跳过页眉页脚。劣势是速度慢处理一个几百页的 PDF 可能要等好几分钟不适合大规模批量场景。PyMuPDF 是我在大批量场景下的主力。它的 get_text(“blocks”) 可以直接返回文本块的坐标和内容比 pdfplumber 快一个数量级而且保留了一定的版面顺序。如果你需要“速度和结构”的平衡PyMuPDF 是目前综合体验最好的轻量方案。注意即使是纯文本型 PDF也有一部分是“伪文本”。比如某些在线工具生成的 PDF文字层其实是曲线路径复制出来是乱码或空字符串。这时候你要么用 OCR 兜底要么确认来源后走扫描件方案。2.2 复杂排版 PDF版面分析才是真正的分水岭如果你处理的文档带多栏、表格、标题层级、页眉页脚那上面这些轻量库就不够用了。原因在于它们只负责“把文字抠出来”不负责“理解版面结构”。而 RAG 分块恰恰需要结构没有结构就只能瞎切。复杂排版文档的正确打开方式是“版面分析 阅读顺序还原”。目前比较成熟的方案有几类基于规则的方案用坐标、字体大小、间距等特征判断标题和正文。比如字体大于正文两倍且居中的块判为一级标题。优点是可控、无需 GPU缺点是规则写起来累换一种版式就要调参。基于深度学习版面检测的方案如 LayoutLMv3、DocLayout-YOLO、PP-StructureV2 等模型可以识别标题、段落、表格、图片、页眉页脚等区域再按区域重组阅读顺序。效果明显更好但需要一点模型部署经验。商业/开源一站式解析库如 PyMuPDF 的布局分析、Unstructured、MinerU前身是 OpenDataLab 的 PDF 解析工具开箱即用程度更高。我现在的推荐顺序是先试 MinerU 或 Unstructured因为它们把版面检测、公式识别、表格转 Markdown 都封装好了省心如果效果不满意再上 PP-StructureV2 做定制训练。尽量不要一上来就自己写规则除非你的文档版式非常固定。这一期因为是“从入门到实战”我给一个可以直接落地的方案用 PyMuPDF pdfplumber 做轻量级版面分析识别标题和正文分块然后把结构化的文本块交给后续分块逻辑。深度学习方案的部署细节后面单独开篇讲这里先把基础链路打通。2.3 扫描件与 OCR解析的最终兜底手段扫描件没有文字层只能走 OCR。目前主流的 OCR 方案Tesseract老牌开源免费中文效果一般需要自己训练或调参。PaddleOCR百度开源中英文效果都很好同时提供版面分析能力是目前中文场景的首选。商业 OCR如云厂商 OCR效果好省心但涉及 API 费用和数据外发敏感数据慎用。PaddleOCR 是我目前的主力。它不仅识别文字还能输出每个文本框的坐标和置信度。你可以同时用它做版面分析识别标题、段落、表格区域。下面给一段最基础 PaddleOCR 的用法用于快速摸底扫描件质量。from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) result ocr.ocr(scan_page.png, clsTrue) for idx, line in enumerate(result[0]): box line[0] # 四个角坐标 text line[1][0] # 识别结果 conf line[1][1] # 置信度 print(idx, text, conf)这段代码会打印每行识别的文字、坐标框和置信度。OCR 结果天然是带坐标的这意味着你可以拿到每个文本块的物理位置非常方便后续做版面重组。实操心得OCR 不是万能的遇到印章遮挡、强底色背景、手写批注识别率会明显下降。我的经验是先跑一遍 OCR按置信度过滤掉低分结果再人工抽查 10% 的页面来估算整份文档的可信度。如果一份文档的 OCR 置信度中位数低于 0.85就要考虑换更高精度的模型或者对源文档做去噪、纠偏预处理。2.4 解析后必做的清洗和结构化处理不管用哪种方式解析 PDF得到的原始结果都不能直接拿去分块。解析出来的是“文本”不是“干净的结构化数据”。我总结了一套清洗流水线你可以按顺序执行去页眉页脚大多数 PDF 的页眉页脚每页重复如果不去除会被向量化成大量重复块干扰检索。去页码页码本身没有语义但经常被拼在正文文本里需要按位置或正则剔除。合并断行PDF 每行文字可能被硬换行截断需要根据行尾是否标点、首字母是否大写等规则把同一段落的断行合并。规范化空白字符全角半角、多个空格、制表符统一处理。保留结构化标记如果解析器能输出标题层级、列表、表格的 Markdown 标记尽量保留这些标记对后续分块很有用。清洗这一步看起来不起眼但直接影响分块质量。同一个页面提取出来的可能是“一段顺滑的正文”也可能是“一堆被硬换行切碎的碎片”。分块的逻辑在下游接的可是 embedding 模型喂给它的文本语义越连贯向量表达越准确。3. 实操过程与核心环节实现完整跑通解析 分块3.1 环境准备与技术选型为了让你有一份“拿来即用”的参考我这一节用一套完整代码把“PDF 解析 → 文本清洗 → 结构化 → 分块 → 输出”整条链路串起来。技术栈如下PyMuPDF负责快速提取文本块和基础结构信息pdfplumber负责局部细节提取比如表格和大段落LangChain 的 RecursiveCharacterTextSplitter负责文本分块我们也可以用自定义分块后面细说简单的 Python 正则和规则逻辑负责清洗和标题识别安装依赖只需要两行命令pip install pymupdf pdfplumber langchain-text-splitters pip install paddleocr paddlepaddle # 如果你需要 OCR 兜底注意paddleocr 的依赖比较重建议放在独立环境里不要和主项目环境混在一起否则容易产生版本冲突。3.2 基于 PyMuPDF 的 PDF 解析与结构提取先实现最基础的解析函数。我们的目标是把 PDF 变成每页的“文本块列表”每个块包含 text、bbox 坐标、块类型。import fitz # PyMuPDF def extract_blocks_from_pdf(pdf_path: str): doc fitz.open(pdf_path) page_blocks [] for page_num in range(len(doc)): page doc[page_num] blocks page.get_text(blocks) page_blocks.append({ page: page_num 1, blocks: blocks }) doc.close() return page_blocks这里返回的 blocks 是元组列表每个元组包含四个角的坐标、文本内容、块 ID 等。你先跑一遍这个函数看看输出的块数量、每块的字数是否均匀。这一步极其重要我见过很多同学直接跳过“摸底”一上来就用重型方案最后花了一堆时间处理一个根本不需要重型方案的文件。摸底之后你需要做一个关键的判断这个 PDF 的正文是不是“按块分好”的如果每个块就是一个自然段落后续分块会非常轻松如果块和块之间是乱序的或者一个段落被切成几十个小块那你需要进一步处理“块合并”。3.3 按坐标过滤页眉页脚与噪声块页眉页脚、页码、水印都带有固定特征。最简单的方式是直接按页面区域的坐标过滤。比如 A4 页面595 × 842 点数页眉通常出现在 top 50 的区域页脚通常出现在 bottom 800 的区域。def filter_header_footer(blocks, page_height842): filtered [] for b in blocks: x0, y0, x1, y1, text, block_id, block_type b # 忽略太靠上和太靠下的区域 if y0 50 or y1 page_height - 50: continue # 忽略纯数字的页码 if text.strip().isdigit(): continue filtered.append(b) return filtered这种粗暴规则在版式固定的文档里非常有效。但注意如果你的文档正文确实有内容在页眉页脚区域比如表格的第一行这套规则会误杀。更稳妥的做法是对同一个 PDF 的前 10 页做统计找出每页相同位置重复出现的文本把它们统一标记为“重复区域”然后过滤。这种方法叫“跨页去重”我推荐你在生产环境里实现一下。3.4 断行合并与段落重建把碎片粘成完整语义单元PDF 提取出来的正文经常长这样这是一个关于文本分块的示例。 文本分块是 RAG 构建中的核心环节 它直接影响向量检索的准确率。但正常阅读时这三行应该是同一段话。断行合并的规则我总结成了四步如果当前行末尾不是句号、感叹号、问号、冒号等终止符并且下一行不是明显的标题字体大小、段前距离判断并且下一行首字母不是大写英文场景那么把当前行和下一行合并。中文场景更简单因为中文句末标点非常明确。你只需要判断行尾有没有句末标点没有就继续拼下一行。import re def merge_lines(text: str) - str: lines text.split(\n) merged [] buffer for line in lines: stripped line.strip() if not stripped: continue if buffer and not re.search(r[。.!?;:]$, buffer): buffer stripped else: if buffer: merged.append(buffer) buffer stripped if buffer: merged.append(buffer) return \n.join(merged)这段代码维护了一个 buffer当 buffer 以句末标点结束时就“结算”成一段否则继续累加下一行。如果你处理的文档里有些“标题行”也希望合并需要再加判断条件比如下一行不是独立标题块。3.5 识别标题层级让你的分块“按结构切”而不是“按字数切”在合并段落之后下一个目标是识别标题。标题识别的规则有很多我根据落地难易度排序给你参考基于字体大小PyMuPDF 可以拿 span 的 size 属性。正文大小设为基准字体更大的判为标题。基于字符特征中文标题通常是短文本不超过 50 字且不带句末标点。基于位置特征标题通常有更大的段前距离或者居中/靠左。基于编号模式形如“1.”、“1.1”、“第一章”、“一、“二、”的文本优先判定为标题。一个可落地的简化实现是用 PyMuPDF 的 dict 模式然后基于字号分组def extract_headings(pdf_path, base_sizeNone): doc fitz.open(pdf_path) headings [] for page_num in range(len(doc)): page doc[page_num] d page.get_text(dict) for block in d[blocks]: for line in block.get(lines, []): for span in line[spans]: text span[text].strip() size span[size] font span[font] if not text: continue if base_size is None: base_size size # 第一个 span 的 size 作为基准 if size base_size * 1.2 and len(text) 50: headings.append({ page: page_num 1, level: h1 if size base_size * 1.5 else h2, text: text, size: size, font: font }) doc.close() return headings这段代码用“字号大于正文 1.2 倍”作为标题判据。实际使用中你还可以结合字体是否加粗font 名称里常带 Bold 字样来提升精度。识别出标题后每个标题和它之后的内容可以作为一个“语义章节”在分块时优先按章节边界切割。实操心得标题识别不是让 RAG 效果变好的“必需项”但它是让 RAG 效果变好的“加速项”。如果你切出的每个块都正好落在一个标题下面embedding 的语义表达会干净很多。后面接重排或者摘要式检索时效果提升会非常明显。3.6 文本分块策略从固定大小到结构感知终于到了分块环节。我见到的最常见的错误就是一上来就问“chunk size 选 300 还是 500”——这个问题本身没有标准答案因为你还没搞清楚你的文档和检索场景。分块的目标是让每个块在语义上是内聚的在检索时是容易被命中的。基于这个目标我建议你先弄清楚三件事你后续用的 embedding 模型支持多大的输入 token超了会被截断信息就丢了。你的用户提问一般是多长、多具体问的是大主题还是小细节如果没有理想的“章节边界”你的文本可以切到哪里而不破坏语义大部分情况下我推荐用“重叠窗口 结构感知边界”的组合策略而不是简单的固定长度硬切。LangChain 的 RecursiveCharacterTextSplitter 就是这么设计的它按分隔符优先级递归切分保住了代码和段落结构。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , ], length_functionlen, ) chunks text_splitter.split_text(merged_text) print(len(chunks))这段代码里有两个关键参数chunk_size每个块的目标长度我这里用字符数而不是 token 数中文场景更直观。如果按 token 数算通常一个中文汉字约等于 1 到 1.5 个 token你可以用 tiktoken 做精确统计。chunk_overlap相邻块的重叠长度。作用是为了防止“刚好在分块边界上的关键信息”被切断后丢失。80 字符通常是一个还不错的起点需要根据文本特性调整。分隔符数组的设计也很有讲究先按“段落”切\n\n再按“行”切\n再按“句号”切最后如果还是太长才硬切。这种优先级保证了分块边界尽量落在语义完整的位置上。3.7 分块参数怎么调一个可复用的实验方法很多教程喜欢给一个“推荐参数”然后就没有然后了。这种推荐只对特定语料有效。我建议你自己搭一个非常简单的实验流程来调参挑出 30 个代表性问题和对应答案。用当前分块参数构建向量库跑一遍检索记录每个问题是否有正确答案出现在 Top 5。改动 chunk_size 和 chunk_overlap重新构建索引重复评测。记录不同参数组合的命中率选效果最好的那组。这个流程看起来简单但非常有效。注意调参时要固定 embedding 和重排模型不变否则你无法判断是“分块变好”还是“模型变好”。我自己常用的一个起点是chunk_size 500chunk_overlap 80separators 按上述顺序。对于技术文档、协议文档这类段落边界清晰的文档这个组合通常能拿到不错的效果。如果文档是碎片化的短段落聚合我会把 chunk_size 降到 250如果是长论述类的书本章节我会提高到 800。注意chunk_size 和 chunk_overlap 一旦太大相邻块之间的重复内容过多会造成向量库里面大量近重复向量检索时多个几乎一样的块同时命中浪费上下文窗口甚至降低回答精度。一般 overlap 不要超过 chunk_size 的 20%。3.8 结构化文档的进阶分块方案如果你的文档是 HTML 转 PDF、Markdown 导出 PDF或者你用了 MinerU 这类输出 Markdown 的解析器那么你可以用“按标题层级切块”的方式比纯文本递归切分更精准。思路很简单先按一级标题把文档切成多个章节如果章节长度超过 chunk_size再在二级标题处继续切以此类推。如果没有子标题了才回退到递归文本切分。这种“结构优先”的分块法能把“学术论文”这种强层级文档的检索质量提升一大截。def split_by_headings(markdown_text, max_chunk_size500): sections [] current_section [] current_heading None for line in markdown_text.splitlines(): if line.startswith(#): if current_heading is not None: sections.append({ heading: current_heading, content: \n.join(current_section) }) current_heading line current_section [] else: current_section.append(line) if current_heading is not None: sections.append({ heading: current_heading, content: \n.join(current_section) }) # 超长 section 再递归切 chunks [] for sec in sections: content sec[content] if len(content) max_chunk_size: sub_splitter RecursiveCharacterTextSplitter( chunk_sizemax_chunk_size, chunk_overlap50, separators[\n\n, \n, 。, , , ] ) sub_chunks sub_splitter.split_text(content) for i, sub in enumerate(sub_chunks): chunks.append(f{sec[heading]}\n{sub}) else: if content.strip(): chunks.append(f{sec[heading]}\n{content}) return chunks这段代码把标题作为块的“锚点”每个块都以标题开头这样检索到块时上下文信息会丰富得多。你可能觉得“标题内容重复”会不会浪费 token但实际效果表明信息冗余的价值远大于 token 开销尤其在做 Top-K 召回时。3.9 一个完整的 RAG 定向流程示例我把上面的模块串成一个最小可运行的流程方便你照着改import fitz import re from langchain_text_splitters import RecursiveCharacterTextSplitter def pdf_to_chunks(pdf_path, chunk_size500, chunk_overlap80): # 1. 提取文本块 doc fitz.open(pdf_path) all_text [] for page in doc: blocks page.get_text(blocks) for b in blocks: x0, y0, x1, y1, text, _, _ b # 2. 过滤页眉页脚和页码 if y0 50 or y1 792 - 50: continue if text.strip().isdigit(): continue all_text.append(text.strip()) doc.close() # 3. 合并断行 full_text \n.join(all_text) full_text merge_lines(full_text) # 4. 递归分块 splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, separators[\n\n, \n, 。, , , , , ], length_functionlen, ) chunks splitter.split_text(full_text) return chunks chunks pdf_to_chunks(sample.pdf) for i, c in enumerate(chunks[:5]): print(f--- chunk {i} ---) print(c[:200])这个流程可以直接跑通“解析 → 清洗 → 分块”的完整链路。你可以在此基础上继续叠加标题识别、表格提取、OCR 兜底等模块。4. 常见问题与排查技巧实录4.1 PDF 解析出来的文本乱序或乱码怎么处理乱序通常是多栏排版造成的。pypdf 这类流式提取工具无法识别栏会把左栏和右栏交叉拼在一起。我的排查步骤先用 pdfplumber 按页面坐标提取文本看左右两栏的坐标分布是否明显分离。通常左栏的 x1 和右栏的 x0 之间会有明显的间隙。按坐标把页面切成左右两个区域分别提取再按需要的顺序拼接。如果你的文档复杂度高直接用 MinerU 这类带版面分析的解析器一步到位。乱码问题大多是编码不识别或字体子集化导致。你可以尝试换 PyMuPDF 试试它内置的字符映射比 pypdf 全如果还不行说明 PDF 字体映射有问题只能 OCR 兜底。4.2 表格被切得七零八落如何保留表格语义表格是 RAG 检索的难点尤其是复杂表头跨行跨列的表格。我的经验是表格优先走“整体识别”而不是“按行切分”。具体操作用 pdfplumber 的 extract_table() 提取表格结构如果表格行列规则效果很好。把表格转换成 Markdown 格式| 列1 | 列2 |这样语义信息比纯文本更好。如果表格太大无法整体嵌入一个 chunk至少保证“一行记录”不被切到两个块里。这里我通常会用“先表格转 Markdown再按表格行递归切分”的策略。PaddleOCR 的 PP-StructureV2 也能直接输出表格的 HTML适合扫描件表格。我踩过的坑是把表格按普通文本切分了结果检索时“第三列内容”和“表头”被分到不同块里导致召回结果虽然命中行内容但缺失列名大模型回答时只能靠猜。所以表格一定要做结构化处理至少保留表头信息。4.3 分块后检索命中率低问题可能不在分块如果调了很多分块参数召回还是差先把问题从“分块”转移出去。我建议按以下顺序排查确认提问和文档的语言是否一致中文文档用英文 embedding 模型效果差是正常的。确认每个块的语义是否完整打印几个块肉眼看看是否出现了“一句话被切掉一半”的情况。确认向量库的 top_k 是否太小比如 top_k3而正确答案排在第 5你会误判为分块问题。确认是否有重复文本块页眉页脚没去掉的话会制造大量重复块挤占检索名额。排查完这些再回头看分块参数不要一上来就调 overlap。4.4 处理超大 PDF 时内存和性能问题几个 G 的 PDF 直接用 PyMuPDF 全量加载会把内存打爆。我的做法是分页流式处理读取一页处理一页只保留最终 chunk 结果。另外可以考虑用doc.subset()或者直接把 PDF 按页拆分成临时小 PDF再并行处理最后合并 chunk 列表。并行处理时注意不要把“同一个文档的切分逻辑”直接并行因为文档内页与页之间有上下文关联需要保留顺序信息。我在每一页提取时会给文本块加上页码元数据这样后续做重排或者检索溯源时能定位到具体页码。4.5 如何验证解析和分块质量三个快速指标回到开头的问题怎么知道解析和分块做得好不好我推荐三个快速指标你可以定期跑一下指标含义简单评测方法文本覆盖率提取文本字符数 / 原始文档预估字符数对比提取结果和原文档页数正常应在 90% 以上块间重复率相似块的数量占比用 embedding 算块之间相似度超过 0.95 的块删掉一个Top-K 命中率标准问题是否命中在检索结果前 K 条构建 20-30 个问答对跑检索看 Top-5 命中率这三个指标不用做得很复杂跑一次半小时之内能完成。效果不好时优先看“文本覆盖率”和“块间重复率”一般能快速定位问题是出在解析还是分块。5. 额外经验RAG 指标评估与持续优化的方向很多同学做完解析和分块就以为大功告成其实这才走完 RAG 数据侧的一半。我在实测中发现解析分块的优化必须配合 RAG 评估指标一起看否则你只是在盲目调参。这里补充几个关键视角。5.1 评估维度要拆开看RAG 评估通常看两个核心维度召回质量有没有把正确答案找出来和生成质量大模型有没有基于召回内容给出正确答案。解析和分块只影响前者。所以评估时必须把两者拆开召回质量看“命中率”和“重排后的命中率”生成质量看“答案相关度”和“忠实度”。我习惯在还没接大模型前先把召回质量测好。这样如果效果差我能明确是数据侧的问题而不是大模型“乱编”的问题。等召回稳定在 80% 以上再接入生成环节调起来才有的放矢。5.2 结构化信息检索优于纯文本检索处理复杂的协议文档、法规文档时纯文本解析往往不够用。比如 3GPP 协议文档很多内容在表格和编号列表里纯文本切分后编号层级关系会丢失。我建议在解析阶段就输出“半结构化数据”比如把表格转成 Markdown把标题层级显式标记为 H1/H2/H3把列表保留成 bullet 结构。这些标记不仅是给分块用的也是给重排模型参考的上下文信号。5.3 多模态文档的未来方向现在很多文档不仅有文字还有图表、示意图。纯文本 RAG 会丢掉图表的信息。如果你处理的文档里图片占比很高可以考虑多模态 RAG也就是把每张图片单独切出来过图像描述模型生成文本描述或者用多模态 embedding 直接向量化图片。这一块已经有现成方案后面我也会单独写一篇展开。但需要注意的是多模态方案对硬件和成本要求更高中小项目建议先从“图片转文本描述”切入性价比最高。我的实际体会写到这里想说一些题外话。做 RAG 这一年多我最大的感受是效果好的 RAG 系统往往不是模型最强而是数据处理最扎实。我见过有人用最便宜的 embedding 模型但解析和分块做得非常细致效果吊打那些“gpt-4 级模型 糙处理”的方案。所以不要觉得 PDF 解析和文本分块是脏活累活不值得花时间。正是这些细节决定了你的 RAG 是“演示可用”还是“生产可用”。最后再分享一个我工作流里的小技巧我们会在解析完每个 PDF 后自动生成一份“Markdown 预览文件”把它当作文档索引的一部分保存下来。这样不仅方便人工抽查解析质量还能在检索命中的时候直接返回给用户一个结构化的“原文片段”而不是纯文本体验会好很多。你可以在自己的项目里试试成本很低但收益很直观。