fix(exam_pkg): 判断题返回bool + 填空题扁平数组归一化
grader.py: - 判断题批阅结果 student_answer/correct_answer 统一返回 true/false (bool) 兼容 "对"/"错"/"true"/"false"/True/False/1/0 等所有输入格式 - 填空题新增 _normalize_fill_blank_answer 兜底归一化 修复 LLM 生成扁平数组 ["ans1","ans2"] 导致 1空多选项误判满分的bug - feedback 文本统一用小写 true/false generator.py: - validate_questions_schema 出题时修正扁平数组为二维格式 从源头防止填空题答案格式错误
This commit is contained in:
@@ -169,6 +169,14 @@ def validate_questions_schema(questions: List[Dict]) -> List[Dict]:
|
|||||||
if not content.get('data', {}).get('options'):
|
if not content.get('data', {}).get('options'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 填空题答案格式归一化:扁平数组 → 二维数组
|
||||||
|
if q_type == 'fill_blank':
|
||||||
|
ans = content.get('answer')
|
||||||
|
if isinstance(ans, list) and ans and all(isinstance(item, str) for item in ans):
|
||||||
|
# 扁平数组 ["答案1", "答案2"] → [["答案1"], ["答案2"]]
|
||||||
|
content['answer'] = [[item] for item in ans]
|
||||||
|
logger.warning(f"填空题答案格式修正: 扁平数组 → 二维数组 ({len(ans)} 空)")
|
||||||
|
|
||||||
validated.append(q)
|
validated.append(q)
|
||||||
|
|
||||||
return validated
|
return validated
|
||||||
|
|||||||
@@ -88,11 +88,33 @@ grading_semaphore = threading.Semaphore(MAX_CONCURRENT_GRADING)
|
|||||||
|
|
||||||
# ==================== 本地批阅函数 ====================
|
# ==================== 本地批阅函数 ====================
|
||||||
|
|
||||||
|
def _normalize_true_false(value) -> bool:
|
||||||
|
"""
|
||||||
|
将判断题的各种表示形式统一转为 bool。
|
||||||
|
|
||||||
|
支持: "对"/"错", "正确"/"错误", "true"/"false", "yes"/"no",
|
||||||
|
True/False, 1/0, "T"/"F", "1"/"0"
|
||||||
|
"""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return bool(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
v = value.strip().lower()
|
||||||
|
if v in ('对', '正确', 'true', 'yes', 't', '1'):
|
||||||
|
return True
|
||||||
|
if v in ('错', '错误', 'false', 'no', 'f', '0'):
|
||||||
|
return False
|
||||||
|
# 无法识别时返回 None,让比较逻辑走原始字符串匹配
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def grade_objective(answer: Dict) -> Dict:
|
def grade_objective(answer: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
批阅客观题(选择/判断)
|
批阅客观题(选择/判断)
|
||||||
|
|
||||||
🔥 本地直接判断,无 LLM 调用
|
🔥 本地直接判断,无 LLM 调用
|
||||||
|
判断题返回的 student_answer / correct_answer 统一为 bool (true/false)
|
||||||
"""
|
"""
|
||||||
q_type = answer['question_type']
|
q_type = answer['question_type']
|
||||||
question_content = answer.get('content', {})
|
question_content = answer.get('content', {})
|
||||||
@@ -100,17 +122,39 @@ def grade_objective(answer: Dict) -> Dict:
|
|||||||
student_answer = answer.get('student_answer')
|
student_answer = answer.get('student_answer')
|
||||||
max_score = answer.get('max_score', 2.0)
|
max_score = answer.get('max_score', 2.0)
|
||||||
|
|
||||||
# 判断正确性
|
# 判断题:归一化为 bool 比较 + bool 输出
|
||||||
if q_type == 'single_choice':
|
if q_type == 'true_false':
|
||||||
|
norm_correct = _normalize_true_false(correct_answer)
|
||||||
|
norm_student = _normalize_true_false(student_answer)
|
||||||
|
if norm_correct is not None and norm_student is not None:
|
||||||
|
correct = norm_correct == norm_student
|
||||||
|
correct_answer = norm_correct
|
||||||
|
student_answer = norm_student
|
||||||
|
else:
|
||||||
|
# 兜底:无法归一化时用原始字符串比较
|
||||||
|
correct = student_answer == correct_answer
|
||||||
|
# 仍尝试转为 bool 输出,转不了则保留原值
|
||||||
|
if norm_correct is not None:
|
||||||
|
correct_answer = norm_correct
|
||||||
|
if norm_student is not None:
|
||||||
|
student_answer = norm_student
|
||||||
|
elif q_type == 'single_choice':
|
||||||
correct = student_answer == correct_answer
|
correct = student_answer == correct_answer
|
||||||
elif q_type == 'multiple_choice':
|
elif q_type == 'multiple_choice':
|
||||||
# 多选题:答案顺序无关
|
# 多选题:答案顺序无关
|
||||||
correct = set(student_answer) == set(correct_answer) if isinstance(student_answer, list) else False
|
correct = set(student_answer) == set(correct_answer) if isinstance(student_answer, list) else False
|
||||||
elif q_type == 'true_false':
|
|
||||||
correct = student_answer == correct_answer
|
|
||||||
else:
|
else:
|
||||||
correct = False
|
correct = False
|
||||||
|
|
||||||
|
# 构造 feedback:判断题用 true/false,其他题型用原值
|
||||||
|
if not correct:
|
||||||
|
if q_type == 'true_false' and isinstance(correct_answer, bool):
|
||||||
|
feedback = f"正确答案: {'true' if correct_answer else 'false'}"
|
||||||
|
else:
|
||||||
|
feedback = f"正确答案: {correct_answer}"
|
||||||
|
else:
|
||||||
|
feedback = "正确!"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"question_id": answer.get('question_id'),
|
"question_id": answer.get('question_id'),
|
||||||
"score": max_score if correct else 0,
|
"score": max_score if correct else 0,
|
||||||
@@ -120,11 +164,49 @@ def grade_objective(answer: Dict) -> Dict:
|
|||||||
"correct": correct,
|
"correct": correct,
|
||||||
"student_answer": student_answer,
|
"student_answer": student_answer,
|
||||||
"correct_answer": correct_answer,
|
"correct_answer": correct_answer,
|
||||||
"feedback": f"正确答案: {correct_answer}" if not correct else "正确!"
|
"feedback": feedback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_fill_blank_answer(correct_answers: list, blank_count: int = 0) -> list:
|
||||||
|
"""
|
||||||
|
归一化填空题答案为二维数组 [["答案1", "同义词"], ["答案2"], ...]
|
||||||
|
|
||||||
|
修复 LLM 生成扁平数组 ["答案1", "答案2"] 的格式错误:
|
||||||
|
- 扁平数组会被误认为"1个空、多个可选答案",导致匹配一个就给满分
|
||||||
|
- 归一化后每个元素独立为空,各自占分
|
||||||
|
|
||||||
|
Args:
|
||||||
|
correct_answers: 原始答案(可能是 1D 或 2D)
|
||||||
|
blank_count: 题目声明的空数(来自 content.data.blank_count),用于辅助判断
|
||||||
|
"""
|
||||||
|
if not correct_answers:
|
||||||
|
return correct_answers
|
||||||
|
|
||||||
|
# 已经是标准二维格式:每个元素都是 list
|
||||||
|
if all(isinstance(item, list) for item in correct_answers):
|
||||||
|
return correct_answers
|
||||||
|
|
||||||
|
# 扁平数组:元素全是字符串 → 每个字符串是独立的空
|
||||||
|
if all(isinstance(item, str) for item in correct_answers):
|
||||||
|
expected_blanks = blank_count if blank_count > 0 else len(correct_answers)
|
||||||
|
logger.warning(
|
||||||
|
f"填空题答案格式修正: 扁平数组 {correct_answers!r} → 二维数组 "
|
||||||
|
f"(检测到 {len(correct_answers)} 个元素, blank_count={blank_count})"
|
||||||
|
)
|
||||||
|
return [[item] for item in correct_answers]
|
||||||
|
|
||||||
|
# 混合类型(不太可能发生),尝试兜底
|
||||||
|
result = []
|
||||||
|
for item in correct_answers:
|
||||||
|
if isinstance(item, list):
|
||||||
|
result.append(item)
|
||||||
|
else:
|
||||||
|
result.append([item])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def grade_fill_blank(answer: Dict) -> Dict:
|
def grade_fill_blank(answer: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
批阅填空题 - 支持同义词匹配
|
批阅填空题 - 支持同义词匹配
|
||||||
@@ -137,6 +219,10 @@ def grade_fill_blank(answer: Dict) -> Dict:
|
|||||||
student_answers = answer.get('student_answer', [])
|
student_answers = answer.get('student_answer', [])
|
||||||
max_score = answer.get('max_score', 4.0)
|
max_score = answer.get('max_score', 4.0)
|
||||||
|
|
||||||
|
# 归一化答案格式(修复 LLM 生成的扁平数组问题)
|
||||||
|
blank_count = question_content.get('data', {}).get('blank_count', 0)
|
||||||
|
correct_answers = _normalize_fill_blank_answer(correct_answers, blank_count)
|
||||||
|
|
||||||
if not correct_answers or not student_answers:
|
if not correct_answers or not student_answers:
|
||||||
return {
|
return {
|
||||||
"question_id": answer.get('question_id'),
|
"question_id": answer.get('question_id'),
|
||||||
|
|||||||
Reference in New Issue
Block a user