feat(exam): 出题批卷 API 增强 — 格式统一、校验、跨调用去重
- grader: 多策略 JSON 提取 + 3 次重试 + prompt 优化,修复 LLM 输出不稳定问题 - grader: question_content → content 统一字段名,与出题接口一致 - api: 新增入参校验(题型枚举、difficulty、总题数上限 20、grade question_type) - api/generator/manager: 新增 exclude_stems 参数,支持跨调用去重
This commit is contained in:
@@ -61,6 +61,22 @@ def validate_difficulty(difficulty) -> str:
|
||||
return "difficulty 必须为 1-5 的整数"
|
||||
return None
|
||||
|
||||
|
||||
MAX_EXCLUDE_STEMS = 100
|
||||
|
||||
def validate_exclude_stems(exclude_stems) -> str:
|
||||
"""校验 exclude_stems 参数(可选)"""
|
||||
if exclude_stems is None:
|
||||
return None
|
||||
if not isinstance(exclude_stems, list):
|
||||
return "exclude_stems 必须为字符串数组"
|
||||
if len(exclude_stems) > MAX_EXCLUDE_STEMS:
|
||||
return f"exclude_stems 数量不能超过 {MAX_EXCLUDE_STEMS},当前 {len(exclude_stems)}"
|
||||
for i, stem in enumerate(exclude_stems):
|
||||
if not isinstance(stem, str) or not stem.strip():
|
||||
return f"exclude_stems 第 {i+1} 项必须为非空字符串"
|
||||
return None
|
||||
|
||||
# 创建蓝图
|
||||
exam_bp = Blueprint('exam', __name__)
|
||||
|
||||
@@ -86,6 +102,7 @@ def api_generate_questions():
|
||||
"subjective": 1
|
||||
},
|
||||
"difficulty": 3,
|
||||
"exclude_stems": ["已有题目的题干1", "已有题目的题干2"], // 可选,排除已有题目避免重复
|
||||
"options": {
|
||||
"include_explanation": true,
|
||||
"max_source_chunks": 50
|
||||
@@ -135,6 +152,20 @@ def api_generate_questions():
|
||||
if diff_error:
|
||||
return error_response("INVALID_PARAMS", BAD_REQUEST, diff_error, http_status=400)
|
||||
|
||||
# 校验总题数上限
|
||||
MAX_TOTAL_QUESTIONS = 20
|
||||
total_requested = sum(question_types.values())
|
||||
if total_requested > MAX_TOTAL_QUESTIONS:
|
||||
return error_response("INVALID_PARAMS", BAD_REQUEST,
|
||||
f"总题数不能超过 {MAX_TOTAL_QUESTIONS} 道,当前请求 {total_requested} 道",
|
||||
http_status=400)
|
||||
|
||||
# 校验排除题干列表(可选)
|
||||
exclude_stems = data.get('exclude_stems')
|
||||
stems_error = validate_exclude_stems(exclude_stems)
|
||||
if stems_error:
|
||||
return error_response("INVALID_PARAMS", BAD_REQUEST, stems_error, http_status=400)
|
||||
|
||||
# 获取当前用户
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
@@ -156,7 +187,8 @@ def api_generate_questions():
|
||||
question_types=question_types,
|
||||
difficulty=data.get('difficulty', 3),
|
||||
options=data.get('options', {}),
|
||||
request_id=data.get('request_id')
|
||||
request_id=data.get('request_id'),
|
||||
exclude_stems=data.get('exclude_stems')
|
||||
)
|
||||
|
||||
return success_response(data=result, status_code=EXAM_SUCCESS, message="出题成功")
|
||||
@@ -199,6 +231,12 @@ def api_generate_smart():
|
||||
if diff_error:
|
||||
return error_response("INVALID_PARAMS", BAD_REQUEST, diff_error, http_status=400)
|
||||
|
||||
# 校验排除题干列表(可选)
|
||||
exclude_stems = data.get('exclude_stems')
|
||||
stems_error = validate_exclude_stems(exclude_stems)
|
||||
if stems_error:
|
||||
return error_response("INVALID_PARAMS", BAD_REQUEST, stems_error, http_status=400)
|
||||
|
||||
# 获取当前用户
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
@@ -231,7 +269,8 @@ def api_generate_smart():
|
||||
question_types=question_types,
|
||||
difficulty=data.get('difficulty', 3),
|
||||
options=data.get('options', {}),
|
||||
request_id=data.get('request_id')
|
||||
request_id=data.get('request_id'),
|
||||
exclude_stems=data.get('exclude_stems')
|
||||
)
|
||||
|
||||
# 3. 在返回结果中添加 AI 分析信息
|
||||
@@ -258,21 +297,21 @@ def api_grade_answers():
|
||||
{
|
||||
"question_id": "uuid",
|
||||
"question_type": "single_choice",
|
||||
"question_content": {"answer": "B"},
|
||||
"content": {"answer": "B"},
|
||||
"student_answer": "A",
|
||||
"max_score": 2
|
||||
},
|
||||
{
|
||||
"question_id": "uuid",
|
||||
"question_type": "fill_blank",
|
||||
"question_content": {"answer": [["答案1"], ["答案2"]]},
|
||||
"content": {"answer": [["答案1", "同义词"], ["答案2"]]},
|
||||
"student_answer": ["学生答案1", "学生答案2"],
|
||||
"max_score": 4
|
||||
},
|
||||
{
|
||||
"question_id": "uuid",
|
||||
"question_type": "subjective",
|
||||
"question_content": {
|
||||
"content": {
|
||||
"stem": "简述...",
|
||||
"data": {"scoring_points": [...]},
|
||||
"answer": "参考范文..."
|
||||
@@ -283,6 +322,12 @@ def api_grade_answers():
|
||||
]
|
||||
}
|
||||
|
||||
说明:
|
||||
- content 字段与出题接口返回的 content 结构一致,后端可直接透传
|
||||
- 客观题:content 只需包含 answer
|
||||
- 填空题:content.answer 格式为 [["答案1", "同义词"], ...] 每空一个列表
|
||||
- 主观题:content 需包含 stem + answer + data.scoring_points
|
||||
|
||||
返回:
|
||||
{
|
||||
"success": true,
|
||||
@@ -312,6 +357,14 @@ def api_grade_answers():
|
||||
if not answers:
|
||||
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少答案数据", http_status=400)
|
||||
|
||||
# 校验每道题的 question_type
|
||||
for i, ans in enumerate(answers):
|
||||
q_type = ans.get('question_type')
|
||||
if q_type not in VALID_QUESTION_TYPES:
|
||||
return error_response("INVALID_PARAMS", BAD_REQUEST,
|
||||
f"第 {i+1} 题的 question_type 无效: {q_type},合法值: {', '.join(sorted(VALID_QUESTION_TYPES))}",
|
||||
http_status=400)
|
||||
|
||||
# 调用新版批题接口
|
||||
result = grade_answers(
|
||||
answers=answers,
|
||||
|
||||
Reference in New Issue
Block a user