chore: 同步项目状态

- main.py: 去掉 emoji 避免 GBK 编码崩溃
- docs/curl测试手册.md: 更新模型名和测试日期
- docs/代码审查报告_2026-06-05.md: 删除过期报告
- docs/main分支独有功能说明.md: 新增
- docs/出题系统逻辑.md: 新增
This commit is contained in:
lacerate551
2026-06-19 15:25:57 +08:00
parent 95f3b99064
commit 858ee40e5b
5 changed files with 1029 additions and 526 deletions

298
docs/出题系统逻辑.md Normal file
View File

@@ -0,0 +1,298 @@
# 出题系统生成逻辑
> 源码:`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. **知识点+题型去重**:题干前 30 字 + 题型相同 → 去掉。
3. **跨调用去重**`exclude_stems` 中已有题目的题干前 80 字预填入去重集合,新生成的题目如果与之冲突也会被过滤。
#### 4b. 题型平衡
`_balance_question_types(questions, target_types)` — 按题型分组,每种题型按目标数量截取(多了截断)。
#### 4c. 补题(仅 v1 结构化路径)
v1 的 `generate_questions_structured()` 有补题机制 `_makeup_questions()`:如果某题型数量不足,用前 5 个 chunks 重新出一轮补充。v2 路径依赖分配阶段的精确控制,不额外补题。
---
## 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`)。