# Maintenance Pool 自动补入实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在 `PerCategoryPoolStrategy.build()` 的池划分前,自动从 benchmark 补入正确题以满足 `batch_correct_ratio`,并在正确率过高时发出警告。 **Architecture:** 给 `PoolConfig` 新增 `batch_correct_ratio` 字段,`PerCategoryPoolStrategy.build()` 新增 `db_path` 参数。在 Phase 2(分组)和 Phase 3(划分)之间插入 Phase 2.5:计算每个 task_type 的正确率缺口 → 从 `test_questions_dir` 加载 benchmark 同类题 → 查 DB 历史记录筛选正确题 → 补入并标记 `family="VME_MAINTENANCE"` → 正确率过高时 warning。 **Tech Stack:** Python 3.11, pytest, sqlite3 **关联设计:** `research-wiki/designs/2026-07-14-maintenance-pool-design.md` --- ### Task 1: PoolConfig 新增 batch_correct_ratio 字段 **Files:** - Modify: `core/types.py:66-96` - Modify: `app/harness/pools.py:332-364` - Test: `tests/integration/test_pool_strategy.py` - [ ] **Step 1: 写失败测试 — PoolConfig 接受 batch_correct_ratio** 在 `tests/integration/test_pool_strategy.py` 文件顶部的 `_make_question` 之后追加: ```python class TestPoolConfigBatchRatio: """PoolConfig 新增 batch_correct_ratio 字段。""" def test_default_none(self): """不传 batch_correct_ratio 时默认 None。""" config = PoolConfig( task_types=None, seed=42, baseline_run_id="bl", diag_size=0, diag_correct_ratio=0.0, val_size=0, val_correct_ratio=0.0, test_size=0, eval_min_per_class=0, train_ratio=0.667, test_questions_dir=None, ) assert config.batch_correct_ratio is None def test_explicit_value(self): """显式传入 batch_correct_ratio。""" config = PoolConfig( task_types=None, seed=42, baseline_run_id="bl", diag_size=0, diag_correct_ratio=0.0, val_size=0, val_correct_ratio=0.0, test_size=0, eval_min_per_class=0, train_ratio=0.667, test_questions_dir=None, batch_correct_ratio=0.5, ) assert config.batch_correct_ratio == 0.5 ``` - [ ] **Step 2: 运行测试验证失败** ```bash conda run -n Video-Tree-TRM pytest tests/integration/test_pool_strategy.py::TestPoolConfigBatchRatio -v ``` 预期:TypeError — PoolConfig 不接受 batch_correct_ratio。 - [ ] **Step 3: 在 PoolConfig 新增字段** 在 `core/types.py` 的 `PoolConfig` dataclass 中,在 `test_questions_dir` 之后追加: ```python batch_correct_ratio: float | None = None ``` - [ ] **Step 4: 在 _to_pool_config 中映射** 在 `app/harness/pools.py` 的 `_to_pool_config` 函数中,`return PoolConfig(...)` 调用里追加: ```python batch_correct_ratio=config.batch_correct_ratio, ``` - [ ] **Step 5: 运行测试验证通过** ```bash conda run -n Video-Tree-TRM pytest tests/integration/test_pool_strategy.py::TestPoolConfigBatchRatio tests/integration/test_pool_strategy.py::TestPerCategoryE2E -v ``` 预期:全部 PASS(新测试通过,旧测试不受影响因为字段有默认值 None)。 - [ ] **Step 6: 提交** ```bash git add core/types.py app/harness/pools.py tests/integration/test_pool_strategy.py git commit -m "feat(pools): add batch_correct_ratio field to PoolConfig" ``` --- ### Task 2: PerCategoryPoolStrategy.build 新增 db_path 参数 + Phase 2.5 maintenance 补入 **Files:** - Modify: `app/ports.py:158-163` - Modify: `app/harness/pools.py:546-624` - Modify: `app/harness/pools.py:526-536` (build_or_load_pools 调用点) - Test: `tests/integration/test_pool_strategy.py` - [ ] **Step 1: 写失败测试 — maintenance 补入核心逻辑** 在 `tests/integration/test_pool_strategy.py` 末尾追加: ```python class TestMaintenanceSupplementation: """Phase 2.5: maintenance 正确题自动补入。""" def _make_benchmark_dir(self, tmp_path: Path, task_type: str, n: int) -> Path: """创建 benchmark 题目目录(模拟 VME)。""" import json bench_dir = tmp_path / "benchmark" bench_dir.mkdir() questions = [] for i in range(n): questions.append({ "question_id": f"vme_{task_type}_{i:03d}", "video_id": f"vid_{i:03d}", "task_type": task_type, "question": f"VME Q{i}?", "options": ["A. a", "B. b", "C. c", "D. d"], "answer": "A", }) (bench_dir / "benchmark.json").write_text( json.dumps(questions, ensure_ascii=False), encoding="utf-8", ) return bench_dir def _make_db_with_correctness( self, tmp_path: Path, qids_correct: list[str], qids_wrong: list[str], ) -> "Path": """创建带历史推理记录的 harness.db。""" import sqlite3 db_path = tmp_path / "harness.db" conn = sqlite3.connect(str(db_path)) conn.execute( "CREATE TABLE IF NOT EXISTS predictions (" "run_id TEXT, timestamp TEXT, video_id TEXT, question_id TEXT, " "task_type TEXT, prediction TEXT, answer TEXT, evidence TEXT, " "reasoning TEXT, steps_used INTEGER, prompt_tokens INTEGER, " "completion_tokens INTEGER, stop_reason TEXT, steps_json JSON)" ) for qid in qids_correct: conn.execute( "INSERT INTO predictions (run_id, question_id, prediction, answer) " "VALUES (?, ?, ?, ?)", ("infer_adhoc", qid, "A", "A"), ) for qid in qids_wrong: conn.execute( "INSERT INTO predictions (run_id, question_id, prediction, answer) " "VALUES (?, ?, ?, ?)", ("infer_adhoc", qid, "B", "A"), ) conn.commit() conn.close() return db_path def test_supplements_when_correct_ratio_too_low(self, tmp_path: Path) -> None: """正确率低于 batch_correct_ratio 时自动补入。""" # 30 道训练题:5 correct, 25 wrong questions = [_make_question(f"q_{i:03d}", "Action Recognition") for i in range(30)] correctness = {q.question_id: (i < 5) for i, q in enumerate(questions)} # 40 道 benchmark 题(模拟 VME),其中 30 道在 DB 中标记为正确 bench_dir = self._make_benchmark_dir(tmp_path, "Action Recognition", 40) correct_vme_ids = [f"vme_Action Recognition_{i:03d}" for i in range(30)] wrong_vme_ids = [f"vme_Action Recognition_{i:03d}" for i in range(30, 40)] db_path = self._make_db_with_correctness(tmp_path, correct_vme_ids, wrong_vme_ids) config = PoolConfig( task_types=("Action Recognition",), seed=42, baseline_run_id="bl", diag_size=0, diag_correct_ratio=0.0, val_size=0, val_correct_ratio=0.0, test_size=0, eval_min_per_class=0, train_ratio=0.667, test_questions_dir=bench_dir, batch_correct_ratio=0.5, ) strategy = PerCategoryPoolStrategy() pools = strategy.build(questions, correctness, config, db_path=db_path) # 需补入 k = (0.5*25 - 0.5*5) / 0.5 = 20 道 total = len(pools.diagnosis) + len(pools.validation) assert total == 50 # 30 原始 + 20 补入 # 验证补入题标记 all_qs = pools.diagnosis + pools.validation maintenance_qs = [q for q in all_qs if q.family == "VME_MAINTENANCE"] assert len(maintenance_qs) == 20 # 验证 correctness 中补入题为 True for q in maintenance_qs: assert pools.correctness[q.question_id] is True def test_no_supplement_when_ratio_satisfied(self, tmp_path: Path) -> None: """正确率已满足时不补入。""" questions = [_make_question(f"q_{i:03d}", "Action Recognition") for i in range(30)] correctness = {q.question_id: (i < 20) for i, q in enumerate(questions)} config = PoolConfig( task_types=("Action Recognition",), seed=42, baseline_run_id="bl", diag_size=0, diag_correct_ratio=0.0, val_size=0, val_correct_ratio=0.0, test_size=0, eval_min_per_class=0, train_ratio=0.667, test_questions_dir=None, batch_correct_ratio=0.5, ) strategy = PerCategoryPoolStrategy() pools = strategy.build(questions, correctness, config) total = len(pools.diagnosis) + len(pools.validation) assert total == 30 # 无补入 def test_no_supplement_when_ratio_not_configured(self, tmp_path: Path) -> None: """batch_correct_ratio 未配置时不补入。""" questions = [_make_question(f"q_{i:03d}", "Action Recognition") for i in range(30)] correctness = {q.question_id: (i < 2) for i, q in enumerate(questions)} config = PoolConfig( task_types=("Action Recognition",), seed=42, baseline_run_id="bl", diag_size=0, diag_correct_ratio=0.0, val_size=0, val_correct_ratio=0.0, test_size=0, eval_min_per_class=0, train_ratio=0.667, test_questions_dir=None, batch_correct_ratio=None, ) strategy = PerCategoryPoolStrategy() pools = strategy.build(questions, correctness, config) total = len(pools.diagnosis) + len(pools.validation) assert total == 30 def test_caps_at_available_candidates(self, tmp_path: Path) -> None: """候选不足时补入全部可用,接受不完美比例。""" # 30 题全错 questions = [_make_question(f"q_{i:03d}", "Action Recognition") for i in range(30)] correctness = {q.question_id: False for q in questions} # 只有 10 道 benchmark 正确题 bench_dir = self._make_benchmark_dir(tmp_path, "Action Recognition", 10) correct_ids = [f"vme_Action Recognition_{i:03d}" for i in range(10)] db_path = self._make_db_with_correctness(tmp_path, correct_ids, []) config = PoolConfig( task_types=("Action Recognition",), seed=42, baseline_run_id="bl", diag_size=0, diag_correct_ratio=0.0, val_size=0, val_correct_ratio=0.0, test_size=0, eval_min_per_class=0, train_ratio=0.667, test_questions_dir=bench_dir, batch_correct_ratio=0.5, ) strategy = PerCategoryPoolStrategy() pools = strategy.build(questions, correctness, config, db_path=db_path) total = len(pools.diagnosis) + len(pools.validation) assert total == 40 # 30 + 10(全部可用) def test_high_correct_ratio_warning(self, tmp_path: Path) -> None: """正确率过高时发出 warning。""" from loguru import logger questions = [_make_question(f"q_{i:03d}", "Action Recognition") for i in range(30)] correctness = {q.question_id: (i < 28) for i, q in enumerate(questions)} config = PoolConfig( task_types=("Action Recognition",), seed=42, baseline_run_id="bl", diag_size=0, diag_correct_ratio=0.0, val_size=0, val_correct_ratio=0.0, test_size=0, eval_min_per_class=0, train_ratio=0.667, test_questions_dir=None, batch_correct_ratio=0.5, ) captured: list[str] = [] sink_id = logger.add(lambda msg: captured.append(str(msg)), level="WARNING") try: strategy = PerCategoryPoolStrategy() strategy.build(questions, correctness, config) finally: logger.remove(sink_id) assert any("出题可能太简单" in m for m in captured) ``` - [ ] **Step 2: 运行测试验证失败** ```bash conda run -n Video-Tree-TRM pytest tests/integration/test_pool_strategy.py::TestMaintenanceSupplementation -v ``` 预期:`test_supplements_when_correct_ratio_too_low` FAIL(build 不接受 db_path)。 - [ ] **Step 3: 修改 PoolStrategy Protocol 签名** 在 `app/ports.py` 中,修改 `PoolStrategy.build` 的签名,新增 `db_path` 关键字参数: ```python def build( self, questions: list[GeneratedQuestion], correctness: dict[str, bool], config: PoolConfig, *, db_path: _Path | None = None, ) -> Pools: ... ``` 需在文件顶部 TYPE_CHECKING 块中添加 `from pathlib import Path as _Path`(若不存在)。 - [ ] **Step 4: 修改 GlobalPoolStrategy.build 签名兼容** 在 `app/harness/pools.py` 的 `GlobalPoolStrategy.build` 中,新增 `db_path` 参数但忽略: ```python def build( self, questions: list[GeneratedQuestion], correctness: dict[str, bool], config: PoolConfig, *, db_path: Path | None = None, ) -> Pools: ``` 函数体不变。 - [ ] **Step 5: 实现 PerCategoryPoolStrategy.build 的 Phase 2.5** 在 `app/harness/pools.py` 的 `PerCategoryPoolStrategy.build` 中: **5a.** 修改签名新增 `db_path`: ```python def build( self, questions: list[GeneratedQuestion], correctness: dict[str, bool], config: PoolConfig, *, db_path: Path | None = None, ) -> Pools: ``` **5b.** 在 Phase 2(分组循环)和 Phase 3(分层划分循环)之间插入 Phase 2.5: ```python # Phase 2.5: 正确率检查 + maintenance 补入 if config.batch_correct_ratio is not None: self._check_and_supplement_maintenance( groups, correctness, config, db_path, ) ``` **5c.** 在 `PerCategoryPoolStrategy` 类中新增 `_supplement_maintenance` 方法: ```python def _check_and_supplement_maintenance( self, groups: dict[str, list[GeneratedQuestion]], correctness: dict[str, bool], config: PoolConfig, db_path: Path | None, ) -> None: """按 task_type 检查正确率,过高警告,过低则从 benchmark 补入正确题。 修改 groups 和 correctness(原地更新)。 参数: groups: task_type → 题目列表映射(原地追加补入题)。 correctness: question_id → 是否正确映射(原地追加补入题标记)。 config: 含 batch_correct_ratio 和 test_questions_dir。 db_path: harness.db 路径,用于查询 benchmark 历史推理记录。 """ import sqlite3 r = config.batch_correct_ratio # Phase 2.5a: 正确率检查(不依赖 test_questions_dir) for task_type, group in groups.items(): c = sum(1 for q in group if correctness.get(q.question_id, False)) n = len(group) ratio = c / n if n > 0 else 0.0 if ratio > 1 - r: logger.warning( "类别 {} 正确率 {:.1%} 过高(阈值 {:.1%}),出题可能太简单", task_type, ratio, 1 - r, ) # Phase 2.5b: maintenance 补入(需要 test_questions_dir) if config.test_questions_dir is None: return from app.question_gen import load_benchmark bench_questions = load_benchmark(config.test_questions_dir) # 查询 DB 中 benchmark 题的历史正确性 bench_correctness: dict[str, bool] = {} if db_path is not None and db_path.exists(): conn = sqlite3.connect(str(db_path)) bench_qids = [q.question_id for q in bench_questions] if bench_qids: placeholders = ",".join("?" for _ in bench_qids) rows = conn.execute( f"SELECT question_id, prediction, answer FROM predictions " f"WHERE question_id IN ({placeholders}) " f"ORDER BY timestamp DESC", bench_qids, ).fetchall() for qid, pred, ans in rows: if qid not in bench_correctness: bench_correctness[qid] = (pred == ans) conn.close() # 按 task_type 索引 benchmark 题 bench_by_type: dict[str, list[GeneratedQuestion]] = defaultdict(list) for q in bench_questions: bench_by_type[q.task_type].append(q) for task_type, group in groups.items(): c = sum(1 for q in group if correctness.get(q.question_id, False)) w = len(group) - c n = len(group) ratio = c / n if n > 0 else 0.0 if ratio >= r: continue # 计算需补入数 k = math.ceil((r * w - (1 - r) * c) / (1 - r)) # 筛选候选:同 task_type + DB 历史正确 + 不在当前组中 existing_ids = {q.question_id for q in group} candidates = [ q for q in bench_by_type.get(task_type, []) if bench_correctness.get(q.question_id, False) and q.question_id not in existing_ids ] if not candidates: logger.warning( "类别 {} 需补入 {} 道正确题,但 benchmark 中无可用候选", task_type, k, ) continue actual = min(k, len(candidates)) for q in candidates[:actual]: supplemented = GeneratedQuestion( question_id=q.question_id, video_id=q.video_id, task_type=q.task_type, question=q.question, options=q.options, answer=q.answer, source_nodes=q.source_nodes, difficulty=q.difficulty, family="VME_MAINTENANCE", skill_target=q.skill_target, difficulty_steps=q.difficulty_steps, ) group.append(supplemented) correctness[supplemented.question_id] = True logger.info( "类别 {} 正确率 {:.1%} < {:.1%},从 benchmark 补入 {} 道 maintenance 正确题", task_type, ratio, r, actual, ) ``` - [ ] **Step 6: 修改 build_or_load_pools 透传 db_path** 在 `app/harness/pools.py` 的 `build_or_load_pools` 函数中(约 536 行),将: ```python pools = strategy.build(questions, correctness, pool_config) ``` 改为: ```python pools = strategy.build(questions, correctness, pool_config, db_path=db_path) ``` - [ ] **Step 7: 运行测试验证通过** ```bash conda run -n Video-Tree-TRM pytest tests/integration/test_pool_strategy.py -v --tb=short ``` 预期:全部 PASS。 - [ ] **Step 8: 提交** ```bash git add app/ports.py app/harness/pools.py tests/integration/test_pool_strategy.py git commit -m "feat(pools): auto-supplement maintenance correct questions in PerCategoryPoolStrategy" ``` --- ### Task 3: 全量回归测试 + lint - [ ] **Step 1: lint** ```bash conda run -n Video-Tree-TRM ruff format app/harness/pools.py app/ports.py core/types.py tests/integration/test_pool_strategy.py conda run -n Video-Tree-TRM ruff check app/harness/pools.py app/ports.py core/types.py ``` - [ ] **Step 2: 全量测试** ```bash conda run -n Video-Tree-TRM pytest tests/unit/ tests/integration/ -q --tb=short ``` 预期:1208+ 全部 PASS - [ ] **Step 3: 提交(如有 lint 修复)** ```bash git add -A && git commit -m "chore: lint and format maintenance pool changes" ``` --- ## 核心算法保真校验 本计划不涉及核心算法迁移,保真校验不适用。 修改仅限于 `PerCategoryPoolStrategy.build()` 内部新增 Phase 2.5(补入逻辑),不改变 `_split_one_category`、`build_pools`、`GlobalPoolStrategy` 或 batching 算法的任何行为。