用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 -m venv .venv
source .venv/bin/activate
pip install docling fastapi uvicorn python-multipart pandas

首次运行可能下载版面、表格或OCR模型,应在部署前预热,不要让生产第一个请求临时下载模型。

四、基础PDF转换

from pathlib import Path

from docling.document_converter import DocumentConverter


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,还可以访问结构化文档元素。

五、导出表格

from pathlib import Path


def export_tables(document, output_dir: Path) -> list[dict]:
    output_dir.mkdir(
        parents=True,
        exist_ok=True
    )

    tables = []

    for index, table in enumerate(
        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
            )
        })

    return tables

表格应同时保存:

  • Markdown;
  • CSV;
  • 行列JSON;
  • 页面位置;
  • 标题和单位。

六、构建FastAPI解析服务

from pathlib import Path
from tempfile import NamedTemporaryFile

from fastapi import FastAPI, UploadFile
from docling.document_converter import DocumentConverter

app = FastAPI()
converter = DocumentConverter()


@app.post("/api/parse")
async def parse(file: UploadFile):
    suffix = Path(file.filename or "upload.pdf").suffix

    with NamedTemporaryFile(
        suffix=suffix,
        delete=False
    ) as temp:
        content = await file.read()
        temp.write(content)
        temp_path = Path(temp.name)

    try:
        result = converter.convert(temp_path)
        document = result.document

        return {
            "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解析客户端

public interface DocumentParseClient {

    ParsedDocument parse(
            Resource resource,
            String filename
    );
}

WebClient实现:

@Component
public class DoclingParseClient
        implements DocumentParseClient {

    private final WebClient webClient;

    public DoclingParseClient(
            WebClient.Builder builder,
            @Value("${docling.base-url}")
            String baseUrl
    ) {
        this.webClient = builder
                .baseUrl(baseUrl)
                .build();
    }

    @Override
    public ParsedDocument parse(
            Resource resource,
            String filename
    ) {
        MultipartBodyBuilder body =
                new MultipartBodyBuilder();

        body.part("file", resource)
                .filename(filename);

        return webClient.post()
                .uri("/api/parse")
                .bodyValue(body.build())
                .retrieve()
                .bodyToMono(
                        ParsedDocument.class
                )
                .timeout(Duration.ofMinutes(5))
                .block();
    }
}

生产环境应避免无限block,并配置连接池、超时和重试策略。

九、Java领域模型

public record ParsedElement(
        String elementId,
        String type,
        int page,
        String text,
        List sectionPath,
        Map metadata
) {
}
public record ParsedDocument(
        String filename,
        String status,
        int pages,
        List elements,
        Map quality
) {
}

十、质量检查器

@Component
public class ParsedDocumentValidator {

    public void validate(
            ParsedDocument document
    ) {
        if (!"SUCCEEDED".equals(
                document.status()
        )) {
            throw new IllegalStateException(
                    "文档解析失败"
            );
        }

        long validElements = document.elements()
                .stream()
                .filter(element ->
                        element.text() != null
                        && !element.text().isBlank()
                )
                .count();

        if (validElements == 0) {
            throw new IllegalStateException(
                    "解析结果没有有效文本"
            );
        }
    }
}

还应检查:

  • 空白页比例;
  • 乱码率;
  • OCR置信度;
  • 表格列数;
  • 页面数量;
  • 语言;
  • 重复行。

十一、将元素转换成Spring AI Document

public List convert(
        String documentId,
        String tenantId,
        ParsedDocument parsed
) {
    return parsed.elements()
            .stream()
            .filter(this::isIndexable)
            .map(element -> new Document(
                    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

@Service
public class KnowledgeIndexService {

    private final VectorStore vectorStore;

    public KnowledgeIndexService(
            VectorStore vectorStore
    ) {
        this.vectorStore = vectorStore;
    }

    public void index(
            List 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、分块和向量库保留空间。

延伸阅读

如果你正在关注企业级 AI 应用、RAG、Agent、MCP 与大模型工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。