diff --git a/exam_pkg/generator.py b/exam_pkg/generator.py index 332861f..eda54c6 100644 --- a/exam_pkg/generator.py +++ b/exam_pkg/generator.py @@ -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 diff --git a/exam_pkg/grader.py b/exam_pkg/grader.py index acfbbf8..18f0e1b 100644 --- a/exam_pkg/grader.py +++ b/exam_pkg/grader.py @@ -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'),