用Docling+Spring AI搭建PDF解析与RAG入库管道:表格、Metadata和质量门禁
文章摘要
Spring AI提供DocumentReader、DocumentTransformer和VectorStore等ETL能力,但复杂PDF的版面、表格和OCR往往需要更专业的解析工具。Docling可以将PDF解析成包含页面、标题、段落、表格和图片信息的结构化文档。本文通过“Python Docling解析服务+Spring Boot入库服务”的方式,实现PDF转换、表格导出、统一Chunk模型、质量校验和Spring AI VectorStore写入。
一、为什么采用两段式架构
Docling主要使用Python生态,Spring AI主要面向Java和Spring Boot。
推荐:
文件上传 → Docling解析服务 → 标准化Document JSON → Spring Boot质量检查 → Chunk → Embedding → VectorStore而不是强行在Java中复刻所有PDF版面分析能力。
两段式的优势:
- 解析能力独立升级;
- Java业务服务保持稳定;
- 可以替换解析引擎;
- 解析失败可单独重试;
- 更容易保存中间产物;
- 表格和图片可以单独处理。
二、统一的解析结果协议
定义:
{"document_id":"DOC-001","filename":"产品手册.pdf","status":"SUCCEEDED","pages":20,"elements":[{"element_id":"E-001","type":"SECTION_HEADER","page":1,"text":"第一章 产品介绍","metadata":{}},{"element_id":"E-002","type":"PARAGRAPH","page":1,"text":"……","metadata":{"section_path":["第一章 产品介绍"]}}],"quality":{"empty_page_ratio":0,"ocr_page_ratio":0.15,"table_count":4}}Java端只依赖该协议,不直接依赖Docling内部对象。
三、安装Docling
python-mvenv .venvsource.venv/bin/activate pipinstalldocling fastapi uvicorn python-multipart pandas首次运行可能下载版面、表格或OCR模型,应在部署前预热,不要让生产第一个请求临时下载模型。
四、基础PDF转换
frompathlibimportPathfromdocling.document_converterimportDocumentConverter converter=DocumentConverter()result=converter.convert(Path("产品手册.pdf"))document=result.document markdown=document.export_to_markdown()Path("output.md").write_text(markdown,encoding="utf-8")Docling输出的不只是Markdown,还可以访问结构化文档元素。
五、导出表格
frompathlibimportPathdefexport_tables(document,output_dir:Path)->list[dict]:output_dir.mkdir(parents=True,exist_ok=True)tables=[]forindex,tableinenumerate(document.tables):dataframe=table.export_to_dataframe()table_id=f"T-{index+1:04d}"csv_path=output_dir/f"{table_id}.csv"html_path=output_dir/f"{table_id}.html"dataframe.to_csv(csv_path,index=False)dataframe.to_html(html_path,index=False)tables.append({"table_id":table_id,"headers":list(dataframe.columns),"rows":dataframe.fillna("").to_dict(orient="records"),"markdown":dataframe.to_markdown(index=False)})returntables表格应同时保存:
- Markdown;
- CSV;
- 行列JSON;
- 页面位置;
- 标题和单位。
六、构建FastAPI解析服务
frompathlibimportPathfromtempfileimportNamedTemporaryFilefromfastapiimportFastAPI,UploadFilefromdocling.document_converterimportDocumentConverter app=FastAPI()converter=DocumentConverter()@app.post("/api/parse")asyncdefparse(file:UploadFile):suffix=Path(file.filenameor"upload.pdf").suffixwithNamedTemporaryFile(suffix=suffix,delete=False)astemp:content=awaitfile.read()temp.write(content)temp_path=Path(temp.name)try:result=converter.convert(temp_path)document=result.documentreturn{"filename":file.filename,"status":"SUCCEEDED","markdown":document.export_to_markdown(),"tables":export_tables_to_json(document)}finally:temp_path.unlink(missing_ok=True)生产环境还要限制:
文件大小 页数 格式 超时 并发 临时目录 恶意文件七、不要只返回一个Markdown字符串
Markdown适合展示,但企业RAG需要Metadata。
建议解析服务返回元素:
{"type":"PARAGRAPH","text":"平台支持批次效期管理。","page":8,"bbox":[100,200,500,260],"section_path":["第三章 仓储管理","3.2 批次管理"],"content_hash":"..."}这样可以支持:
- 页面引用;
- 章节分块;
- 相邻元素合并;
- 表格和正文区分;
- 解析质量追踪。
八、Spring Boot解析客户端
publicinterfaceDocumentParseClient{ParsedDocumentparse(Resourceresource,Stringfilename);}WebClient实现:
@ComponentpublicclassDoclingParseClientimplementsDocumentParseClient{privatefinalWebClientwebClient;publicDoclingParseClient(WebClient.Builderbuilder,@Value("${docling.base-url}")StringbaseUrl){this.webClient=builder.baseUrl(baseUrl).build();}@OverridepublicParsedDocumentparse(Resourceresource,Stringfilename){MultipartBodyBuilderbody=newMultipartBodyBuilder();body.part("file",resource).filename(filename);returnwebClient.post().uri("/api/parse").bodyValue(body.build()).retrieve().bodyToMono(ParsedDocument.class).timeout(Duration.ofMinutes(5)).block();}}生产环境应避免无限block,并配置连接池、超时和重试策略。
九、Java领域模型
publicrecordParsedElement(StringelementId,Stringtype,intpage,Stringtext,List<String>sectionPath,Map<String,Object>metadata){}publicrecordParsedDocument(Stringfilename,Stringstatus,intpages,List<ParsedElement>elements,Map<String,Object>quality){}十、质量检查器
@ComponentpublicclassParsedDocumentValidator{publicvoidvalidate(ParsedDocumentdocument){if(!"SUCCEEDED".equals(document.status())){thrownewIllegalStateException("文档解析失败");}longvalidElements=document.elements().stream().filter(element->element.text()!=null&&!element.text().isBlank()).count();if(validElements==0){thrownewIllegalStateException("解析结果没有有效文本");}}}还应检查:
- 空白页比例;
- 乱码率;
- OCR置信度;
- 表格列数;
- 页面数量;
- 语言;
- 重复行。
十一、将元素转换成Spring AI Document
publicList<Document>convert(StringdocumentId,StringtenantId,ParsedDocumentparsed){returnparsed.elements().stream().filter(this::isIndexable).map(element->newDocument(element.text(),Map.of("document_id",documentId,"tenant_id",tenantId,"element_id",element.elementId(),"content_type",element.type(),"page",element.page(),"section_path",String.join(" > ",element.sectionPath())))).toList();}不建议把:
- 页码;
- 空文本;
- 装饰元素;
- 重复页眉页脚;
直接入库。
十二、结构化分块
同一章节中的短段落可以合并:
标题 +段落1 +段落2遇到以下元素时建立边界:
SECTION_HEADER TABLE CODE FORMULA LIST表格单独处理,不交给普通TokenTextSplitter破坏。
普通长段落再使用Spring AI TokenTextSplitter控制上限。
十三、写入VectorStore
@ServicepublicclassKnowledgeIndexService{privatefinalVectorStorevectorStore;publicKnowledgeIndexService(VectorStorevectorStore){this.vectorStore=vectorStore;}publicvoidindex(List<Document>chunks){vectorStore.add(chunks);}}大批量文档要:
- 分批;
- 控制Token;
- 处理限流;
- 记录失败批次;
- 支持断点续传;
- 使用稳定Chunk ID。
十四、幂等与版本
文档Metadata:
document_id document_version content_hash parser_version chunk_strategy_version embedding_model同一文档重新上传时:
计算Hash → 相同则跳过 → 不同则创建新版本 → 新版本入库 → 验证成功 → 旧版本失效不要先删除旧版本再解析新文件,否则失败时知识库为空。
十五、解析失败如何降级
Docling标准解析 → 结果质量低 → 启用OCR → 仍失败 → 切换备用解析器 → 人工审核状态:
UPLOADED PARSING PARSED PARTIAL OCR_REQUIRED MANUAL_REVIEW INDEXED FAILED十六、建议记录的指标
parse_duration_ms pages ocr_pages element_count table_count empty_page_ratio garbled_ratio chunk_count embedding_duration_ms index_duration_ms failed_batch_count解析质量和检索质量要关联分析。
十七、部署注意事项
Docling解析服务通常比普通Web接口消耗更多CPU、内存和模型资源。
建议:
- 独立容器;
- 限制并发;
- 使用任务队列;
- 文件落对象存储;
- 结果异步回调;
- 模型预下载;
- 临时文件定期清理;
- 大文件设置页数上限。
总结
Docling和Spring AI的合理分工是:
Docling → 理解PDF版面、表格和文档结构 Spring AI → 管理Document、Chunk、Embedding和VectorStore通过统一解析协议和质量门禁,可以避免解析引擎与业务代码强耦合,并为后续替换OCR、分块和向量库保留空间。
