From ee69721ea3254f3e7a02991c5ce7e668063c3eda Mon Sep 17 00:00:00 2001 From: iomgaa Date: Thu, 16 Jul 2026 07:13:31 -0400 Subject: [PATCH] fix: filter frozen pools by requested task_types subset --- app/harness/runner.py | 8 +++++++- tests/unit/test_harness_runner.py | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/harness/runner.py b/app/harness/runner.py index ae35031..3c8b1a1 100644 --- a/app/harness/runner.py +++ b/app/harness/runner.py @@ -320,9 +320,15 @@ def _filter_untrainable_types( """ diag_by_type = Counter(u.task_type for u in build_units(pools.diagnosis)) val_by_type = Counter(u.task_type for u in build_units(pools.validation)) + # 先按调用方显式 task_types 收窄候选集:未请求的题型(哪怕可训练)不得进入 + # keep,否则冻结全局 pools 后 batch/diagnosis 会训练非请求题型,而 gate 只覆盖 + # 请求题型 → 静默语义偏差(I-4)。task_types=None 表示全部题型皆为候选。 + candidates = set(diag_by_type) | set(val_by_type) + if task_types is not None: + candidates &= set(task_types) keep: set[str] = set() dropped: list[tuple[str, str]] = [] - for tt in set(diag_by_type) | set(val_by_type): + for tt in candidates: n_val = val_by_type.get(tt, 0) n_units = diag_by_type.get(tt, 0) + n_val if n_val < eval_min_per_class: diff --git a/tests/unit/test_harness_runner.py b/tests/unit/test_harness_runner.py index 98dad2f..44f0711 100644 --- a/tests/unit/test_harness_runner.py +++ b/tests/unit/test_harness_runner.py @@ -490,6 +490,33 @@ class TestFilterUntrainableTypes: assert "P" not in new_types assert "A" in new_types + def test_task_types_subset_drops_non_requested_trainable(self) -> None: + """指定 task_types 子集时:非请求但可训练的题型也被剔除出 diag/val。 + + 回归 I-4:pools 过滤此前只按可训练性 keep、不按 task_types 收窄,导致 + 冻结全局 pools 后 batch/diagnosis 会训练非请求题型(gate 只覆盖请求题型 + → 静默语义偏差)。此处 A、B 均可训,仅请求 A,B 必须被剔除。 + """ + # 题型 A:diag=8 + val=2 → units=10、val=2,可训 + diag = [_FakeQuestion(question_id=f"A-d{i}", task_type="A") for i in range(8)] + val = [_FakeQuestion(question_id=f"A-v{i}", task_type="A") for i in range(2)] + # 题型 B:diag=8 + val=2 → units=10、val=2,同样可训(但未被请求) + diag += [_FakeQuestion(question_id=f"B-d{i}", task_type="B") for i in range(8)] + val += [_FakeQuestion(question_id=f"B-v{i}", task_type="B") for i in range(2)] + pools = _FakePools(diagnosis=diag, validation=val, test=[]) + + new_pools, new_types = _filter_untrainable_types( + pools, + task_types=["A"], + eval_min_per_class=2, + trainable_min_units=8, + ) + + # 非请求题型 B(可训)被剔除出 diag/val + assert {q.task_type for q in new_pools.diagnosis} == {"A"} + assert {q.task_type for q in new_pools.validation} == {"A"} + assert new_types == ["A"] + def test_all_filtered_raises(self) -> None: """所有题型都被剔除时 fail-fast:raise RuntimeError 并列出剔除原因。""" diag = [_FakeQuestion(question_id="B-d0", task_type="B")] # val=0 < 2