用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/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。