Files
rag/docs/出题系统逻辑.md
lacerate551 a31ee4bba0 feat(exam_pkg): 优化出题系统稳定性与推理模型适配
- core/llm_utils: MiMo模型自动注入thinking=disabled参数,全局生效
- config: 新增LLM_DISABLE_THINKING配置项(默认true)
- generator: 推理模型自适应max_tokens(1.5x)、429限流重试+指数退避、v2管线补题机制
- generator: analyze_document_for_exam新增max_total参数控制AI出题上限
- generator: validate_questions_schema兼容type和question_type字段
- grader: 主观题max_tokens从1000提升至2000、fuzzy_match增加编辑距离容错
- manager: results变量初始化防NameError、透传max_total参数
- api: /exam/generate-smart支持max_total请求参数
2026-06-22 18:51:11 +08:00

12 KiB
Raw Permalink Blame History

出题系统生成逻辑

源码:exam_pkg/ 主入口:manager.py → generate_questions_from_file() 核心生成器:generator.py → generate_questions_structured_v2()


1. 整体架构

                        ┌─────────────────────┐
                        │   API 入口 (api.py)  │
                        │  /exam/generate      │
                        │  /exam/generate-smart│
                        └──────────┬──────────┘
                                   │ 异步任务 (task_id)
                        ┌──────────▼──────────┐
                        │  manager.py          │
                        │  generate_questions  │
                        │  _from_file()        │
                        └──────────┬──────────┘
                                   │
              ┌────────────────────▼────────────────────┐
              │        retrieve_file_chunks()            │
              │  构建语义 query → 向量检索 → 文件切片列表 │
              └────────────────────┬────────────────────┘
                                   │
              ┌────────────────────▼────────────────────┐
              │   generator.py: generate_questions_     │
              │           structured_v2()                │
              │                                          │
              │  Phase 1  文档结构分析(按章节分组)       │
              │  Phase 2  知识点规划LLM 提取 + 去重)   │
              │  Phase 3  精准出题AI分配 + 逐点检索)   │
              │  Phase 4  质量校验(去重 + 平衡 + 补题)  │
              └────────────────────┬────────────────────┘
                                   │
                        ┌──────────▼──────────┐
                        │  返回题目列表 + 溯源  │
                        └─────────────────────┘

2. API 入口

2.1 标准出题 /exam/generate

请求体:

{
  "file_path": "public_kb/产品手册.pdf",
  "collection": "public_kb",
  "question_types": {
    "single_choice": 3,
    "multiple_choice": 2,
    "true_false": 2,
    "fill_blank": 2,
    "subjective": 1
  },
  "difficulty": 3,
  "exclude_stems": ["已有题干1", "已有题干2"],
  "options": { "max_source_chunks": 50 }
}

返回 task_id,通过 GET /tasks/{task_id} 轮询结果。

2.2 智能出题 /exam/generate-smart

不传 question_types,先调用 analyze_document_for_exam() 让 LLM 分析文档后自动推荐题型和数量,再走标准生成流程。

2.3 约束

约束项
总题数上限 20 道
难度范围 1-5
exclude_stems 上限 100 条
合法题型 single_choice, multiple_choice, true_false, fill_blank, subjective

3. 切片检索Phase 0

retrieve_file_chunks() 在出题前先检索文件的相关切片:

  1. 构建语义 query:根据 question_types 自动拼装检索词。例如需要填空题会追加"术语 公式 数值",需要主观题追加"流程 步骤 原则"。
  2. 向量检索:调用 engine.search_knowledge(),按文件名过滤(source_filter),支持文件名和完整路径两种格式,支持多 collection 按优先级检索。
  3. 动态 top_kmin(50, 总题数 × 3),确保切片数量足够覆盖所有题目。

4. 四阶段生成流水线

Phase 1文档结构分析

group_chunks_by_section(chunks) — 将所有切片按 section 字段分组为 Dict[章节名, List[切片]]

清理章节名中的 ** 等标记,空章节归入"未分类"。

Phase 2知识点规划

对每个章节调用 _extract_knowledge_points(section, chunks, max_points=3)

  • 长内容≥100 字):调用 LLM 提取 3 个关键知识点短短语5-15 字prompt 要求"适合出考试题、不重复不重叠"。
  • 短内容(<100 字):直接清理后作为知识点名称,不调 LLM。

全局去重:所有知识点按 name 去重(seen_kp_names 集合),确保跨章节不重复。

每个知识点标记来源章节(kp['section']),供后续精准检索使用。

降级路径:如果所有章节都提取不出知识点,走 _generate_questions_fallback() — 把全部 chunks 拼成一个大 prompt 直接让 LLM 出题。

Phase 3精准出题

3a. AI 分配题型

_ai_assign_question_types(knowledge_points, question_types) — 按章节轮询分配"哪个知识点出什么题型"

  • 每种题型独立分配,确保题型覆盖。
  • 轮询章节,优先从不同章节选知识点。
  • 每个知识点最多出 1 道同题型题目。

输出 assignments 列表:[{"knowledge_point": "请假流程", "question_type": "single_choice", "section": "第三章"}, ...]

3b. 逐知识点出题

