Compare commits
2 Commits
a61eeb3e3f
...
2f7ee02b48
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f7ee02b48 | ||
|
|
a3aa6cbcf3 |
1276
docs/出题批卷系统设计.md
1276
docs/出题批卷系统设计.md
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user