ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

企业RAG知识库的PDF结构化之道:Markdown与D-RAC

企业RAG知识库的PDF结构化之道:Markdown与D-RAC 做企业RAG知识库的朋友大概都经历过这么一幕高高兴兴把一批合同、产品手册、技术文档交给团队说直接喂给大模型就行结果上线一测问本季度退货政策是什么模型答非所问甚至把两个不同版本的条款混在一起讲。问题基本不在模型而在你对PDF做的处理或者说没处理。今天想聊的是一个被很多人忽视但极其关键的环节企业PDF到底该不该先转成Markdown再喂给RAG以及Yellow.ai提出的D-RAC方案背后究竟在讲什么逻辑。这篇文章不是学术复述而是站在工程落地的角度把文档解析、结构化分块、检索增强这套链路拆开揉碎顺便给出我踩坑换来的实操建议。适合正在搭知识库、做企业问答机器人、搞文档智能检索的同学参考。1. 企业RAG落地为什么卡在PDF这道坎上1.1 PDF是为打印设计的格式不是为机器阅读设计的很多人一上来就忽略了最根本的问题PDF这种格式从诞生那天起目标是无论在哪个设备上打开排版都不变。它服务的是人眼不是算法。PDF内部记录的是一堆坐标、字体、路径、图像对象告诉你这段文字出现在页面这个位置而不是这篇文章包含一个一级标题叫退货政策下面跟着一段定义列表。企业知识库里PDF占了绝大多数。合同、产品规格书、测试报告、售后手册、培训材料、ISO体系文件基本全是PDF。而这些PDF往往还不是干净的文本型PDF常见形态我列一下扫描件典型的图片PDF本质是每页一张图文字全部在图里文本型但带复杂表格财务对账单、排产计划表格线框会把文本流撕得稀碎多栏排版很多技术白皮书、期刊格式PDF是双栏甚至三栏按正常从左到右读文本出来的内容会交叉错乱带页眉页脚、水印、批注页眉页码会污染正文水印会混进检索片段。这类文件直接用现有工具抽取文本得到的就是一段没有层级、没有语义边界、顺序还可能错的纯字符流。拿这种文本去切片、做向量化等于先把书撕成纸条再丢进碎纸机后面怎么检索都是残缺的。1.2 直接把PDF喂给RAG会发生什么很多团队一开始图省事直接把PDF丢给LangChain或LlamaIndex的PDFLoader或者干脆调一个在线解析接口拿到文本就灌库。后果通常集中在四个地方第一分块没有语义边界。经典的分块策略是固定token数比如512加重叠但PDF文本不会按照语义给你换行一个段落可能被拦腰截断上一句的问话和下一句的回答被分到两个块里检索时永远凑不齐上下文。第二表格变成天书。PDF里的表格拿文本抽取出来往往是这样的季度 销售额 同比 Q1 100 5% Q2 115 10%看着还行实际抽取出来可能是一大串空格加换行行列关系全没了。更惨的是某些PDF把表格里的每个单元格画成一个独立文本对象抽取顺序完全是乱的。第三标题层级信息彻底丢失。你的知识库明明有清晰的目录结构3.2 退货政策下面有两页说明但解析成纯文本后3.2就是个普通数字标题和正文没有任何层级关系。检索的时候系统根本不知道一个片段到底属于哪个章节回答时自然也无法给出根据《售后政策》3.2节这类有据可查的结论。第四图片和图表里的信息直接蒸发。产品手册里大量关键信息是藏在示意图、流程图、性能曲线里的。纯文本抽取对这些内容无能为力图片位置只留下一段空引用或干脆什么都没有。相关热词里有人问RAG知识库能存储图片嘛这正是大家在实际中撞到墙的表现。1.3 企业场景对可解释、可追溯的要求更高个人玩RAG答错了大不了再问一次。企业场景不行法务、客服、质检部门拿着回答要去核对原文答错了要有追溯链审计要查你知识库内容的来源和版本。这就要求RAG系统不只是检索到一段相似的文本而是要知道这段文本来自哪份文档的哪个章节、哪个表格、第几页。纯文本切片做不到这一点。你需要一种既保留人可读结构、又方便机器切分和检索的中间格式这正是Markdown被推到台前的原因。2. Markdown为什么是企业RAG的理想中间格式2.1 Markdown天生自带结构先看一个最简单的对照。同一段内容纯文本抽取大概是1. Scope This policy applies to all products purchased after Jan 1 2024. If you are not satisfied... 2. Returns You may return unopened products within 30 days...而转成Markdown之后# 1. Scope This policy applies to all products purchased after Jan 1 2024. If you are not satisfied with your purchase, you may request a refund... # 2. Returns You may return unopened products within 30 days of delivery. Refunds will be issued to the original payment method...区别不是多几个井号的事而是机器终于能理解文档的层级关系了。标题标记了语义边界列表项标记了并列关系表格标记了行列结构代码块标记了程序片段。这些信息对RAG至少有三个直接价值分块有锚点可以按标题把文档切成有语义边界的块而不是按固定字符数硬切检索有特征标题、加粗、表格结构都可以作为关键词匹配和重排的信号回答有引用块自带章节路径比如/docs/售后政策/3.2/退货流程回答时可以直接展示来源位置。2.2 Markdown对LLM和工具链都友好LLM本身在训练时见过海量Markdown它对这种格式的理解远好于一堆被抽出来的纯文本。同样一段内容用Markdown喂给模型模型对表格里第三行第二列这种问题的把握明显更稳。另外Markdown是纯文本兼容性极好几乎所有RAG框架、向量库、前端组件都能处理不会有编码、二进制、权限这类额外麻烦。而且从工程运营角度看Markdown是可人读、可手改、可diff的。知识库维护人员可以打开Markdown文件直接修正OCR错误、删掉页眉页脚、补上缺失的标题。换成PDF试试想改一个错别字都得重新导出维护成本差了一个量级。2.3 先转Markdown再喂RAG到底解决了什么问题用一句话总结PDF转Markdown的本质是把人眼才能看懂的排版转换成机器能理解的树状结构让后续的分块、检索、回答都站在结构化的地基上。解决了四个层面的问题环节纯PDF直接切片PDF转Markdown后分块按字符数硬切撕裂语义按标题、段落、表格切分块内语义完整检索只能匹配文本相似度可以结合结构权重标题命中优先、表格列名命中优先回答缺乏上下文结构容易串味保留章节路径回答可引用可追溯维护PDF改不了错了只能重导Markdown直接编辑可持续校对更新3. 拆解D-RAC文档感知的RAG管道设计3.1 D-RAC是什么从命名和背景推演核心思路先说明一下目前公开渠道能看到的资料更多停留在方案框架层面并没有像学术论文那样给出逐行的公式和消融实验。所以我下面的拆解是基于文档感知这个方向结合Yellow.ai做企业级客服对话的工程背景以及RAG落地的通用实践做的逻辑梳理。你把它理解成这类方案的标准设计也可以。D-RAC可以展开为Document-aware Retrieval-Augmented Chat核心主张是不要只把PDF当成要抽取的文本而要把它当成具有层级结构的文档对象来感知和处理。先转Markdown再喂RAG不是可选项而是这套架构里文档理解环节的标准动作。为什么Yellow.ai会提出这个东西因为他们是做企业客服对话机器人的客户拿来的知识库大量是PDF手册、政策条文、产品说明。过去客服机器人最常被吐槽的就是答非所问没有依据根子往往不在模型而在文档进系统之前就被处理坏了。D-RAC想解决的正是这个工程问题。3.2 D-RAC管道的五个关键环节基于文档感知的思路这类管道通常由五个环节组成环节一文档预处理与格式识别。进来一个PDF先体检。是扫描件还是文本型里面有没有复杂表格是不是多栏要不要OCR这一步的好坏直接决定后面所有环节。跳过体检直接转扫描件转出来就是一堆乱码。环节二结构化解析。这一步的任务是把PDF里的视觉布局还原成文档逻辑结构。识别标题层级、段落边界、表格行列、列表项、页眉页脚并剔除。输出的就是干净、结构完整的Markdown或类似的结构化中间表示。环节三结构感知的分块。拿到结构化的Markdown后不是简单按token切而是以标题为界把文档切成一棵章节树。每个块知道自己属于哪一章哪一节表格块还知道自己是一个独立表格带表头和列名。环节四索引与混合检索。向量检索之外加一层关键词和结构检索。比如用户问退货政策30天这个30天如果只在某个表格单元格里出现向量检索的召回效果通常一般但结合表格列名和关键词检索就能很快定位到那一行。环节五生成与引用。检索到的每个块自带章节路径和原始页码生成回答时可以把这些信息传给模型让模型在回答里标注根据《退货政策》第4.2节。这一步的引用能力恰恰是企业场景最看重的。注意D-RAC具体实现里的模块细节我无法逐字复现但上面这五个环节是当前做文档感知RAG的通用骨架照着这个方向设计大概率不会跑偏。3.3 与朴素RAG对比差异在哪朴素RAG的链路是PDF → 文本抽取 → 固定长度切片 → 向量化 → 检索 → 拼Prompt → 生成。D-RAC的链路是PDF → 布局分析 → 结构化Markdown → 按语义结构分块 → 混合索引 → 结合结构的重排和引用 → 生成。差异集中在三点多了一层结构化理解而不是直接透视文本分块单元从多少字符变成什么语义检索信号从词面相似扩展到结构命中和表格列名。这三点带来的体感提升非常明显。举个我们实际测试过的例子一份产品FAQ文档里有个表格左侧列是故障现象右侧列是解决方法。朴素RAG把整个表格按纯文本切碎之后用户问设备开机没反应怎么办系统召回的是表格的某一行碎片回答生硬。走结构化处理之后表格块被完整保留表头故障现象/解决方法直接参与匹配回答能把对应行的完整解决方法带出来还带表头上下文质量完全不是一个档次。4. 实操环节把一张企业PDF正确喂给RAG4.1 工具选型目前能打的主流方案这里我把实际用下来靠谱的工具整理一下按用途分PyMuPDFfitzPython里处理PDF最顺手的库文本抽取快、支持按块和坐标拿文本、可以抽图片。适合做自定义解析和轻量清洗。pdfplumber表格抽取比PyMuPDF稳很多能按行列还原表格。复杂表格优先用它。OCR工具扫描件绕不开OCR。本地可以用PaddleOCR、Tesseract云端有各家接口。中文场景PaddleOCR效果明显比Tesseract好但依赖重一些。marker / MinerU / docling这三类是PDF直接转Markdown的完整管道型工具内置了布局模型、表格识别和OCR模块不用自己搓轮子。其中MinerU在中文复杂版式上表现不错docling对表格和阅读顺序的处理设计比较现代。unstructured批处理生态全能输出带元数据的结构化文档适合和LangChain/LlamaIndex直接对接。工具选型有个原则简单场景别上重武器复杂场景别用手工活。如果你的PDF大多是文本型、排版规整用PyMuPDF搭一条轻量清洗管线就够了。如果文档来源五花八门扫描件、多栏、表格混杂直接用marker或MinerU这类整体方案省心得多。4.2 一套可复现的转换与入库流程我习惯把整个流程拆成五步每一步都有明确的产出物第一步文档体检。拿到PDF先跑一个检查脚本判断三个关键指标是否扫描件直接看文本提取结果如果整页提取不到文字或文字极少大概率是图片型是否加密带权限读取元数据判断有密码锁要先处理是否有复杂表格或多栏用pdfplumber简单跑一下看页面上的矩形线框数量和文本块分布。体检脚本大概长这样import fitz # PyMuPDF def pdf_check(path): doc fitz.open(path) total_text table_lines 0 for page in doc: text page.get_text() total_text text # 检测页面上的绘图对象表格线框通常以drawings出现 drawings page.get_drawings() if drawings: table_lines len(drawings) if len(total_text.strip()) 50: print(该PDF大概率是扫描件/图片型需要走OCR) if table_lines 10: print(检测到大量表格线框建议用pdfplumber或docling单独处理表格)第二步选择转换策略。文本型PDF走文本抽取规则清洗扫描件先OCR再转Markdown。这一步别混着处理因为OCR出来的文本没有布局信息转Markdown的难度完全不同。第三步PDF转Markdown。结构化文档场景我现在的做法是优先让marker或MinerU直接出Markdown输出带标题层级和表格结构。如果只是少量规整文档也可以自己用PyMuPDF抽取标题后拼Markdown。下面给一个最简版本思路是把字号大的行当作标题构建出基本的层级import fitz def extract_markdown_simple(pdf_path): doc fitz.open(pdf_path) lines [] for page in doc: blocks page.get_text(dict)[blocks] for block in blocks: if lines not in block: continue for line in block[lines]: text .join(span[text] for span in line[spans]) size max(span[size] for span in line[spans]) if size 14: # 阈值需要根据实际文档调整 lines.append(f## {text.strip()}) else: lines.append(text.strip()) return \n.join(lines)注意这个极简版完全依赖字号判断标题对排版规整的内部文档够用对复杂版式必须上布局模型比如MinerU。第四步人工抽查与清洗。这一步最容易被忽略但实际价值最大。转换出来的Markdown至少抽5~10页人工看一眼重点核对页眉页脚、页码是否混进正文表格行列是否错位多栏文本的阅读顺序是否错乱目录和封面这种干扰页要不要删除。第五步结构化分块与入库。拿到干净Markdown之后按标题层级切块。我的做法是按二级标题切太长的段落再按三级标题细切每个块保留完整的标题路径作为元数据。有了层级路径检索结果就能直接展示来源。一个实用的分块思路import re def split_markdown_by_heading(md_text, level2): lines md_text.splitlines() chunks [] current_title 前言 current_buf [] for line in lines: m re.match(r^(#{1,6})\s(.*), line) if m and len(m.group(1)) level: if current_buf: chunks.append((current_title, \n.join(current_buf))) current_title line current_buf [] else: current_buf.append(line) if current_buf: chunks.append((current_title, \n.join(current_buf))) return chunks这样切出来的块带有完整章节上下文而不是从文档中间硬截出来的文字残片。4.3 分块参数与混合检索的调优经验结构化分块做好之后检索层面的调优也很关键。我常用的组合是向量检索负责语义召回Embedding模型根据你的语种和领域选中文场景BGE系列或者M3E都不错BM25关键词检索负责精确匹配尤其处理产品型号、条款编号、报错代码这类硬词结构加权重排命中标题的块权重加高命中表格块表头的权重也加高用Rerank模型或者简单的规则都能做。分块大小上我自己的经验是按语义结构切完的块平均600~800字最稳。块太小上下文不完整块太大向量检索的精度下降。如果单个段落确实很长可以做小切片大上下文的两级策略检索用小块拼给模型的Prompt用整节这样两头都兼顾。5. 常见问题与排查技巧实录5.1 扫描件转Markdown之后还是一堆乱码这个问题90%出在OCR阶段。我先建议你排查几点确认输入图片的分辨率。OCR对DPI敏感低于150dpi的扫描件识别效果断崖式下降尽量保证源PDF的扫描分辨率在200~300dpi检查OCR语言包。中文文档必须加载中文模型Tesseract只带英文包跑中文产出基本不能看观察是否版面分析缺失。很多OCR引擎按单行识别表格、多栏直接乱了需要开启版面分析选项。实测下来中英文混排、带表格的扫描件PaddleOCR的鲁棒性最好。代价是部署重一些但企业场景值得。5.2 转出的Markdown表格是坏的表格是PDF转Markdown的重灾区常见坏法有表头丢失、行列错位、多行单元格被拆成多个块。我的排查顺序是先确认源PDF是文本型还是扫描型。文本型表格优先用pdfplumber单独提取然后手工转成Markdown表格语法比直接让通用管道识别更可靠扫描型表格先OCR再提取这时候非常依赖工具的表格结构还原能力docling和MinerU都是这个方向上的选项。如果你的表格结构不复杂从pdfplumber拿到的表格数据自己生成Markdown表格文本其实不难import pdfplumber def table_to_markdown(pdf_path, page_num): with pdfplumber.open(pdf_path) as pdf: page pdf.pages[page_num] table page.extract_table() if not table: return md [] for i, row in enumerate(table): cells [c.replace(|, \\|) if c else for c in row] md.append(| | .join(cells) |) if i 0: md.append(| ---| * len(row)) return \n.join(md)5.3 多栏PDF的阅读顺序错乱提取出来的文本一会儿左栏一会儿右栏这是多栏PDF没做栏检测造成的。解决办法有两个方向一是用带布局分析的转换工具MinerU、docling这类在版面还原上做得更到位二是手动按坐标排序。手动排序的原理是先按栏把文本块分组栏内部再按自上而下排序左右栏拼起来才是正确的阅读顺序。import fitz def get_blocks_sorted_by_columns(pdf_path, page_num): doc fitz.open(pdf_path) page doc[page_num] blocks page.get_text(blocks) # (x0, y0, x1, y1, text, ...) mid_x page.rect.width / 2 left [b for b in blocks if b[0] mid_x] right [b for b in blocks if b[0] mid_x] left.sort(keylambda b: b[1]) right.sort(keylambda b: b[1]) return \n.join([b[4] for b in left right])注意这个方案对标准双栏有效三栏或不对称栏要按实际页面布局调整分组逻辑。最好的方案永远是直接上专业转换工具坐标排序只作为补充手段。5.4 检索命中率高但回答质量还是差这个问题我踩过很久。后来定位到根因检索命中的是句子片段但模型需要的是完整上下文。解决方案是前面提过的小切片大上下文策略。检索时用较短的块比如400字命中之后把那一整节比如2000字都拿给模型作为上下文。这样既保证了检索精度又让模型有足够的上下文理解前因后果。另一个常见问题是多轮对话中的指代混乱。用户先问退货政策是什么再问时限呢朴素RAG在第二轮检索时只拿时限去查很容易召回无关内容。工程上建议把上一轮的意图和实体拼接进本轮检索词这种优化对客服问答场景的提升非常显著。5.5 图片里的信息怎么办前面提到RAG知识库能存储图片嘛是很多人的困惑。我的答案是能但要分情况。如果文档里是带文字的截图比如流程图里的判断框、产品界面图直接把整张图丢给文本RAG是没用的。三个可选方案截图上文字不多的话OCR出文字作为替代文本插到Markdown里图片引用位置用多模态Embedding模型把图片和文字都向量化检索时图文同时召回如果图片承载的信息对问答确实重要最可靠的做法是人工提炼图片要点以文字补充说明的形式放进文档对应位置。我的建议是先盘一下知识库里图片信息的实际价值。大多数客服FAQ场景真正需要看图回答的比例很低OCR人工补充基本能覆盖别一上来就上多模态大模型成本和复杂度都是实打实的。6. 从一次真实项目里总结的经验前面讲了原理、方案和工具最后分享一点我自己的真实体会。有一阵子我在做一个产品售后知识库客户给的材料里混杂着五年间的PDF手册、政策更新、客服聊天记录整理。最初图省事直接对PDF抽文本、按512字符切块、丢进向量库。结果内部测试里客服同学问一句质保期内维修要提供什么材料系统答非所问给出的依据还指向了一份已经废止的旧手册。后来换成PDF先转Markdown再喂RAG的路线加了文档版本字段按标题层级切块检索结果强制带章节路径再把回答生成的Prompt改成请依据给定上下文回答并标注引用位置。修完这一轮之后同样的问题能够稳定引用正确版本的手册并且指出具体在质保服务章节下。整个改造没有换模型没有调Embedding纯粹是把文档级的问题处理干净了。这个过程让我深刻认识到一件事企业RAG的瓶颈往往不在模型而在文档预处理。模型参数可以调Embedding可以换但源头数据如果是一团乱麻后面怎么优化都是事倍功半。把文档结构化当作RAG项目的一等公民对待先建一条可靠的文档流水线再去追求模型效果这个顺序不能反。最后分享一个实用小技巧转出来的Markdown入库前一定让业务方或文档管理人员人工过一遍。这不是流程冗余而是把文档质量的责任前置。哪怕只抽查10%都能提前暴露大量版式、OCR、版本问题省下的返工时间远超投入。PDF转Markdown不是终点而是企业知识库治理的起点。文档质量理顺了RAG的在线效果是水到渠成的事。
返回列表