资讯详情

资讯详情

Day99-复杂文档解析:表格/多模态/扫描件的处理方案

上篇我们把高级 RAG 的全景图画完了——Query 改写、多路检索、上下文压缩、自纠错。但有个扎心的事实得先说破这些全建立在喂进去的文档已经被正确解析成文本这个前提下。现实里企业文档全是带表格的 PDF、图文混排的产品手册、扫描盖章的合同。解析这关过不去垃圾进垃圾出谁也救不了。这篇就专门拆文档解析这道上游工序。一、为什么文档解析是 RAG 的隐形天花板新手做 RAG精力全花在选向量库、调 Prompt 上结果一跑答案里表格数据全错位、图片内容完全丢失、扫描件干脆空空如也。问题不在下游在源头。我把复杂文档分成三类难度递增类型典型场景核心难点粗暴解析的后果结构化文本含表格财报、产品说明书、API 文档表格跨页、单元格合并、多级标题表格被压成一段乱序文字数值对不上行图文混排技术白皮书、PPT 导出的 PDF图注与正文分离、公式、双栏排版切分把图和说明拆开语义断裂扫描件/图片盖章合同、历史档案、手写单本质是图片无文本层直接拿到空字符串检索全失效痛点很清楚文本型 PDF 丢结构扫描型 PDF 丢内容。下面一段段给可落地的 Java 方案。二、统一解析入口先把文档抽象成一个接口不管什么来源我们最终要的是一块块带结构的文本。先定义统一契约后续不同文件类型各自实现上层检索逻辑不感知差异。// 统一的文档解析接口所有文件类型都实现它 // 依赖JDK 17 public interface DocumentParser { ​ // 解析结果保留结构块而不是一坨纯文本 ListDocChunk parse(InputStream source, ParseContext ctx) throws ParseException; ​ boolean supports(String fileExtension); } ​ // 结构块区别于普通字符串带类型和层级方便后续切分与重排 public record DocChunk( String id, ChunkType type, // TEXT / TABLE / IMAGE_CAPTION / FORMULA int pageNo, String content, // 表格已是 Markdown 表格串图片是 OCR/多模态描述 MapString, Object meta ) {} ​ public enum ChunkType { TEXT, TABLE, IMAGE_CAPTION, FORMULA } ​ public record ParseContext( boolean extractTable, // 是否抽表格 boolean ocrFallback, // 无文本层时是否走 OCR String lang // 语种影响 OCR 模型选择 ) {}这个抽象的价值检索层永远拿ListDocChunk后面换 PDFBox、换视觉模型、换 OCR 引擎都不用动上面。我见过太多项目把 PDFBox 代码直接写死在 Controller 里半年后想加扫描件支持得重写一遍。三、表格提取让财报里的数字对得上行很多 PDF 表格没有真正的表格线文字是靠坐标排成表格的。直接用PDFTextStripper抽数字会乱序。正确做法是用区域化抽取 列边界推断。tabula-java基于 PDFBox是工业级选择。// Maven 依赖 // dependency // groupIdtechnology.tabula/groupId // artifactIdtabula/artifactId // version1.0.5/version // /dependency ​ public class PdfTableParser implements DocumentParser { ​ private final ObjectExtractor extractor new ObjectExtractor(); ​ Override public ListDocChunk parse(InputStream source, ParseContext ctx) throws ParseException { ListDocChunk chunks new ArrayList(); try (PDDocument doc PDDocument.load(source)) { for (int p 1; p doc.getNumberOfPages(); p) { PageArea page extractor.extract(doc, p); // 启发式探测表格区域基于空格列边界识别 ListTable tables page.getTables(); for (Table t : tables) { String md tableToMarkdown(t); // 转成 Markdown 表格 chunks.add(new DocChunk( p p -tbl t.getIndex(), ChunkType.TABLE, p, md, Map.of(rows, t.getRowCount(), cols, t.getColCount()) )); } // 表格之外的正文单独成块 String text page.getTextWithoutTables(); if (!text.isBlank()) { chunks.add(new DocChunk(p p -txt, ChunkType.TEXT, p, text, Map.of())); } } } catch (IOException e) { throw new ParseException(PDF 解析失败: e.getMessage(), e); } return chunks; } ​ // 把 tabula 的 Table 转成 Markdown保留行列结构检索时数值不会错位 private String tableToMarkdown(Table t) { StringBuilder sb new StringBuilder(); ListListString grid t.getData(); for (int r 0; r grid.size(); r) { sb.append(| ).append(String.join( | , grid.get(r))).append( |\n); if (r 0) sb.append(|).append(---|.repeat(grid.get(r).size())).append(\n); } return sb.toString(); } ​ Override public boolean supports(String ext) { return pdf.equalsIgnoreCase(ext); } }关键点表格转成Markdown 表格串再入库。这样 Embedding 时行和列关系保留用户问2024 年 Q3 营收多少向量能命中对应行而不是在一坨乱序文字里大海捞针。我踩过的坑曾经直接把表格当纯文本切结果营收 1200 万和成本 800 万被切到两个 chunk大模型算净利润直接瞎编。四、扫描件与图文混排OCR 与多模态的分级处理扫描件无文本层和带图文档光靠 PDFBox 拿不到内容必须上 OCR 或多模态视觉模型。这里有个成本分级的取舍别无脑上大模型那会烧穿预算。// 解析策略选择器根据文件特征选最划算的路径 // 低成本本地 Tesseract OCR表格少、清晰扫描件 // 高保真视觉大模型直接读图图文混排、公式、复杂版面 public class ParseStrategySelector { ​ public ParsePlan select(DetectResult detect) { // 1. 有文本层且表格少 → 纯文本解析几乎零成本 if (detect.hasTextLayer() detect.tableRatio() 0.1) { return ParsePlan.TEXT_ONLY; } // 2. 无文本层扫描件但页面规整 → 本地 OCR单页几分钱 if (!detect.hasTextLayer() detect.isCleanScan()) { return ParsePlan.LOCAL_OCR; // TesseractCPU 即可 } // 3. 图文混排 / 多栏 / 含公式 → 上视觉大模型保真但贵 return ParsePlan.VISION_LLM; // 如 Qwen-VL / GPT-4V按图计费 } ​ // 扫描件 OCR 兜底实现Tesseract 4.x // 依赖net.sourceforge.tess4j:tess4j:5.9.0 public String ocrScan(InputStream img, String lang) { Tesseract t new Tesseract(); t.setDatapath(/opt/tessdata); // 语言包路径 t.setLanguage(lang); // chi_simeng try { return t.doOCR(ImageIO.read(img)); } catch (TesseractException | IOException e) { throw new ParseException(OCR 失败, e); } } }成本取舍的经验值仅供参考按时价本地 OCR 单页几分钱、CPU 可跑视觉大模型单图几毛到一块但能直接理解图表含义、把柱状图读成Q3 营收环比下降 12%。所以我的判据是——能本地 OCR 解决的绝不调大模型只有版面复杂、含非文本信息时再上视觉模型。一个 200 页的历史合同扫描库全用视觉模型跑一遍账单能吓你一跳。五、多模态块把图翻译成可检索的描述图文混排文档里图本身没有文本。我们不直接把图片塞进向量库除非用多模态 Embedding更务实的做法是用视觉模型生成一段结构化描述作为该块的content让它参与检索。// 用视觉大模型为图片块生成可检索描述保留图注上下文 // 依赖Spring AI 1.0 ChatClient 多模态消息 public class ImageChunker { ​ private final ChatClient visionClient; // 注入支持视觉的模型 ​ public DocChunk describeImage(BufferedImage img, String nearbyCaption) { String b64 encodeBase64(img); String prompt 请描述这张图的关键信息若含图表请转述核心数据趋势 若含流程图请说明步骤。图注上下文%s。 输出不超过 120 字不要编造图中没有的数字。 .formatted(nearbyCaption); ​ String desc visionClient.prompt() .user(u - u.text(prompt) .media(MimeType.valueOf(image/png), new ByteArrayResource(Base64.getDecoder().decode(b64)))) .call().content(); ​ return new DocChunk( img- UUID.randomUUID(), ChunkType.IMAGE_CAPTION, -1, desc, Map.of(source, vision-llm) ); } }注意 prompt 里那句不要编造图中没有的数字——这是真实教训。视觉模型幻觉起来比文本模型还离谱让它转述数据必须加约束并且和 Day 98 讲的自纠错链路打通检索到图片描述块后生成答案时让它引用图中原话再让RetrievalGrader校验。六、整体流水线解析 → 结构块 → 切分 → 入库把上面几块串起来一条干净的解析流水线长这样原始文档 (PDF/扫描件/图片) │ ▼ [探测器] 有没有文本层表格占比是否扫描件 │ ├─有文本层──────► [PdfTableParser] 抽文本表格 → Markdown 块 │ ├─扫描件────────► [ParseStrategySelector] │ ├─清晰规整 → 本地 OCR便宜 │ └─复杂版面 → 视觉大模型贵但准 │ └─图文混排──────► [ImageChunker] 图→视觉描述块 │ ▼ [ListDocChunk] 结构块TEXT/TABLE/IMAGE_CAPTION │ ▼ [语义切分] 表格整块不切断正文按 512 token 滑窗 │ ▼ [Embedding 入库] → 进入 Day98 的高级 RAG 检索链路这条线的核心纪律表格整块不切断图片必须转成文字描述再入库。守这两条下游检索质量的提升比换一个更贵的向量库明显得多。七、实战建议先搞清楚你的文档长什么样再选技术。别一上来就接视觉大模型。我建议先用探测脚本统计一批真实文档纯文本占比多少、含表格多少页、扫描件多少。80% 的复杂文档其实只是普通 PDF 带几张表PDFBox tabula 就够了根本用不着 OCR。OCR/视觉模型的输出一定要留原图可追溯。把图片块 → 原图路径的映射存下来。哪天用户质疑答案你能一键调出原图人工复核而不是对着一段 AI 生成的描述干瞪眼。这也是 Day 96 讲的可靠性工程在解析侧的延伸。解析质量必须进回归集。把 Day 95 的离线回归集用起来挑 20 份典型文档人工标注这段表格应该抽到什么每次改解析逻辑就跑一遍看表格抽取准确率有没有掉。解析器是最容易改一处崩一片的模块没有量化守住迟早翻车。八、下篇预告RAG 的精度上限往往在文档被塞进向量库之前的那个 parse 函数里就定了——下游再花哨也救不回一个把好表格拆成乱码的解析器。下篇预告Day 100我们聊 RAG 的记忆与状态——多轮对话里的会话记忆、长期用户画像怎么存、如何让同一个知识库对不同用户给出不同口径的回答。这关系到 RAG 从能答到懂你的最后一跃。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →