AI + 教育 栏目

基于 MinerU 与 LLM 的试卷/教辅自动化抽取方案

2026年10月04日    43 次浏览    AI + 教育

基于 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 页面,形成完整的数据飞轮闭环。

上一篇 / 下一篇