From 14cef5c9297d431de3b506f5ddedaf5f74a80101 Mon Sep 17 00:00:00 2001 From: aerospaceboy <790286734@qq.com> Date: Sat, 17 Oct 2026 20:44:24 +0800 Subject: [PATCH] perf: skip fact extraction for fully graded projects on batch retries --- backend/tests/test_optimization.py | 37 ++++++++++++++++++++++++++++++ backend/wisegrade/scoring.py | 12 ++++++++++ 2 files changed, 49 insertions(+) diff --git a/backend/tests/test_optimization.py b/backend/tests/test_optimization.py index 1c73346..283a579 100644 --- a/backend/tests/test_optimization.py +++ b/backend/tests/test_optimization.py @@ -402,6 +402,43 @@ def test_failed_model_call_cannot_overwrite_a_concurrent_teacher_review(sqlite_d assert db.get(ScoringBatch, batch.id).progress_done == 1 +@pytest.mark.parametrize("complete", [True, False]) +def test_retry_skips_facts_only_for_fully_graded_projects(sqlite_db, monkeypatch, complete): + from wisegrade import scoring + db = sqlite_db + item = project(db) + created = services.create_batch(SimpleNamespace(course_version_id=1, rubric_version_id=1), db) + batch = db.get(ScoringBatch, created["id"]) + criteria = batch.rubric_snapshot["criteria"][:2] + batch.rubric_snapshot = {**batch.rubric_snapshot, "criteria": criteria} + batch.status, batch.progress_total, batch.progress_done = "running", 2, 0 + item.facts = {"conclusions": ["已抽取并确认的事实"]} + db.add_all([ + CriterionScore(batch_id=batch.id, project_id=item.id, criterion_id=criteria[0]["criterion_id"], status="scored", model_score=0), + CriterionScore(batch_id=batch.id, project_id=item.id, criterion_id=criteria[1]["criterion_id"], + status="reviewed" if complete else "needs_review", teacher_score=0 if complete else None), + ]) + db.commit() + calls = [] + def client_factory(*args): + calls.append("client") + def facts(*args, **kwargs): + calls.append("facts") + return {} + def scored(session, batch, item, criterion, *args, **kwargs): + return session.scalar(select(CriterionScore).where(CriterionScore.batch_id == batch.id, + CriterionScore.project_id == item.id, CriterionScore.criterion_id == criterion["criterion_id"])) + monkeypatch.setattr(scoring, "_project_facts", facts) + monkeypatch.setattr(scoring, "_score_criterion", scored) + factory = sessionmaker(bind=db.get_bind(), expire_on_commit=False) + failures, reviews = scoring._run_project(batch.id, item.id, factory, client_factory, FLAT, lambda *a, **k: []) + assert calls == ([] if complete else ["client", "facts"]) + assert failures == 0 and reviews == (0 if complete else 1) + db.expire_all() + assert db.get(ScoringBatch, batch.id).progress_done == 2 + assert db.get(Project, item.id).facts == {"conclusions": ["已抽取并确认的事实"]} + + def test_unrebuilt_course_document_does_not_complete_task(sqlite_db): db = sqlite_db doc = KnowledgeDocument(course_version_id=1, title="无页面资料", document_type="course", diff --git a/backend/wisegrade/scoring.py b/backend/wisegrade/scoring.py index 52f3e09..c176e66 100644 --- a/backend/wisegrade/scoring.py +++ b/backend/wisegrade/scoring.py @@ -358,6 +358,18 @@ def _run_project(batch_id, project_id, session_factory, client_factory, embedder with session_factory() as db: batch = db.get(ScoringBatch, batch_id) project = db.get(Project, project_id) + existing = db.scalars(select(CriterionScore).where(CriterionScore.batch_id == batch_id, + CriterionScore.project_id == project_id)).all() + graded = {score.criterion_id for score in existing + if score.status in (STATUS_SCORED, STATUS_REVIEWED)} + criteria = batch.rubric_snapshot["criteria"] + if all(criterion["criterion_id"] in graded for criterion in criteria): + # A retry in a multi-student batch must not spend two more model calls + # extracting facts for a student whose scores will all be skipped. + db.execute(update(ScoringBatch).where(ScoringBatch.id == batch_id).values( + progress_done=ScoringBatch.progress_done + len(criteria))) + db.commit() + return 0, 0 try: client = client_factory(db, batch) except Exception: