生产环境实践:基于 Docker 二阶段构建的 MinerU + 千问异步处理架构
Laiyong Wang Lv6

一、核心问题

这类场景的本质不是“怎么调用 AI”,而是:

如何把 OCR + LLM 这种长耗时链路稳定接入业务系统

典型问题:

  • PDF 解析耗时长(分钟级)
  • LLM 不稳定(超时 / 输出异常)
  • HTTP 不能阻塞
  • 任务需要可追踪

二、核心解法:异步化 + 本地执行 + 云模型

整体架构很简单:

1
2
3
4
5
6
7
8
9
请求 → 落库 → 异步任务

MinerU(本地)

Markdown

千问(云)

结构化结果 → 回写数据库

三、关键实现(结合代码)

3.1 入口:只触发,不执行

以过程文档接口为例:

1
pdRouter.POST("/uploadAndParse", pdApi.UploadAndParse)

设计点:

  • HTTP 只做:

    • 入参校验
    • 创建任务
    • 返回成功
  • 实际处理放到 goroutine

👉 避免接口阻塞,这是第一原则


3.2 状态控制:防重复执行

审批场景里做了关键控制:

1
2
Where(SmartAuditStatusNEQ(InProgress)).
SetSmartAuditStatus(InProgress)

核心思想:

用数据库保证“只有一个任务在跑”

效果:

  • 避免重复触发
  • 并发安全(比加锁更可靠)

3.3 异步主流程(核心链路)

以智能审核为例,实际执行逻辑可以抽象为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go func() {
// 1. 下载 PDF
// 2. 执行 MinerU
cmd := exec.CommandContext(ctx, mineruPath, args...)
out, err := cmd.CombinedOutput()

// 3. 读取 markdown
md := readMarkdown()

// 4. 调用千问
resp := callQwen(md)

// 5. 解析 JSON
risks := parse(resp)

// 6. 写数据库
}()

这里有三个关键点:


✅ 1. MinerU 用子进程执行

1
exec.CommandContext(...)

原因:

  • Python 环境隔离
  • 不污染 Go 进程
  • 可超时控制

✅ 2. LLM 只做“理解”,不做解析

1
2
输入:markdown
输出:结构化 JSON

约束模型输出:

  • 固定 JSON 格式
  • 后端严格解析

👉 避免“模型自由发挥”


✅ 3. DB 写入单独超时

不要用主 ctx:

1
ctxDB, cancel := context.WithTimeout(context.Background(), 5*time.Second)

原因:

  • 防止 MinerU/LLM 卡死导致状态写不进去

四、Docker 设计(关键)

核心 Dockerfile:

1
2
3
4
5
6
FROM xxx/alpine-builder AS builder
RUN make

FROM xxx/mineru:latest AS runner
COPY --from=builder /app /usr/bin/app
ENTRYPOINT ["app"]

为什么这么设计?

1️⃣ MinerU 不自己装

直接用基础镜像:

  • 避免 Python + OCR 依赖地狱
  • 环境统一

2️⃣ Go 只保留二进制

  • 镜像小
  • 启动快
  • 无运行时依赖

3️⃣ 本地执行 MinerU

配置:

1
mineru_path: /usr/bin/mineru

👉 容器内路径固定,避免环境差异


六、工程上的几个关键点

6.1 并发控制

  • DB 状态控制
  • 避免重复执行

五、总结(核心就三点)

这套方案真正有价值的不是“用了 AI”,而是:

1️⃣ 异步化

避免接口阻塞,提升系统稳定性

2️⃣ 本地 + 云分层

  • MinerU 本地执行
  • 千问云端推理

3️⃣ 容器固化环境

通过 Docker 保证:

  • 依赖一致
  • 部署简单

一句话总结

用容器封装 OCR,用云模型做理解,用异步任务串联整条链路