基于 MinerU 与 LLM 的试卷/教辅自动化抽取方案
基于 MinerU 与 LLM 的试卷/教辅自动化抽取方案 (混合架构版)
1. 架构概述 (Two-Pass 架构)
针对长文档 PDF(如带有目录的教辅书籍或长卷),直接整章送入大模型会导致输出 Token 截断(Output Limit)以及“中间内容迷失”(Lost in the Middle)现象。
本方案采用“基于章节做路由规划,基于单页做底层抽取”的混合架构,在保障抽取稳定性的同时,获取结构化的知识点(章节)元数据。
核心处理流
- 阶段一:目录映射 (TOC Mapping) —— 利用前序页面提取目录,计算“逻辑页码”与“物理页码”的偏移量(Offset),划定章节的物理页码边界。
- 阶段二:单页抽取 (Page-level Extraction) —— 以单页为单位,利用 MinerU 过滤手写痕迹等干扰项,并向 LLM 动态注入当前章节上下文,提取题目 JSON。
- 阶段三:跨页合并 (Post-processing) —— 在本地依据题号(
qno)进行跨页残段合并与 JSON 格式清洗。
---
2. 阶段一:目录解析与物理页码对齐
电子书的“逻辑页”(印在纸上的页码)与 MinerU 解析的“物理页”(page_no)通常存在偏差。需先计算出偏移量。
操作逻辑:
- 提取包含“目录”字样页面的文本,请求 LLM 输出目录 JSON(如:
[{"chapter": "第一章 集合", "logical_page": 1}])。 - 找到正文第一页所在的物理页码(例如物理页 8),计算偏移量:
Offset = 8 - 1 = 7。 - 推算各章节的真实物理起止页:
物理页码 = 逻辑页码 + Offset。
---
3. 阶段二:注入章节上下文的单页抽取
3.1 版面清洗规则 (基于 MinerU)
- 保留
type == "text"(印刷体)。 - 强剔除
type == "handwritten"(学生手写答案)。 - 过滤极短文本(如页码、页眉、无意义水印)。
3.2 动态注入 System Prompt
针对单页请求 LLM 时,需将阶段一获取的章节信息作为上下文注入。
你是专业的题库数据解析器。
【当前上下文】
以下 OCR 文本属于:《{动态注入的章节名称}》。
【任务】
请提取本页的全部题目,输出纯 JSON 数组。
字段要求:
* qno (题号,数字,无题号请留空或 null)
* content (题干,完整内容)
* options (选项数组,无选项则为 [])
* chapter (将当前上下文中的章节名称原样填入)
【注意】只输出纯 JSON,禁止任何解释。若遇到跨页不完整的残缺题目,请如实提取你看到的部分,切勿自行脑补。
---
4. 阶段三:后处理与跨页拼接核心代码
4.1 JSON 响应清洗与提取
兜底处理 LLM 违背指令、输出包含 Markdown 代码块或前言后语的情况。
import json
import re
def clean_llm_json(raw_text):
"""
清洗大模型返回的结果,剥离解释性废话,提取纯 JSON 数组
"""
match = re.search(r"```(?:json)?(.*?)```", raw_text, re.DOTALL | re.IGNORECASE)
json_str = match.group(1) if match else raw_text
json_str = json_str.strip()
try:
parsed_data = json.loads(json_str)
if isinstance(parsed_data, dict):
return [parsed_data]
return parsed_data if isinstance(parsed_data, list) else []
except json.JSONDecodeError as e:
print(f"JSON Decode Error, 原始文本前缀: {json_str[:50]}... Error: {e}")
return []
4.2 跨页残缺题目无缝合并
通过判断当前题号与上一题号的连续性,修复因物理分页导致的结构断裂。
def merge_cross_page_questions(all_questions):
"""
跨页题目合并逻辑:
若当前片段无题号 (qno 缺失),或题号与上一题相同,
则视为上一题的跨页延续,进行内容拼接。
"""
if not all_questions:
return []
merged_questions = []
for q in all_questions:
if not merged_questions:
merged_questions.append(q)
continue
last_q = merged_questions[-1]
current_qno = q.get("qno")
last_qno = last_q.get("qno")
# 触发合并:当前无题号(纯文本材料延续)或题号与上一题完全一致
# 同一章节下的跨页合并才绝对安全,可增加 chapter 校验
is_same_chapter = q.get("chapter") == last_q.get("chapter")
if is_same_chapter and (not current_qno or str(current_qno) == str(last_qno)):
# 拼接题干
last_q["content"] += "\n" + q.get("content", "")
# 拼接并去重选项 (防止 LLM 在两页重复输出了部分相同选项)
if q.get("options"):
existing_options = set(last_q.get("options", []))
for opt in q["options"]:
if opt not in existing_options:
last_q.setdefault("options", []).append(opt)
existing_options.add(opt)
else:
# 独立新题,正常追加
merged_questions.append(q)
return merged_questions
---
5. 工程落地选型与避坑指南
- 模型选型策略:
- 提取目录树:可采用推理能力较强的模型(如
gpt-4o或claude-3-sonnet)以保证结构解析的绝对正确。 - 单页批量抽取:任务明确、并发量大,强烈推荐高性价比模型(如
gpt-4o-mini、qwen-vl-plus、豆包-pro)。 - 超参设置:
temperature务必设置为0.1甚至0,大幅降低随机性,保证 JSON 字段的强格式化。 - 并发与限流防护:单页抽取非常适合异步并发(如 Python
asyncio+aiohttp),但需设置信号量(Semaphore)并在异常捕获中加入指数退避(Exponential Backoff)重试机制,防止触发 API 速率限制。 - 数据冗余与可追溯性:在写入数据库前,每个题目对象除了包含
chapter外,应保留page_no(来源物理页),以便于在后续前端人工复核平台中,直接高亮渲染对应 PDF 页面,形成完整的数据飞轮闭环。