feat: 历史背诵问答引擎 — 355题七年级中国历史题库
This commit is contained in:
parent
90ac682a06
commit
44cd6cc234
184
tutor/history_quiz.py
Normal file
184
tutor/history_quiz.py
Normal file
@ -0,0 +1,184 @@
|
||||
"""
|
||||
历史知识问答引擎 — 七年级中国历史背诵题库
|
||||
从 data/历史背诵题库.json 加载题目,支持随机出题、答题判断、进度追踪
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
QUIZ_FILE = Path(os.environ.get(
|
||||
"HISTORY_QUIZ_FILE",
|
||||
os.path.join(os.path.dirname(__file__), "..", "data", "历史背诵题库.json")
|
||||
))
|
||||
|
||||
# 单元别名映射(用于前端显示)
|
||||
UNIT_ALIASES = {
|
||||
"U1": "七上·第一单元 史前时期",
|
||||
"U2": "七上·第二单元 夏商周时期",
|
||||
"U3": "七上·第三单元 秦汉时期",
|
||||
"U4": "七上·第四单元 三国两晋南北朝",
|
||||
"U5": "七下·第一单元 隋唐时期",
|
||||
"U6": "七下·第二单元 辽宋夏金元",
|
||||
"U7": "七下·第三单元 明清时期",
|
||||
}
|
||||
|
||||
|
||||
class HistoryQuiz:
|
||||
"""历史题库引擎"""
|
||||
|
||||
def __init__(self):
|
||||
self.data = self._load_quiz()
|
||||
self.units = {u["unit_id"]: u for u in self.data.get("units", [])}
|
||||
self.all_questions = []
|
||||
for unit in self.data.get("units", []):
|
||||
for q in unit.get("questions", []):
|
||||
q["unit_id"] = unit["unit_id"]
|
||||
q["unit_title"] = unit.get("title", "")
|
||||
self.all_questions.append(q)
|
||||
|
||||
def _load_quiz(self) -> dict:
|
||||
if QUIZ_FILE.exists():
|
||||
return json.loads(QUIZ_FILE.read_text(encoding="utf-8"))
|
||||
# 降级:空题库
|
||||
return {"metadata": {}, "units": []}
|
||||
|
||||
def get_units(self) -> list:
|
||||
"""获取所有单元信息(含题目数量)"""
|
||||
result = []
|
||||
for unit in self.data.get("units", []):
|
||||
uid = unit["unit_id"]
|
||||
result.append({
|
||||
"unit_id": uid,
|
||||
"title": unit.get("title", ""),
|
||||
"grade": unit.get("grade", ""),
|
||||
"alias": UNIT_ALIASES.get(uid, uid),
|
||||
"question_count": len(unit.get("questions", [])),
|
||||
"lessons": unit.get("lessons", []),
|
||||
})
|
||||
return result
|
||||
|
||||
def get_random_question(self, unit_id: str = None, question_type: str = None) -> dict:
|
||||
"""随机获取一道题,可选按单元/题型过滤"""
|
||||
pool = self.all_questions
|
||||
if unit_id:
|
||||
pool = [q for q in pool if q.get("unit_id") == unit_id]
|
||||
if question_type:
|
||||
pool = [q for q in pool if q.get("type") == question_type]
|
||||
if not pool:
|
||||
return {"error": "该条件下没有题目"}
|
||||
q = random.choice(pool)
|
||||
return {
|
||||
"id": q["id"],
|
||||
"type": q["type"],
|
||||
"question": q["question"],
|
||||
"answer": q["answer"],
|
||||
"unit_id": q["unit_id"],
|
||||
"unit_title": q.get("unit_title", ""),
|
||||
"unit_alias": UNIT_ALIASES.get(q["unit_id"], q["unit_id"]),
|
||||
"source": q.get("source", ""),
|
||||
}
|
||||
|
||||
def get_batch(self, count: int = 10, unit_id: str = None, question_type: str = None) -> list:
|
||||
"""批量获取题目(不重复)"""
|
||||
pool = list(self.all_questions)
|
||||
if unit_id:
|
||||
pool = [q for q in pool if q.get("unit_id") == unit_id]
|
||||
if question_type:
|
||||
pool = [q for q in pool if q.get("type") == question_type]
|
||||
random.shuffle(pool)
|
||||
selected = pool[:count]
|
||||
return [{
|
||||
"id": q["id"],
|
||||
"type": q["type"],
|
||||
"question": q["question"],
|
||||
"answer": q["answer"],
|
||||
"unit_id": q["unit_id"],
|
||||
"unit_title": q.get("unit_title", ""),
|
||||
"unit_alias": UNIT_ALIASES.get(q["unit_id"], q["unit_id"]),
|
||||
} for q in selected]
|
||||
|
||||
def check_answer(self, question_id: str, student_answer: str) -> dict:
|
||||
"""检查答案 — 模糊匹配(关键词匹配)"""
|
||||
q = next((q for q in self.all_questions if q["id"] == question_id), None)
|
||||
if not q:
|
||||
return {"correct": False, "error": "题目不存在"}
|
||||
|
||||
correct = q["answer"].strip()
|
||||
student = student_answer.strip()
|
||||
|
||||
# 精确匹配
|
||||
if student == correct:
|
||||
return {
|
||||
"correct": True,
|
||||
"question_id": question_id,
|
||||
"student_answer": student,
|
||||
"correct_answer": correct,
|
||||
"match_type": "exact"
|
||||
}
|
||||
|
||||
# 包含匹配(学生答案包含在标准答案中,或反之)
|
||||
if len(student) >= 2 and (student in correct or correct in student):
|
||||
return {
|
||||
"correct": True,
|
||||
"question_id": question_id,
|
||||
"student_answer": student,
|
||||
"correct_answer": correct,
|
||||
"match_type": "partial"
|
||||
}
|
||||
|
||||
# 关键词匹配(答案拆分为关键词,匹配度>60%算对)
|
||||
keywords = set(correct.replace(",", " ").replace("、", " ").replace(";", " ").split())
|
||||
student_words = set(student.replace(",", " ").replace("、", " ").replace(";", " ").split())
|
||||
if keywords and student_words:
|
||||
overlap = len(keywords & student_words) / len(keywords)
|
||||
if overlap >= 0.6:
|
||||
return {
|
||||
"correct": True,
|
||||
"question_id": question_id,
|
||||
"student_answer": student,
|
||||
"correct_answer": correct,
|
||||
"match_type": "keyword",
|
||||
"keyword_match": f"{overlap:.0%}"
|
||||
}
|
||||
|
||||
return {
|
||||
"correct": False,
|
||||
"question_id": question_id,
|
||||
"student_answer": student,
|
||||
"correct_answer": correct,
|
||||
"hint": self._generate_hint(correct)
|
||||
}
|
||||
|
||||
def _generate_hint(self, answer: str) -> str:
|
||||
"""生成提示(显示答案首字+长度)"""
|
||||
if len(answer) <= 1:
|
||||
return "答案很短,再想想?"
|
||||
first_char = answer[0]
|
||||
return f"提示:答案以「{first_char}」开头,共{len(answer)}个字"
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""题库统计"""
|
||||
type_counts = {}
|
||||
for q in self.all_questions:
|
||||
t = q.get("type", "unknown")
|
||||
type_counts[t] = type_counts.get(t, 0) + 1
|
||||
|
||||
return {
|
||||
"total_questions": len(self.all_questions),
|
||||
"total_units": len(self.units),
|
||||
"by_type": type_counts,
|
||||
"by_unit": {uid: len(u.get("questions", [])) for uid, u in self.units.items()},
|
||||
}
|
||||
|
||||
|
||||
# 全局单例
|
||||
_quiz_instance = None
|
||||
|
||||
def get_quiz() -> HistoryQuiz:
|
||||
global _quiz_instance
|
||||
if _quiz_instance is None:
|
||||
_quiz_instance = HistoryQuiz()
|
||||
return _quiz_instance
|
||||
Loading…
x
Reference in New Issue
Block a user