9.9 KiB
Per-Category Pool Strategy 设计
状态: 已批准
日期: 2026-07-12
关联: app-harness-design、question-gen-v2-design
1. 动机
v2 题目生成管线为 12 个 task_type 各生成 30 题(共 360 题)。需要支持:
| 需求 | 说明 |
|---|---|
| 按类增量 baseline | 类别生成完成后立即可跑 baseline inference,无需等全量完成 |
| Per-category 分层划分 | 每类 30 题按 correctness 2:1 分为 20 train / 10 val |
| 按类训练 | --task-types 控制训练作用域,快速验证单类别效果 |
| 外部 test 池 | test 用 Video-MME 900 道真题中同类题目,不从生成题中抽取 |
| 全局训练兼容 | 全类别联合训练时 system/tool 慢进化看全量 val 信号 |
2. 方案选型
| 方案 | 描述 | 取舍 |
|---|---|---|
| A. 最小改动 | 在 build_pools 内加 if 分支 |
改动小但两条路径耦合 |
| B. 替换为纯 per-category | 删除全局模式 | 简洁但丢失 Video-MME 900 题训练能力 |
| C. Pool 工厂模式 ✓ | PoolStrategy Protocol + 具体策略 |
符合 Clean Architecture DIP/OCP;策略可独立测试替换 |
选择 C:池分割是真正易变的接缝(已有两种策略,未来可扩展),满足 CLAUDE.md 抽象引入条件。
3. 核心抽象
3.1 PoolStrategy Protocol
# app/ports.py 新增(应用层端口,非 core 层——因为返回值 Pools 定义在 app/harness/)
class PoolStrategy(Protocol):
"""池构建策略端口。"""
def build(
self,
questions: list[GeneratedQuestion],
correctness: dict[str, bool],
config: PoolConfig,
) -> Pools: ...
def build_incremental(
self,
new_task_types: list[str],
questions: list[GeneratedQuestion],
correctness: dict[str, bool],
config: PoolConfig,
) -> dict[str, dict[str, list[str]]]:
"""增量构建新类别的 train/val 划分。
返回 {task_type: {"train": [qid, ...], "val": [qid, ...]}}。
仅 PerCategoryPoolStrategy 有实质实现;GlobalPoolStrategy
不支持增量,调用时抛出 NotImplementedError。
"""
...
层级决策:
PoolStrategy放在app/ports.py而非core/protocols.py,因为返回类型Pools定义在app/harness/pools.py。若放入 core 会导致 core → app 的逆向依赖,违反 Clean Architecture 四层分层。注:
PoolConfig的两组字段(Global 系列 / PerCategory 系列)由两个策略各取所需,未使用的字段被忽略。这避免了为每个策略定义专属 config 子类的过度抽象。
3.2 PoolConfig
# core/types.py 新增
@dataclass(frozen=True)
class PoolConfig:
task_types: tuple[str, ...] | None # None = 全部类别
seed: int # 随机种子
baseline_run_id: str # 基线 run 标识(用于读 correctness、写入 pools 指纹)
# GlobalStrategy 使用
diag_size: int
diag_correct_ratio: float
val_size: int
val_correct_ratio: float
test_size: int
eval_min_per_class: int
# PerCategoryStrategy 使用
train_ratio: float # train/(train+val),默认 2/3
test_questions_dir: Path | None # 外部 test 题源路径(与 RunConfig.test_questions 对应)
3.3 两个具体策略
GlobalPoolStrategy:封装现有 build_pools 逻辑,test → val → diag 全局三分。
PerCategoryPoolStrategy:
对每个 task_type (共 N 题,目标 train=20, val=10):
correct 题 (Nc 个) → shuffle(seed)
train_correct = floor(Nc * 20/N) # 按总量 20/10 比例分配
val_correct = Nc - train_correct
wrong 题 (Nw 个) → shuffle(seed)
train_wrong = 20 - train_correct # 确保 train 总量恰好 20
val_wrong = Nw - train_wrong
assert train_correct + train_wrong == 20
assert val_correct + val_wrong == 10
diagnosis = 所有选中类别的 train 合并
validation = 所有选中类别的 val 合并
test = load_benchmark(test_questions_dir) 按 task_types 过滤
边界场景处理:
| 场景 | 行为 |
|---|---|
| 某类别全部 correct(0 wrong) | 退化为非分层 random 20/10,记录 WARNING |
| 某类别全部 wrong(0 correct) | 同上,退化为非分层 random 20/10 |
| 某类别 correctness 不完整(部分 qid 无 baseline) | fail-fast,列出缺失 qid |
| Video-MME 中某 task_type 无题 | test 池该类别为空,记录 WARNING |
4. RunConfig 变更
| 新增字段 | 类型 | 默认值 | 归属 |
|---|---|---|---|
task_types |
tuple[str, ...] | None |
None |
CLI 临时覆盖 |
pool_split_mode |
Literal["global", "per_category"] |
"global" |
科研 YAML |
train_ratio |
float |
0.667 |
科研 YAML |
test_questions |
str |
"benchmarks/Video-MME" |
科研 YAML |
default.yaml 新增:
harness:
pool_split_mode: global
train_ratio: 0.667
test_questions: "benchmarks/Video-MME"
5. 增量 Baseline Inference
5.1 流程
# 类别 A、B 完成 → 跑 baseline
python main.py harness --mode infer \
--run-id baseline_v2 --questions generated-v2-360 \
--task-types "Object Recognition" "Scene Understanding"
# 类别 C 完成 → 追加到同一 run_id
python main.py harness --mode infer \
--run-id baseline_v2 --questions generated-v2-360 \
--task-types "Action Reasoning"
# promote 为 seed
python main.py harness --mode promote \
--run-id baseline_v2 --seed baseline_v2_seed
# 按类训练
python main.py harness --mode train --fresh \
--seed baseline_v2_seed --run-id train_obj_recog_01 \
--task-types "Object Recognition" --pool-split-mode per_category
5.2 增量写入规则
| 表 | 唯一键 | 同 run_id 多次 infer 行为 |
|---|---|---|
_runs |
run_id |
upsert:更新 updated_at,保留原始 created_at |
predictions |
(run_id, question_id) |
INSERT;主键冲突报错(防重复推理) |
traces |
trace_id |
正常 INSERT(UUID 天然唯一) |
重跑同类别:若需重新推理某类别,先 DELETE 该类别的 predictions(WHERE run_id=? AND question_id IN (?)),再重新 infer。
promote 读取规则:promote_to_seed 读取指定 run_id 下全部 predictions,不区分追加批次。promote 前应确保所有目标类别均已完成 baseline。
6. 池冻结格式
6.1 per_category 格式
{
"split_mode": "per_category",
"train_ratio": 0.667,
"baseline_run_id": "baseline_v2",
"categories": {
"Object Recognition": {
"train": ["qid_1", "qid_2", "..."],
"val": ["qid_21", "..."]
}
},
"test_source": "benchmarks/Video-MME",
"questions": [...]
}
6.2 增量更新
def build_or_load_pools(
config: RunConfig,
strategy: PoolStrategy,
db_path: Path,
) -> Pools:
"""构建或加载三池。
参数:
config: 运行配置(含 task_types, pool_split_mode, baseline_run_id 等)。
strategy: 池构建策略实例。
db_path: harness.db 路径(用于读取 baseline correctness)。
流程:
1. 若 pools.json 存在 → 加载
a. 若 config.task_types 中有类别不在已冻结的 categories → 增量构建并追加
b. 若 pools.json 的 seed/train_ratio/baseline_run_id 与当前 config 不一致 → 报错
2. 若不存在 → 从 DB 加载 correctness → strategy.build() → 冻结
3. 按 config.task_types 从 categories 中组装 Pools 返回
"""
冻结一致性校验:加载已有 pools.json 时,若 split_mode、seed、train_ratio、baseline_run_id 与当前 config 不一致,直接报错并提示删除 pools.json 重建。已冻结类别的划分不可变,新类别可追加。
6.3 向后兼容
旧格式 pools.json(无 split_mode 字段)自动识别为 global 模式。
7. task_types 作用域贯穿
策略在池构建阶段完成所有过滤,下游无需感知 task_types:
| 阶段 | 行为 |
|---|---|
| 池构建 | strategy 只处理指定类别 |
| mini-batch | 消费 scoped 后的 diagnosis 池,无改动 |
| per-skill gate | 池中只有指定类别的题,自然 scoped |
| 慢进化 | 用 scoped 后的 val 池评估,照常进化 |
| held-out eval | test 池已是 Video-MME 同类题 |
8. main.py train 接线
# composition root 伪代码
elif config.mode == "train":
strategy = (PerCategoryPoolStrategy()
if config.pool_split_mode == "per_category"
else GlobalPoolStrategy())
pools = build_or_load_pools(config, strategy)
await runner.train(pools)
9. 文件改动地图
| 文件 | 改动类型 | 内容 |
|---|---|---|
app/ports.py |
新增 | PoolStrategy Protocol(应用层端口,避免 core → app 逆向依赖) |
core/types.py |
新增 | PoolConfig dataclass |
app/harness/pools.py |
重构 | GlobalPoolStrategy + PerCategoryPoolStrategy + 增量逻辑 |
app/harness/config.py |
修改 | RunConfig 新增 4 字段 + 校验;pool_split_mode 用 Literal 类型 |
config/default.yaml |
修改 | 3 个新配置项 |
main.py |
修改 | task_types 纳入 cli_overrides + RunConfig;train 接线;strategy 组装 |
app/harness/log.py |
微调 | _runs 表 INSERT OR IGNORE 改为 ON CONFLICT DO UPDATE(增量 infer 更新时间戳) |
tests/unit/test_pools.py |
新增 | 两种策略单元测试 |
tests/integration/test_pool_strategy.py |
新增 | 端到端集成测试 |
不改动
app/harness/batching.py— 消费 scoped 池,无需感知策略core/evolution/— 进化引擎与池来源解耦app/harness/inference.py— 推理逻辑不变app/question_gen/— 题目生成与池构建解耦