feat: add adversarial_verdicts table with resume and terminal-verdict query
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,28 @@ _DDL_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_qgi_task_type ON question_gen_items(task_type);",
|
||||
]
|
||||
|
||||
_DDL_VERDICTS = """
|
||||
CREATE TABLE IF NOT EXISTS adversarial_verdicts (
|
||||
question_id TEXT NOT NULL,
|
||||
question_hash TEXT NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
round INTEGER NOT NULL,
|
||||
agent_prediction TEXT,
|
||||
agent_correct INTEGER,
|
||||
verdict TEXT NOT NULL,
|
||||
pair_id TEXT,
|
||||
agent_config TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (question_id, question_hash, stage)
|
||||
);
|
||||
"""
|
||||
|
||||
_DDL_VERDICTS_INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_av_qid ON adversarial_verdicts(question_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_av_verdict ON adversarial_verdicts(verdict);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_av_round ON adversarial_verdicts(round);",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Store 实现
|
||||
@@ -176,6 +198,9 @@ class QuestionGenStore:
|
||||
self._conn.execute(_DDL_ITEMS)
|
||||
for idx_sql in _DDL_INDEXES:
|
||||
self._conn.execute(idx_sql)
|
||||
self._conn.execute(_DDL_VERDICTS)
|
||||
for idx_sql in _DDL_VERDICTS_INDEXES:
|
||||
self._conn.execute(idx_sql)
|
||||
self._conn.commit()
|
||||
|
||||
# 幂等迁移:为已有表加 sub_pattern / selector_scores 列
|
||||
@@ -382,6 +407,166 @@ class QuestionGenStore:
|
||||
if cursor.rowcount == 0:
|
||||
raise ValueError(f"item_id 不存在: {item_id}")
|
||||
|
||||
def record_verdict(
|
||||
self,
|
||||
*,
|
||||
question_id: str,
|
||||
question_hash: str,
|
||||
stage: str,
|
||||
round: int, # noqa: A002 — 与设计列名一致,仅 kwargs 传入无遮蔽风险
|
||||
agent_prediction: str | None,
|
||||
agent_correct: bool | None,
|
||||
verdict: str,
|
||||
pair_id: str | None,
|
||||
agent_config: str,
|
||||
) -> None:
|
||||
"""写入一条 agent 门判定(同 (question_id, question_hash, stage) upsert)。
|
||||
|
||||
每次写入立即 commit,保证崩溃安全(进程中断最多丢失当前未提交的一条)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
question_id, question_hash, stage : str
|
||||
续跑主键三元组(stage ∈ cheat|flip_original|flip_mirror)。
|
||||
round : int
|
||||
过滤轮次。
|
||||
agent_prediction : str | None
|
||||
agent 预测答案字母。
|
||||
agent_correct : bool | None
|
||||
作弊门是否答对(翻转门 stage 可为 None)。
|
||||
verdict : str
|
||||
passed | filtered_too_easy | filtered_no_flip | flip_skipped。
|
||||
pair_id : str | None
|
||||
关联原题与镜像题。
|
||||
agent_config : str
|
||||
agent 配置指纹(skill_mode/max_steps/model)。
|
||||
"""
|
||||
self._conn.execute(
|
||||
"""
|
||||
INSERT INTO adversarial_verdicts
|
||||
(question_id, question_hash, stage, round, agent_prediction,
|
||||
agent_correct, verdict, pair_id, agent_config)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(question_id, question_hash, stage) DO UPDATE SET
|
||||
round=excluded.round,
|
||||
agent_prediction=excluded.agent_prediction,
|
||||
agent_correct=excluded.agent_correct,
|
||||
verdict=excluded.verdict,
|
||||
pair_id=excluded.pair_id,
|
||||
agent_config=excluded.agent_config,
|
||||
created_at=datetime('now')
|
||||
""",
|
||||
(
|
||||
question_id, question_hash, stage, round, agent_prediction,
|
||||
None if agent_correct is None else int(agent_correct),
|
||||
verdict, pair_id, agent_config,
|
||||
),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def completed_stages(
|
||||
self, question_id: str, question_hash: str, agent_config: str
|
||||
) -> set[str]:
|
||||
"""返回该题在当前 hash+config 下已完成的 stage 集合(续跑用)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
question_id : str
|
||||
题目唯一标识。
|
||||
question_hash : str
|
||||
当前题面指纹;hash 不匹配的旧行视为未完成,需重跑试答。
|
||||
agent_config : str
|
||||
当前 agent 配置指纹。
|
||||
|
||||
Returns
|
||||
-------
|
||||
set[str]
|
||||
已完成的 stage 名称集合。
|
||||
"""
|
||||
rows = self._conn.execute(
|
||||
"SELECT stage FROM adversarial_verdicts "
|
||||
"WHERE question_id=? AND question_hash=? AND agent_config=?",
|
||||
(question_id, question_hash, agent_config),
|
||||
).fetchall()
|
||||
return {r[0] for r in rows}
|
||||
|
||||
def invalidate_stale_config(self, question_id: str, agent_config: str) -> None:
|
||||
"""agent_config 变化时,删除该题所有非当前 config 的旧 verdict。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
question_id : str
|
||||
题目唯一标识。
|
||||
agent_config : str
|
||||
当前 agent 配置指纹;保留该 config 行,其余全部删除。
|
||||
"""
|
||||
self._conn.execute(
|
||||
"DELETE FROM adversarial_verdicts "
|
||||
"WHERE question_id=? AND agent_config!=?",
|
||||
(question_id, agent_config),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def cheat_agent_accuracy(self, round_no: int) -> float:
|
||||
"""某轮作弊门 agent 正确率(agent_correct 聚合),无数据返 0.0。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
round_no : int
|
||||
过滤轮次。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
该轮 stage='cheat' 的 agent_correct 平均值;无数据时返回 0.0。
|
||||
"""
|
||||
row = self._conn.execute(
|
||||
"SELECT AVG(agent_correct) FROM adversarial_verdicts "
|
||||
"WHERE stage='cheat' AND round=?",
|
||||
(round_no,),
|
||||
).fetchone()
|
||||
return float(row[0]) if row and row[0] is not None else 0.0
|
||||
|
||||
def final_passed_question_ids(
|
||||
self, hash_by_qid: dict[str, str], agent_config: str
|
||||
) -> set[str]:
|
||||
"""在当前 hash+config 下通过两门的 question_id 集合(final JSON 全量重建用)。
|
||||
|
||||
终判规则(防 stale 泄漏):仅当该题在 **当前 question_hash + 当前
|
||||
agent_config** 下同时满足——存在 stage='cheat' 且 verdict='passed'
|
||||
(agent 答错=不太简单),且不存在任何 stage 的 verdict='filtered_no_flip'
|
||||
(未被翻转门剔除)——才计入 final-passed。stale hash / stale config 的旧行
|
||||
因不匹配传入的 (qid, hash, config) 天然被排除,绝不泄漏进最终题库。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hash_by_qid : dict[str, str]
|
||||
question_id → 当前 question_hash 映射(来自本轮 all_questions)。
|
||||
agent_config : str
|
||||
当前 agent 配置指纹。
|
||||
|
||||
Returns
|
||||
-------
|
||||
set[str]
|
||||
终判 passed 的 question_id 集合。
|
||||
"""
|
||||
passed: set[str] = set()
|
||||
for qid, qhash in hash_by_qid.items():
|
||||
rows = self._conn.execute(
|
||||
"SELECT stage, verdict FROM adversarial_verdicts "
|
||||
"WHERE question_id=? AND question_hash=? AND agent_config=?",
|
||||
(qid, qhash, agent_config),
|
||||
).fetchall()
|
||||
if not rows:
|
||||
continue
|
||||
cheat_passed = any(
|
||||
stage == "cheat" and verdict == "passed" for stage, verdict in rows
|
||||
)
|
||||
no_flip = any(verdict == "filtered_no_flip" for _, verdict in rows)
|
||||
if cheat_passed and not no_flip:
|
||||
passed.add(qid)
|
||||
return passed
|
||||
|
||||
def update_selector_scores(self, item_id: str, selector_scores_json: str) -> None:
|
||||
"""写入 grounded selector 打分观测(JSON 字符串)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user