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:
lacerate551
2026-06-30 10:27:57 +08:00
parent dd213df0e2
commit 4a262728b1
2 changed files with 99 additions and 5 deletions

View File

@@ -169,6 +169,14 @@ def validate_questions_schema(questions: List[Dict]) -> List[Dict]:
if not content.get('data', {}).get('options'):
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)
return validated

View File

@@ -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:
"""
批阅客观题(选择/判断)
🔥 本地直接判断,无 LLM 调用
判断题返回的 student_answer / correct_answer 统一为 bool (true/false)
"""
q_type = answer['question_type']
question_content = answer.get('content', {})
@@ -100,17 +122,39 @@ def grade_objective(answer: Dict) -> Dict:
student_answer = answer.get('student_answer')
max_score = answer.get('max_score', 2.0)
# 判断正确性
if q_type == 'single_choice':
# 判断题:归一化为 bool 比较 + bool 输出
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
elif q_type == 'multiple_choice':
# 多选题:答案顺序无关
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:
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 {
"question_id": answer.get('question_id'),
"score": max_score if correct else 0,
@@ -120,11 +164,49 @@ def grade_objective(answer: Dict) -> Dict:
"correct": correct,
"student_answer": student_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:
"""
批阅填空题 - 支持同义词匹配
@@ -137,6 +219,10 @@ def grade_fill_blank(answer: Dict) -> Dict:
student_answers = answer.get('student_answer', [])
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:
return {
"question_id": answer.get('question_id'),