修正 AI 复盘解读:按日期顺序、得分率与科目专属建议。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,26 +37,57 @@ def _subject_display_name(score: SubjectScore) -> str:
|
||||
return score.subject.name if score.subject else f"科目{score.subject_id}"
|
||||
|
||||
|
||||
def _build_review_records_text(exams: list[ExamRecord], subject_name: str) -> str:
|
||||
lines: list[str] = []
|
||||
for exam in sorted(exams, key=lambda item: item.exam_date, reverse=True):
|
||||
def _build_review_insight_context(exams: list[ExamRecord], subject_name: str) -> str:
|
||||
entries: list[tuple[ExamRecord, SubjectScore, list[ReviewStatusEnum]]] = []
|
||||
status_counts = {status: 0 for status in ReviewStatusEnum}
|
||||
|
||||
for exam in exams:
|
||||
for score in exam.scores:
|
||||
if _subject_display_name(score) != subject_name:
|
||||
continue
|
||||
statuses = _parse_review_statuses(score.review_statuses_json)
|
||||
if not statuses:
|
||||
continue
|
||||
entries.append((exam, score, statuses))
|
||||
for status in statuses:
|
||||
status_counts[status] += 1
|
||||
|
||||
if not entries:
|
||||
return ""
|
||||
|
||||
entries.sort(key=lambda item: (item[0].exam_date, item[0].created_at))
|
||||
total = len(entries)
|
||||
|
||||
summary_parts = [
|
||||
f"{_REVIEW_STATUS_LABELS[s]}{status_counts[s]}次"
|
||||
for s in ReviewStatusEnum
|
||||
if status_counts[s] > 0
|
||||
]
|
||||
lines = [
|
||||
f"科目:{subject_name}",
|
||||
f"共 {total} 次有复盘记录的考试",
|
||||
f"状态统计:{'、'.join(summary_parts)}",
|
||||
"",
|
||||
"【按考试日期从早到晚,请严格按此顺序解读,不得颠倒】",
|
||||
]
|
||||
|
||||
for index, (exam, score, statuses) in enumerate(entries, start=1):
|
||||
type_label = _EXAM_TYPE_LABELS.get(exam.exam_type.value, exam.exam_type.value)
|
||||
status_text = "、".join(_REVIEW_STATUS_LABELS.get(s, s.value) for s in statuses)
|
||||
obtained = float(score.obtained_score)
|
||||
total_score = float(score.total_score)
|
||||
ratio = float(score.ratio) * 100
|
||||
lost = total_score - obtained
|
||||
when = "(最近一次)" if index == total else ""
|
||||
line = (
|
||||
f"- {exam.exam_date} {type_label} "
|
||||
f"得分{float(score.obtained_score):g}/{float(score.total_score):g}({ratio:.1f}%) "
|
||||
f"状态:{status_text}"
|
||||
f"第{index}次{when} | 日期 {exam.exam_date} | {type_label} | "
|
||||
f"得分 {obtained:g}/{total_score:g}(得分率 {ratio:.1f}%,失分 {lost:g} 分)| "
|
||||
f"复盘状态:{status_text}"
|
||||
)
|
||||
if exam.title:
|
||||
line += f" 备注:{exam.title}"
|
||||
line += f" | 备注:{exam.title}"
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -298,7 +329,7 @@ async def review_insight(
|
||||
.all()
|
||||
)
|
||||
subject_name = data.subject_name.strip()
|
||||
records = _build_review_records_text(exams, subject_name)
|
||||
records = _build_review_insight_context(exams, subject_name)
|
||||
if not records:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
+84
-16
@@ -89,29 +89,89 @@ ERROR_DETECT_PROMPT = """你是{stage}{subject}老师。以下是试卷/作业 O
|
||||
若整张图就是一道错题,请标注含有错误答案或作答的行;找不到则标注最后作答行。
|
||||
"""
|
||||
|
||||
REVIEW_INSIGHT_PROMPT = """你是一位{stage}{subject}学习顾问。根据学生历次考试的复盘状态,给出解读与可执行建议。
|
||||
REVIEW_INSIGHT_PROMPT = """你是一位{stage}{subject}学习顾问。请仅根据下方「复盘数据」做分析,不得编造未出现的考试或状态。
|
||||
|
||||
【学段】{stage},科目:{subject}
|
||||
【学段】{stage}
|
||||
【科目】{subject}(所有建议必须贴合本科目,禁止套用其他科目的说法)
|
||||
|
||||
历次复盘记录(按时间从新到旧):
|
||||
【复盘数据】
|
||||
{review_records}
|
||||
|
||||
状态说明:粗心=审题/计算失误;不会=知识点未掌握;紧张=心态影响发挥;正常发挥=状态良好。
|
||||
【状态含义(结合本科目理解)】
|
||||
- 粗心:{careless_hint}
|
||||
- 不会:该科知识点/题型尚未掌握
|
||||
- 紧张:心态影响,发挥低于平时水平
|
||||
- 正常发挥:状态稳定
|
||||
|
||||
请用 Markdown 输出,结构如下:
|
||||
【科目建议方向】
|
||||
{subject_hints}
|
||||
|
||||
【必须遵守】
|
||||
1. 解读时必须写清具体考试日期(如 2026-06-21),按时间从早到晚分析,不得把「第1次」说成最近一场
|
||||
2. 得分率 = 得分÷总分;95% 以上才可称「接近满分」,85% 左右应如实描述为「良好但仍有失分空间」,禁止夸大
|
||||
3. 改进建议必须针对 {subject},禁止出现与本科目无关的表述(如英语科禁止写「计算验算」)
|
||||
4. 只分析数据中列出的复盘状态,不要臆测未勾选的原因
|
||||
|
||||
请用 Markdown 输出:
|
||||
|
||||
## 情况解读
|
||||
(2-4 句话:从成绩与状态看出什么规律或趋势)
|
||||
(2-4 句:按时间顺序说明每次考试得分率、失分与复盘状态的关系,以及是否有改善或反复)
|
||||
|
||||
## 改进建议
|
||||
(3-5 条具体可执行建议,针对出现最多的问题状态,适合{stage}学生)
|
||||
(3-5 条,针对出现最多的问题状态,具体可操作,仅限 {stage}{subject} 范围)
|
||||
|
||||
## 近期重点
|
||||
(1-2 条本周可落实的小目标)
|
||||
|
||||
语气亲切、务实,不要空泛鸡汤;不要超纲推荐学习内容。
|
||||
语气务实,不要空泛鸡汤。
|
||||
"""
|
||||
|
||||
SUBJECT_REVIEW_HINTS: dict[str, dict[str, str]] = {
|
||||
"语文": {
|
||||
"careless": "看错题干、漏读要求、作文偏题或漏写要点",
|
||||
"hints": "阅读审题、文言文/语言运用、作文结构与素材积累",
|
||||
},
|
||||
"数学": {
|
||||
"careless": "审题不清、计算或抄错、步骤跳步",
|
||||
"hints": "错题归类、计算验算、典型题型归纳与限时练习",
|
||||
},
|
||||
"英语": {
|
||||
"careless": "看错词义/时态、漏读题干、拼写与语法笔误",
|
||||
"hints": "词汇语法、阅读完形、听力与写作模板,禁止建议计算类训练",
|
||||
},
|
||||
"物理": {
|
||||
"careless": "审题漏条件、公式代错、单位换算失误",
|
||||
"hints": "概念理解、建模分析、实验题与计算规范",
|
||||
},
|
||||
"化学": {
|
||||
"careless": "方程式配平/条件遗漏、计算失误",
|
||||
"hints": "方程式、物质性质、实验与推断题",
|
||||
},
|
||||
"生物": {
|
||||
"careless": "概念混淆、漏答得分点",
|
||||
"hints": "教材概念、图表分析、实验设计表述",
|
||||
},
|
||||
"历史": {
|
||||
"careless": "材料题漏读、时间/人物混淆",
|
||||
"hints": "时间线、材料分析、论述题答题模板",
|
||||
},
|
||||
"地理": {
|
||||
"careless": "读图漏信息、术语使用不当",
|
||||
"hints": "地图判读、区域分析、综合题答题条理",
|
||||
},
|
||||
"政治": {
|
||||
"careless": "漏答采分点、概念表述不准",
|
||||
"hints": "时政结合、材料分析、观点表述规范",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _subject_review_hints(subject: str) -> tuple[str, str]:
|
||||
block = SUBJECT_REVIEW_HINTS.get(subject)
|
||||
if block:
|
||||
return block["careless"], block["hints"]
|
||||
return "审题或作答细节失误", f"针对{subject}常见失分点制定练习与错题巩固"
|
||||
|
||||
class AIConfig:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -151,16 +211,21 @@ def load_ai_config(db: Session) -> AIConfig:
|
||||
)
|
||||
|
||||
|
||||
async def _ollama_generate(prompt: str, cfg: AIConfig) -> str:
|
||||
async def _ollama_generate(prompt: str, cfg: AIConfig, *, temperature: float = 0.3) -> str:
|
||||
url = f"{cfg.ollama_base_url.rstrip('/')}/api/generate"
|
||||
payload = {"model": cfg.ollama_model, "prompt": prompt, "stream": False}
|
||||
payload = {
|
||||
"model": cfg.ollama_model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": temperature},
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=180.0) as client:
|
||||
response = await client.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
return (response.json().get("response") or "").strip()
|
||||
|
||||
|
||||
async def _openai_generate(prompt: str, cfg: AIConfig) -> str:
|
||||
async def _openai_generate(prompt: str, cfg: AIConfig, *, temperature: float = 0.3) -> str:
|
||||
if not cfg.openai_api_key:
|
||||
raise ValueError("未配置 OpenAI API Key")
|
||||
url = f"{cfg.openai_base_url.rstrip('/')}/chat/completions"
|
||||
@@ -168,7 +233,7 @@ async def _openai_generate(prompt: str, cfg: AIConfig) -> str:
|
||||
payload = {
|
||||
"model": cfg.openai_model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0.3,
|
||||
"temperature": temperature,
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=180.0) as client:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
@@ -177,10 +242,10 @@ async def _openai_generate(prompt: str, cfg: AIConfig) -> str:
|
||||
return (data["choices"][0]["message"]["content"] or "").strip()
|
||||
|
||||
|
||||
async def generate_text(prompt: str, cfg: AIConfig) -> str:
|
||||
async def generate_text(prompt: str, cfg: AIConfig, *, temperature: float = 0.3) -> str:
|
||||
if cfg.provider == "openai":
|
||||
return await _openai_generate(prompt, cfg)
|
||||
return await _ollama_generate(prompt, cfg)
|
||||
return await _openai_generate(prompt, cfg, temperature=temperature)
|
||||
return await _ollama_generate(prompt, cfg, temperature=temperature)
|
||||
|
||||
|
||||
async def format_question(
|
||||
@@ -233,9 +298,12 @@ async def generate_review_insight(
|
||||
school_level=None,
|
||||
) -> str:
|
||||
stage = school_level_label(school_level)
|
||||
careless_hint, subject_hints = _subject_review_hints(subject)
|
||||
prompt = REVIEW_INSIGHT_PROMPT.format(
|
||||
stage=stage,
|
||||
subject=subject,
|
||||
review_records=review_records,
|
||||
careless_hint=careless_hint,
|
||||
subject_hints=subject_hints,
|
||||
)
|
||||
return await generate_text(prompt, cfg)
|
||||
return await generate_text(prompt, cfg, temperature=0.2)
|
||||
Reference in New Issue
Block a user