fix(exam): 修复出题批卷 API 契约一致性问题

P0:
- 修复 V2 降级路径响应格式,fallback 输出经过 enrich + dedup + balance
- 补充主观题 format example,要求 LLM 生成 scoring_points 评分标准

P1:
- 补充多选题、判断题 format example,明确 answer 为列表格式
- 统一批卷响应格式,所有题型增加 grading_status 字段
- 出题数量不足时返回 requested_types/actual_types/warnings
- 修复 _call_llm 未配置时抛异常而非静默返回 0 分

P2:
- API 层增加 question_types 和 difficulty 入参校验
- 修复 generate_questions_from_content 方法名和参数类型 bug
This commit is contained in:
lacerate551
2026-06-05 12:06:49 +08:00
parent 42434781f7
commit b6631c626b
4 changed files with 957 additions and 793 deletions

View File

@@ -32,6 +32,35 @@ from auth.gateway import (
from core.status_codes import EXAM_SUCCESS, GRADE_SUCCESS, EXAM_ERROR, GRADE_ERROR, BAD_REQUEST, NO_CONTENT, LLM_ERROR
from api.response_utils import success_response, error_response
# 合法题型
VALID_QUESTION_TYPES = {'single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'subjective'}
def validate_question_types(question_types: dict) -> str:
"""
校验 question_types 参数
Returns:
None 表示校验通过,字符串表示错误信息
"""
if not isinstance(question_types, dict):
return "question_types 必须为对象"
for q_type, count in question_types.items():
if q_type not in VALID_QUESTION_TYPES:
return f"不支持的题型: {q_type},合法值: {', '.join(sorted(VALID_QUESTION_TYPES))}"
if not isinstance(count, int) or count < 0:
return f"题型 {q_type} 的数量必须为非负整数,当前值: {count}"
return None
def validate_difficulty(difficulty) -> str:
"""校验 difficulty 参数"""
if not isinstance(difficulty, int) or difficulty < 1 or difficulty > 5:
return "difficulty 必须为 1-5 的整数"
return None
# 创建蓝图
exam_bp = Blueprint('exam', __name__)
@@ -69,12 +98,16 @@ def api_generate_questions():
"request_id": "uuid",
"questions": [
{
"metadata": {"question_id": "...", "question_type": "...", ...},
"source_trace": {"document_name": "...", "sources": [...], ...},
"content": {"stem": "...", "data": {...}, "answer": "...", ...}
"question_type": "single_choice",
"difficulty": 3,
"content": {"stem": "...", "data": {...}, "answer": "...", "explanation": "..."},
"source_trace": {"document_name": "...", "chunk_ids": [...], "sources": [...]}
}
],
"total": 10,
"requested_types": {"single_choice": 3, "fill_blank": 2},
"actual_types": {"single_choice": 3, "fill_blank": 2},
"warnings": ["fill_blank: 请求 3 道,实际生成 2 道"],
"source_chunks_used": 15
}
"""
@@ -91,6 +124,17 @@ def api_generate_questions():
if not question_types:
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少 question_types 参数", http_status=400)
# 校验题型和数量
type_error = validate_question_types(question_types)
if type_error:
return error_response("INVALID_PARAMS", BAD_REQUEST, type_error, http_status=400)
# 校验难度
difficulty = data.get('difficulty', 3)
diff_error = validate_difficulty(difficulty)
if diff_error:
return error_response("INVALID_PARAMS", BAD_REQUEST, diff_error, http_status=400)
# 获取当前用户
user = get_current_user()
if not user:
@@ -149,6 +193,12 @@ def api_generate_smart():
if not file_path or not collection:
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少 file_path 或 collection 参数", http_status=400)
# 校验难度
difficulty = data.get('difficulty', 3)
diff_error = validate_difficulty(difficulty)
if diff_error:
return error_response("INVALID_PARAMS", BAD_REQUEST, diff_error, http_status=400)
# 获取当前用户
user = get_current_user()
if not user:
@@ -237,11 +287,23 @@ def api_grade_answers():
{
"success": true,
"request_id": "uuid",
"results": [...],
"results": [
{
"question_id": "uuid",
"score": 2,
"max_score": 2,
"grading_status": "success",
"details": {"correct": true, "student_answer": "A", "correct_answer": "B", "feedback": "..."}
}
],
"total_score": 10.5,
"total_max_score": 16.0,
"score_rate": 65.6
}
grading_status 取值:
- "success": 评分成功
- "failed": 评分失败LLM 超时/解析失败等score 为 0
"""
try:
data = request.json