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

302 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 出题系统生成逻辑
> 源码:`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`
请求体:
```json
{
"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_k**`min(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_exclude``exclude_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 结构
### 单选题
```json
{
"type": "single_choice",
"content": {
"stem": "题干内容",
"data": { "options": [{"key": "A", "content": "..."}, ...] },
"answer": "B",
"explanation": "解析..."
},
"referenced_chunk_ids": ["chunk_001"]
}
```
### 多选题
```json
{
"type": "multiple_choice",
"content": {
"stem": "题干",
"data": { "options": [...] },
"answer": ["A", "C"],
"explanation": "解析..."
}
}
```
### 判断题
```json
{
"type": "true_false",
"content": {
"stem": "判断:某陈述",
"data": {},
"answer": "对",
"explanation": "解析..."
}
}
```
### 填空题
```json
{
"type": "fill_blank",
"content": {
"stem": "RAG的全称是___核心在于___。",
"data": { "blank_count": 2 },
"answer": [["检索增强生成"], ["外部知识库", "检索"]],
"explanation": "解析..."
}
}
```
`answer` 是二维数组:每个空一个数组,数组内元素为该空的可接受答案(第一个为标准答案,其余为同义词)。
### 主观题
```json
{
"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`)。