fix: address whole-impl review (INFRA T0 rows, reproducible manifest, evolution_target report, dead config, canonical DRY)
C-1: persist_infra_t0_rows 补 INFRA/空预测错题的 T0 信号行(不进诊断故须单独落库),run_pipeline 加 Phase 0,dry-run 用假数据走通。 C-2: CLI 加 --generated-at,真实运行默认盖真实 UTC now,可显式固定以字节级复现 manifest。 I-1: coverage_report 增 evolution_target_distribution(T2 信号按 tool/skill/system 计数)。 I-2: 删除 PoolConfig 死字段 n_trainval/floor_k/epsilon/report_floor/val_wrong_min(grep 确认无消费者,视频级切分用独立 VideoSplitConfig/SplitBuildConfig/SelectConfig)。 I-3: 抽共享 load_canonical_predictions(db_path, run_id),CLI 与 build_split 共用;消除 canonical 取行 + correct 判定重复。 M-1: build_split docstring 注明 val_wrong_min-agnostic 契约(McNemar 护栏由 CLI 冻结后执行,Task 11 契约)。
This commit is contained in:
+122
-48
@@ -5,6 +5,14 @@
|
||||
Phase 2 冻结切分(build_split,纯 code-controlled,产出 pools.json + manifest)→
|
||||
McNemar 功效护栏(validation 池错题数达阈校验)。
|
||||
|
||||
复现锚点约定(C-2):
|
||||
- pools.json 的内容(+ seed + diag_fingerprint)是切分的**复现锚点**——相同输入
|
||||
产出字节级相同的 pools.json 与 pools_sha256。
|
||||
- manifest 的 generated_at 是**溯源元数据**,非复现锚点:真实运行默认盖真实 UTC
|
||||
now(记录本次切分何时产出),但可用 `--generated-at <ISO>` 显式固定,以对
|
||||
manifest 做字节级复现比对。write_manifest 库内不调 datetime.now,时间戳一律由
|
||||
本 CLI 传入。
|
||||
|
||||
设计要点:
|
||||
- 诊断口径指纹 = (诊断 prompt 版本, 模型名, git 短 SHA) 三分量合成,隔离不同
|
||||
诊断配置的信号;换 prompt / 模型 / 代码实现即换指纹,旧信号不被覆盖。
|
||||
@@ -16,7 +24,8 @@
|
||||
用于校验装配正确性(对齐 CLAUDE.md §2.5 smoke test)。
|
||||
|
||||
编排函数(run_pipeline)通过依赖注入接收 DiagnosisDeps / signal_store / wrong_ids /
|
||||
questions,便于单测用假实现替换、不触真实 LLM 与 harness.db。
|
||||
questions / canonical_preds,便于单测用假实现替换、不触真实 LLM 与 harness.db。
|
||||
其中 canonical_preds 供 Phase 0 补 INFRA / 空预测错题的 T0 信号(这些题不进诊断)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -25,7 +34,6 @@ import argparse
|
||||
import asyncio
|
||||
import datetime
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -35,9 +43,15 @@ import yaml
|
||||
from loguru import logger
|
||||
|
||||
from app.harness.baseline_diagnosis import DiagnosisDeps, run_baseline_diagnosis
|
||||
from app.harness.build_split import SplitBuildConfig, SplitBuildResult, build_split
|
||||
from app.harness.build_split import (
|
||||
SplitBuildConfig,
|
||||
SplitBuildResult,
|
||||
build_split,
|
||||
load_canonical_predictions,
|
||||
)
|
||||
from app.harness.split_selection import diag_fingerprint
|
||||
from app.question_gen.loader import load_benchmark
|
||||
from core.evolution.types import DiagnosisSignalRow
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.harness.pools import Pools
|
||||
@@ -351,57 +365,81 @@ def _load_diagnose_prompts() -> Any:
|
||||
)
|
||||
|
||||
|
||||
def _normalize_choice(choice: str | None) -> str:
|
||||
"""选项归一:strip → 大写 → 取首字母(None 归一为空串)。"""
|
||||
return (choice or "").strip().upper()[:1]
|
||||
def select_diagnosable_wrong_ids(preds: list[dict]) -> list[str]:
|
||||
"""从 canonical 预测筛出可诊断错题 question_id(保序)。
|
||||
|
||||
|
||||
def load_diagnosable_wrong_ids(harness_db: Path, baseline_run_id: str) -> list[str]:
|
||||
"""从 harness.db 读 baseline run 的可诊断错题 question_id(保序、canonical 首行)。
|
||||
|
||||
可诊断错题判据:canonical 首行(rowid 最小)预测非空 且 stop_reason 非 INFRA
|
||||
(error / parse_error)且 归一后预测 != 答案。INFRA / 空预测题不进 wrong_ids
|
||||
(run_diagnosis 内部也会二次排除,此处前置过滤减少无谓 LLM 调用)。
|
||||
可诊断错题判据:预测非空 且 stop_reason 非 INFRA(error / parse_error)且
|
||||
归一后预测 != 答案。INFRA / 空预测错题不进 wrong_ids——它们改由
|
||||
persist_infra_t0_rows 直接落 T0(run_diagnosis 内部也会二次排除同类题)。
|
||||
|
||||
参数:
|
||||
harness_db: harness.db 路径(只读打开)。
|
||||
baseline_run_id: 基线 run 标识。
|
||||
preds: load_canonical_predictions 产出的 canonical 预测行(已按 qid 去重)。
|
||||
|
||||
返回:
|
||||
可诊断错题 question_id 列表(按 rowid 升序 canonical 顺序,去重)。
|
||||
|
||||
异常:
|
||||
SystemExit: 该 run 无任何预测行(fail loud)。
|
||||
可诊断错题 question_id 列表(保 preds 顺序)。
|
||||
"""
|
||||
conn = sqlite3.connect(f"file:{harness_db}?mode=ro", uri=True)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT question_id, prediction, answer, stop_reason "
|
||||
"FROM predictions WHERE run_id = ? ORDER BY rowid",
|
||||
(baseline_run_id,),
|
||||
).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
if not rows:
|
||||
raise SystemExit(
|
||||
f"run_id={baseline_run_id} 在 {harness_db} 无任何预测行,无法诊断(P5 fail loud)"
|
||||
)
|
||||
seen: set[str] = set()
|
||||
wrong_ids: list[str] = []
|
||||
for row in rows:
|
||||
qid = row["question_id"]
|
||||
if qid in seen:
|
||||
for pred in preds:
|
||||
prediction = (pred["prediction"] or "").strip()
|
||||
if not prediction or pred["stop_reason"] in _INFRA_STOP_REASONS:
|
||||
continue
|
||||
seen.add(qid)
|
||||
prediction = (row["prediction"] or "").strip()
|
||||
if not prediction or row["stop_reason"] in _INFRA_STOP_REASONS:
|
||||
continue
|
||||
if _normalize_choice(row["prediction"]) != _normalize_choice(row["answer"]):
|
||||
wrong_ids.append(qid)
|
||||
if not pred["correct"]:
|
||||
wrong_ids.append(pred["question_id"])
|
||||
return wrong_ids
|
||||
|
||||
|
||||
def persist_infra_t0_rows(
|
||||
store: DiagnosisSignalStore,
|
||||
preds: list[dict],
|
||||
baseline_run_id: str,
|
||||
diag_fingerprint: str,
|
||||
) -> int:
|
||||
"""把非正确且 INFRA / 空预测的错题以 T0 信号行 upsert 落库(幂等)。
|
||||
|
||||
这些题(stop_reason ∈ {error, parse_error} 或预测为空)从不进入 run_diagnosis
|
||||
(筛选时被前置排除),故其 T0 信号必须在此单独补齐——否则 signal store 缺这些行,
|
||||
tier 分布 / manifest 不完整(计划要求 4 个 INFRA 空预测错题 → T0)。
|
||||
|
||||
投影口径与 baseline_diagnosis 的 INFRA 投影一致:infra=True、tier="T0"、
|
||||
error_type / cause_category / evolution_target 均 None、degraded=False;
|
||||
video_id / task_type 从 canonical 预测取。store.upsert 按主键
|
||||
(question_id, baseline_run_id, diag_fingerprint) 幂等,重复调用零副作用。
|
||||
|
||||
参数:
|
||||
store: 诊断信号存储端口(与诊断落库同一 store)。
|
||||
preds: load_canonical_predictions 产出的 canonical 预测行。
|
||||
baseline_run_id: 基线 run 标识(信号行主键之一)。
|
||||
diag_fingerprint: 诊断口径指纹(信号行主键之一)。
|
||||
|
||||
返回:
|
||||
落库的 T0 行数(供日志)。
|
||||
"""
|
||||
count = 0
|
||||
for pred in preds:
|
||||
prediction = (pred["prediction"] or "").strip()
|
||||
is_infra_or_empty = pred["stop_reason"] in _INFRA_STOP_REASONS or not prediction
|
||||
if pred["correct"] or not is_infra_or_empty:
|
||||
continue
|
||||
store.upsert(
|
||||
DiagnosisSignalRow(
|
||||
question_id=pred["question_id"],
|
||||
video_id=pred["video_id"],
|
||||
baseline_run_id=baseline_run_id,
|
||||
diag_fingerprint=diag_fingerprint,
|
||||
task_type=pred["task_type"],
|
||||
error_type=None,
|
||||
cause_category=None,
|
||||
tier="T0",
|
||||
evolution_target=None,
|
||||
degraded=False,
|
||||
infra=True,
|
||||
session_id=None,
|
||||
)
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def load_questions_by_id(questions_dir: Path) -> dict[str, GeneratedQuestion]:
|
||||
"""加载 benchmark 全部题并建 question_id → GeneratedQuestion 映射。
|
||||
|
||||
@@ -460,28 +498,34 @@ async def run_pipeline(
|
||||
signal_store: DiagnosisSignalStore,
|
||||
wrong_ids: list[str],
|
||||
questions: dict[str, GeneratedQuestion],
|
||||
canonical_preds: list[dict],
|
||||
harness_db: Path,
|
||||
questions_dir: Path,
|
||||
out_dir: Path,
|
||||
generated_at: str,
|
||||
) -> SplitBuildResult:
|
||||
"""内联两阶段:Phase 1 诊断 → Phase 2 冻结切分 → McNemar 护栏。
|
||||
"""内联三阶段:Phase 0 INFRA T0 补录 → Phase 1 诊断 → Phase 2 冻结切分 → McNemar 护栏。
|
||||
|
||||
参数:
|
||||
config: 科研旋钮快照。
|
||||
fingerprint: 诊断口径指纹(已合成,作诊断信号主键之一)。
|
||||
diagnosis_deps: Phase 1 诊断依赖束(真实或假实现)。
|
||||
signal_store: 诊断信号存储端口(Phase 1 写、Phase 2 读)。
|
||||
signal_store: 诊断信号存储端口(Phase 0/1 写、Phase 2 读)。
|
||||
wrong_ids: 待诊断的可诊断错题 question_id 列表。
|
||||
questions: question_id → GeneratedQuestion 映射。
|
||||
canonical_preds: canonical 预测行(Phase 0 从中筛 INFRA / 空预测错题补 T0)。
|
||||
harness_db: harness.db 路径(Phase 2 读 canonical 预测)。
|
||||
questions_dir: benchmark 题库目录(Phase 2 加载题库切池)。
|
||||
out_dir: 冻结产物目录(pools.json + split_manifest.json)。
|
||||
generated_at: 生成时间戳(ISO 字符串,由调用方传入保证可复现)。
|
||||
generated_at: 生成时间戳(ISO 字符串,由调用方传入;见模块 C-2 复现锚点约定)。
|
||||
|
||||
返回:
|
||||
SplitBuildResult(冻结三池 + manifest + assignment)。
|
||||
"""
|
||||
# Phase 0: INFRA / 空预测错题补 T0(这些题不进诊断,须单独落库保证 tier 分布/manifest 完整)。
|
||||
n_t0 = persist_infra_t0_rows(signal_store, canonical_preds, config.baseline_run_id, fingerprint)
|
||||
logger.info("Phase 0 INFRA T0 补录:落库 {} 行(INFRA / 空预测错题不进诊断)", n_t0)
|
||||
|
||||
# Phase 1: 离线诊断(断点续跑幂等:done_question_ids 已完成题跳过)。
|
||||
logger.info(
|
||||
"Phase 1 离线诊断:baseline={} 待诊断错题 {} 题", config.baseline_run_id, len(wrong_ids)
|
||||
@@ -548,12 +592,16 @@ def _execute_real(config: VideoSplitConfig, fingerprint: str, args: argparse.Nam
|
||||
harness_db, questions_dir, out_dir = _resolve_paths(args)
|
||||
if not harness_db.exists():
|
||||
raise SystemExit(f"harness.db 不存在: {harness_db}(P5 fail loud)")
|
||||
wrong_ids = load_diagnosable_wrong_ids(harness_db, config.baseline_run_id)
|
||||
canonical_preds = load_canonical_predictions(harness_db, config.baseline_run_id)
|
||||
wrong_ids = select_diagnosable_wrong_ids(canonical_preds)
|
||||
questions = load_questions_by_id(questions_dir)
|
||||
deps = build_diagnosis_deps(
|
||||
harness_db=harness_db, concurrency=args.concurrency, expected_model=config.model
|
||||
)
|
||||
|
||||
# generated_at:默认盖真实 UTC now(溯源用),--generated-at 可显式固定以复现(C-2)。
|
||||
generated_at = args.generated_at or datetime.datetime.now(datetime.UTC).isoformat()
|
||||
|
||||
from adapters.baseline_diagnosis_store import SqliteDiagnosisSignalStore
|
||||
|
||||
store = SqliteDiagnosisSignalStore(str(harness_db))
|
||||
@@ -566,10 +614,11 @@ def _execute_real(config: VideoSplitConfig, fingerprint: str, args: argparse.Nam
|
||||
signal_store=store,
|
||||
wrong_ids=wrong_ids,
|
||||
questions=questions,
|
||||
canonical_preds=canonical_preds,
|
||||
harness_db=harness_db,
|
||||
questions_dir=questions_dir,
|
||||
out_dir=out_dir,
|
||||
generated_at=datetime.datetime.now(datetime.UTC).isoformat(),
|
||||
generated_at=generated_at,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
@@ -637,6 +686,20 @@ def _execute_dry_run(config: VideoSplitConfig, fingerprint: str, args: argparse.
|
||||
dry_db.parent.mkdir(parents=True, exist_ok=True)
|
||||
store = SqliteDiagnosisSignalStore(str(dry_db))
|
||||
try:
|
||||
# Phase 0 装配:用一条假 INFRA 空预测走通 persist_infra_t0_rows(不触 LLM)。
|
||||
fake_infra_preds = [
|
||||
{
|
||||
"question_id": "_dry_infra",
|
||||
"video_id": "_dry_v",
|
||||
"task_type": "Counting Problem",
|
||||
"prediction": "",
|
||||
"answer": "A",
|
||||
"stop_reason": "error",
|
||||
"correct": False,
|
||||
}
|
||||
]
|
||||
n_t0 = persist_infra_t0_rows(store, fake_infra_preds, config.baseline_run_id, fingerprint)
|
||||
logger.info("Phase 0 装配 OK:persist_infra_t0_rows 落 {} 行 INFRA T0(假数据)", n_t0)
|
||||
logger.info("Phase 1 装配 OK:run_baseline_diagnosis 以空错题走早返回路径(不触 LLM)")
|
||||
asyncio.run(
|
||||
run_baseline_diagnosis(
|
||||
@@ -667,6 +730,17 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--harness-db", type=Path, default=None, dest="harness_db")
|
||||
parser.add_argument("--questions-dir", type=Path, default=None, dest="questions_dir")
|
||||
parser.add_argument("--out-dir", type=Path, default=None, dest="out_dir")
|
||||
parser.add_argument(
|
||||
"--generated-at",
|
||||
type=str,
|
||||
default=None,
|
||||
dest="generated_at",
|
||||
help=(
|
||||
"manifest generated_at 时间戳(ISO 字符串);默认盖真实 UTC now(溯源元数据)。"
|
||||
"复现锚点是 pools.json 内容 + seed + fingerprint;generated_at 可显式传入以"
|
||||
"对 manifest 做字节级复现比对。"
|
||||
),
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user