对每个 assignment

  1. 精准检索_retrieve_kp_chunks_v2(kp_name, section_chunks, top_k=5) — 用知识点名称做关键词匹配,在该章节的切片中评分排序,取 top 5 最相关的切片。评分规则:知识点全文匹配 +100 分,关键词匹配 +10 分,内容长度适中加分。
  2. 构建上下文build_source_context(kp_chunks) — 拼接切片内容,每个切片带 [chunk_id:xxx | 第N页 章节] 溯源标记。
  3. 构造 Prompt_build_prompt_for_kp() — 指定核心知识点、难度、题型数量,要求"必须围绕该知识点出题、每道题不同角度、严禁非 JSON 内容"。附带 5 种题型的 JSON 格式示例。
  4. 调用 LLM_generate_with_retry() — 最多重试 2 次。每次调用后 safe_parse_questions() 解析 JSON支持直接解析、提取代码块、提取数组三种方式validate_questions_schema() 校验(必须有 type/stem/answer选择题必须有 options
  5. 补充溯源_enrich_with_source_trace() — 给每道题附加 source_trace文档名、chunk_id 列表、来源信息)。

Phase 4质量校验

4a. 去重

_deduplicate_questions(questions, exclude_stems) — 四层去重:

  1. 题干前缀去重:题干前 80 字相同 → 去掉。
  2. 跨调用排除_matches_excludeexclude_stems 中已有题目的前缀≤30 字),新题干前 30 字如果以此为前缀 → 去掉。用 startswith() 匹配,解决排除题干短于 30 字时的长度不匹配问题。
  3. 知识点+题型去重:题干前 30 字 + 题型相同 → 去掉。
  4. 跨调用题干去重exclude_stems 中已有题目的题干前 80 字预填入去重集合,精确匹配过滤。

4b. 题型平衡

_balance_question_types(questions, target_types) — 按题型分组,每种题型按目标数量截取(多了截断)。

4c. 补题

v1 的 generate_questions_structured() 有补题机制 _makeup_questions():如果某题型数量不足,用前 5 个 chunks 重新出一轮补充。

v2 路径(generate_questions_structured_v2同样支持补题Phase 4.4 检查各题型是否达到目标数量,不足的题型调用 _makeup_questions() 补充,补题结果也经过去重处理。


5. 各题型的 JSON 结构

单选题

{
  "type": "single_choice",
  "content": {
    "stem": "题干内容",
    "data": { "options": [{"key": "A", "content": "..."}, ...] },
    "answer": "B",
    "explanation": "解析..."
  },
  "referenced_chunk_ids": ["chunk_001"]
}

多选题

{
  "type": "multiple_choice",
  "content": {
    "stem": "题干",
    "data": { "options": [...] },
    "answer": ["A", "C"],
    "explanation": "解析..."
  }
}

判断题

{
  "type": "true_false",
  "content": {
    "stem": "判断:某陈述",
    "data": {},
    "answer": "对",
    "explanation": "解析..."
  }
}

填空题

{
  "type": "fill_blank",
  "content": {
    "stem": "RAG的全称是___核心在于___。",
    "data": { "blank_count": 2 },
    "answer": [["检索增强生成"], ["外部知识库", "检索"]],
    "explanation": "解析..."
  }
}

answer 是二维数组:每个空一个数组,数组内元素为该空的可接受答案(第一个为标准答案,其余为同义词)。

主观题

{
  "type": "subjective",
  "content": {
    "stem": "请简述...",
    "data": {
      "scoring_points": [
        { "point": "要点1", "weight": 0.4 },
        { "point": "要点2", "weight": 0.3 },
        { "point": "要点3", "weight": 0.3 }
      ]
    },
    "answer": "参考范文...",
    "explanation": "解析..."
  }
}

6. 批题逻辑

批题入口:POST /exam/grade,同样是异步任务。

grader.py → grade_answers() 按题型分流:

题型 批阅方式 说明
single_choice / true_false 本地判分 直接比对答案,不调 LLM
multiple_choice 本地判分 set(student) == set(correct),顺序无关
fill_blank 模糊匹配 逐空比对支持同义词answer 数组中的备选项),忽略空格和标点差异
subjective LLM 评分 将题目 stem + scoring_points + 参考答案 + 学生答案一起送给 LLM按要点权重评分

并发控制:最多 3 路并发批阅(threading.Semaphore(3)),带 2 次重试。


7. 关键函数索引

函数 文件 作用
generate_questions_from_file manager.py 出题总入口
analyze_file_for_exam manager.py AI 智能分析(推荐题型)
retrieve_file_chunks manager.py 切片检索
generate_questions_structured_v2 generator.py v2 四阶段生成主流程
generate_questions_structured generator.py v1 生成主流程(含补题)
group_chunks_by_section generator.py 按章节分组
_extract_knowledge_points generator.py LLM 知识点提取
_ai_assign_question_types generator.py AI 题型分配
_retrieve_kp_chunks_v2 generator.py 知识点精准检索
_build_prompt_for_kp generator.py 构造出题 Prompt
_generate_with_retry generator.py 带重试的 LLM 调用
safe_parse_questions generator.py JSON 安全解析
validate_questions_schema generator.py 题目 Schema 校验
_enrich_with_source_trace generator.py 补充溯源信息
_deduplicate_questions generator.py 四层去重
_balance_question_types generator.py 题型数量平衡
_generate_questions_fallback generator.py 降级路径(无知识点时)
_makeup_questions generator.py v1 补题机制
grade_answers grader.py 批题总入口
grade_objective grader.py 客观题本地批阅
grade_fill_blank grader.py 填空题模糊匹配

8. LLM 调用统计

一次标准出题10 题、5 章节)的 LLM 调用次数估算:

阶段 调用次数 说明
知识点提取 ~5 次 每章节 1 次
题型分配 0 次 本地算法分配
出题 ~10 次 每知识点 1 次(含重试)
Schema 校验 0 次 本地逻辑
去重 / 平衡 0 次 本地逻辑
合计 ~15 次

智能出题额外增加 1 次 LLM 调用(analyze_document_for_exam)。