15 KiB
id, title, type, created, status
| id | title | type | created | status |
|---|---|---|---|---|
| adversarial-question-gen-phaseB | 出题质量提升 Phase B — Agent 对抗过滤(独立后置过滤层) | design | 2026-07-14 | draft |
Phase B:Agent 对抗过滤(独立后置过滤层)
1. 目标与范围
在 Phase A 的 grounded 单题基础上,加两个"用完整 inference agent 揪残余 shortcut"的机制:作弊者门(AFLite 式——agent 能秒杀的题剔除)和配对翻转门(agent 答案必须随问题翻转,否则揪出偏好蒙答)。
关键架构决策:Phase B 是 Phase A 产物 accepted_questions.json 之上的独立后置过滤层,不改 Phase A 的逐题 accepted 语义、on_accept 逐题持久化、record_item/update_gates 状态机、load_progress 断点续跑。Phase B 有自己的过滤状态表,与 Phase A 的 final_status 正交。这个选择有明确代价(§2 权衡),但换来对现有状态机的零改动。
前置依赖(硬依赖,缺则 Phase B 无法判 flip):Phase A([2026-07-14-grounded-question-gen-phaseA-design.md])已落地,且 accepted_questions.json 必须写入 sub_pattern 字段——Phase B 按题的 sub_pattern 查其 supports_flip/flip_axis(Codex 指出当前 _on_accept 未写该字段,Phase A 的"sub_pattern 持久化"必须落实,否则 Phase B 无法区分哪些题走翻转门)。
问题来源:[2026-07-14-hard-distractor-literature.md](AFLite、TempCompass QuadAcc、DMC3 before/after 问题翻转)。
路径归属
| 改动 | 代码位置 | 生效范围 |
|---|---|---|
| 作弊者门 + 配对翻转门 | 新建 adversarial_filter.py(独立过滤层) |
filter 层自己的配置 filter_task_types(默认 [Action Recognition])选择过滤哪些题型——不塞进 Phase A strategy 主流程,保持概念隔离 |
| supports_flip 声明 | SubPattern 数据类加 supports_flip/flip_axis |
仅声明的 SubPattern(当前只 AR 的 temporal/cross_segment) |
| 过滤状态表 | run_store.py 新增 adversarial_verdicts 表 |
公共(新表,不动现有 question_gen_items) |
2. 两种架构对比与权衡
Codex 审查了"改内部状态机"方案(把 accepted 时机推迟到 agent 门后),指出三个 Critical:record_item 只存 question_text 无法从 DB 重建完整 GeneratedQuestion;改 accepted 时机与 update_gates/on_accept/load_progress 三处状态机强冲突;load_progress 不返回 pending 无恢复入口。
这三个 Critical 不是靠打补丁修,而是靠换架构从根上消除——这正是本设计选独立后置过滤层的理由:
| Critical(改内部状态机方案的问题) | 独立后置层如何从根消除 |
|---|---|
| DB 只存 question_text,无法重建 GeneratedQuestion | Phase B 从 accepted_questions.json 重建(question/options/answer/source_nodes/video_id 字段完整),根本不碰 DB 重建这条路 |
| 改 accepted 时机与三处状态机冲突 | Phase B 不改 accepted 时机——Phase A 照常逐题 accepted,Phase B 在其产物之上过滤,冲突的前提(改时机)不存在 |
| load_progress 不返回 pending | Phase B 不引入 pending 状态,用独立 adversarial_verdicts 表管自己的过滤进度,不依赖 load_progress |
但独立后置层有它自己的代价(诚实标注):
| 代价 | 说明 | 缓解 |
|---|---|---|
| 两份 JSON | accepted_questions.json(便宜门过,Phase A 原始产物)不再是最终题库,最终是 accepted_questions_final.json(agent 门过)。使用者(训练/评估脚本)若读错会用到未过 agent 门的题 |
明确命名 + 文档标注训练脚本必须读 _final。不覆盖写回——覆盖写回会与补生成的 on_accept 追加产生时序打架,故保留两份、各有明确语义。这是本架构无法消除的真实代价 |
| 补生成多 run | 被过滤题需再跑 Phase A 补缺额,多次出题 run | 迭代上限 adversarial_max_rounds 兜底 |
| agent + 镜像成本翻倍 | 每题一次 agent 试答,supports_flip 的题额外一次镜像生成 + 一对试答 | 批次后置只对便宜门过的题跑,不浪费在中途废题 |
净判断:独立后置层用"两份 JSON + 补生成成本"换"现有状态机零改动 + 从完整 JSON 重建",符合"避免过度修改引入 bug"的约束。
3. 架构与数据流
flowchart TD
A[Phase A 产物 accepted_questions.json<br/>N 道便宜门过的 AR 题] --> B[adversarial_filter 后置过滤]
B --> C[作弊者门: 完整 agent 标准设定试答每题]
C -->|agent 答对=太简单| D[verdict=filtered_too_easy]
C -->|agent 答错=有效| E{supports_flip?}
E -->|否| H[verdict=passed]
E -->|是| F[现场生成镜像题 + agent 试答一对]
F -->|P/Q 答案不同=随问题翻转| H
F -->|P/Q 答案相同=没翻转/瞎选| G[verdict=filtered_no_flip]
D --> I[统计缺额]
G --> I
H --> J[写 final: accepted_questions_final.json]
I -->|缺额>0 且 轮次<上限| K[调 run_pipeline_v2 补生成<br/>避开已用节点/已过滤题]
K --> A
I -->|够数或达轮次上限| L[难度报告 + 告警]
4. 组件设计
4.1 作弊者门(adversarial_filter.py)
对 accepted_questions.json 中每道 AR 题,用完整 inference agent(标准设定,看完整题;agent 配置 skill_mode/max_steps 从训练配置继承)试答:
- agent 答对 →
verdict=filtered_too_easy(EOB/排除法残余,剔除) - agent 答错 → 进入配对翻转门
adversarial_verdicts 表(schema 补齐以支撑严格续跑):
| 列 | 说明 |
|---|---|
question_id |
题标识(跨轮唯一,见 §4.3 ID 方案) |
round |
过滤轮次 |
stage |
cheat(作弊门)| flip_original | flip_mirror(区分同一 pair 的三次 agent 试答) |
question_hash |
原题 payload(question+options+answer)hash,防 JSON 变动后误用旧 verdict |
agent_prediction |
agent 答案 |
agent_correct |
是否答对(作弊门用) |
verdict |
passed | filtered_too_easy | filtered_no_flip | flip_skipped |
pair_id |
关联原题与镜像题 |
agent_config |
agent 配置指纹(skill_mode/max_steps/model),固化以保证续跑一致 |
续跑:重启时按 (question_id, question_hash, stage) 查已完成的试答,未完成的重跑;agent_config 变化则该题全部 verdict 作废重判。每题试答结果立即落表(崩溃不丢)。
4.2 配对翻转门(现场生成镜像,仅 supports_flip 的 SubPattern)
声明粒度是 SubPattern 级(不是 strategy 级):SubPattern 数据类加 supports_flip: bool + flip_axis: str | None("before/after" | "first/last")。因为 AR 内部 6 个 SubPattern 只有部分支持翻转:temporal_reasoning_failure=before/after、cross_segment_entity_tracking=first/last;其余 4 个=None。Phase B 按题的 sub_pattern(从 JSON 读,见 §依赖)查该 SubPattern 的 flip_axis。
判定语义(修正前版逻辑洞):配对翻转不要求答对,只看答案是否随问题翻转。前版"两题都答对才 passed"与作弊门"agent 答错才进入"直接矛盾(同一 agent 对原题先错后对判定不稳定)。修正为:
| agent 对 (P, Q) 的答案 | 含义 | verdict |
|---|---|---|
| 两题给不同答案 | 答案随问题翻转 → agent 在尝试理解视频(即使都错) | passed(有效难题) |
| 两题给相同答案 | 问题明明相反却不翻转 → agent 没看视频瞎选、题诱导瞎选(有偏置) | filtered_no_flip(剔除) |
流程(完全在 Phase B 内,Phase A 不动):
- 从原题
source_nodes重建素材(Phase B 已持有树); - 现场调 VLM 生成镜像题(同素材、翻转 flip_axis:
"X 之前"→"X 之后"),正解天然相反; - 原题 P 的 agent 预测复用作弊门那次结果(不重跑,避免同一 agent 非确定性引入噪声);仅新跑镜像题 Q;同一
pair_id。 - 按上表判定(比较 P/Q 所选是否指向同一语义内容,不判对错)。
- 镜像生成失败(造不出合法 before/after 对)→
flip_skipped,该题只经作弊门判定,不误杀。
判定工程化细则(消除判定噪声):
- 按 canonical 语义比较,不比字母:镜像题选项会重洗牌,A/B/C/D 字母无语义;比较 agent 所选选项规范化文本(canonical option)是否指向同一内容。
- 无效答案保守处理:P/Q 任一为拒答/非法答案 → 不判 passed,记
flip_skipped(退回只经作弊门),避免"P 拒答、Q 随机给答"被误判为翻转。 - 镜像正解字面校验:镜像生成后校验
canonical_correct(P) != canonical_correct(Q)(before/after 不总保证正解不同,如列表答案、"无法确定"类);相同则造不出有效镜像对 →flip_skipped。
镜像题仅用于翻转检验,不进最终题库。
门顺序(逻辑自洽):① 作弊门用完整 agent 试答原题,答对=太简单→剔除;② 存活的"agent 答错"题进翻转门,看 P/Q 答案是否翻转。两门都用完整 agent,串联无矛盾:答对的在作弊门被剔除,答错的在翻转门按"答案是否翻转"区分"有效难题"vs"诱导瞎选的偏置题"。
4.3 补生成与迭代
缺额 = 目标数 − passed 数。缺额 >0 且轮次 < adversarial_max_rounds 时,调 run_pipeline_v2(Phase A)补生成。
需要的接口改动(明确,非"显式传入即可"一笔带过):run_pipeline_v2 现每 run 新建空 embed_pool/used_node_ids(pipeline_v2.py:793 附近),补生成需新增两个可选参数:
initial_used_node_ids: set[str] | None— 传入已用节点,补生成避开(sampler 的 used_node_ids 初值);initial_embed_pool: list[np.ndarray] | None— 传入已接受题的 embedding,补生成对它们去重。 不传时行为与现状完全一致(默认 None → 空初始化),11 题型不受影响。
question_id 跨轮防撞:现 question_id = f"{video_id}_{task_type}_{seq:04d}"(generator_v2.py:340)。补生成新 run 复用 seq 会撞 ID。方案:补生成 run 传入 seq_offset(= 上一轮最大 seq),新题从 offset 续编,保证 accepted_questions.json 与 adversarial_verdicts 中 ID 全局唯一。
两份 JSON 时序(消除歧义):
accepted_questions.json:Phase A 各 run(含补生成)的on_accept滚动追加(保持 Phase A 语义)。accepted_questions_final.json:Phase B 每轮结束全量重写(tmp+os.replace 原子写),内容 = 当前所有verdict=passed的题。非增量追加——避免断点在"verdict 已写、final 未重写"之间时状态不一致;final 可随时从 verdicts 表全量重建。
4.4 难度报告(run_store.py)
- 每轮记录 agent 正确率(=作弊门答对率,越低越难)到
adversarial_verdicts聚合。 - 批次 agent 正确率 >
difficulty_warn_threshold(默认 0.85)→logger.warning告警"出题太简单"。 - 难度用独立字段(agent_correct 聚合),不复用
difficulty_steps(那是 heavy_check 步数)。
5. Prior-Version Audit(Phase A 后的管线行为)
| Phase A 后行为 | Phase B 处置 |
|---|---|
| on_accept 逐题追加 JSON(便宜门过即 accepted) | 保留 — Phase B 不改,只在其产物之上过滤 |
| record_item/update_gates 状态机(final_status) | 保留 — Phase B 用独立 adversarial_verdicts 表 |
| load_progress 断点续跑 | 保留 — Phase B 有独立过滤进度(未试答题续跑) |
| run_pipeline_v2 逐 slot 出题 | 保留 — 补生成是标准 Phase A run |
| embed_pool/used_node_ids 每 run 独立初始化 | 保留 — 补生成新 run 不继承,显式传避开节点 |
| 四门 gate | 保留 — 作弊门/翻转门是 JSON 产物之后的独立层 |
未发现隐式删除。Phase B 是 additive 层,Phase A 状态机零改动。
6. 非功能性需求
| 维度 | 设计 |
|---|---|
| 持久化 | 每题 agent 试答结果按 (question_id, question_hash, stage) 立即写 adversarial_verdicts 表;final JSON 每轮全量重写(tmp+os.replace)。原题 GeneratedQuestion 从 Phase A 的 JSON 完整重建,无需 DB 重建 |
| 幂等性 | agent 试答非确定性(LLM 固有),但过滤决策幂等(同 verdict 不重复处理);question_hash 防 JSON 变动误用旧 verdict;补生成用递增 seed + seq_offset 可复现 |
| 断点续跑 | adversarial_verdicts 按 (question_id, question_hash, stage) 记录已完成试答,重启只重跑未完成的;agent_config 变则该题 verdict 作废重判。补生成复用 Phase A progress |
| 原子性 | 每题 verdict 单条 DB 写;final JSON 全量 tmp+os.replace 原子重写(非追加,可随时从 verdicts 表重建) |
7. 行为保真检查清单
| # | 行为 | 状态 |
|---|---|---|
| 1 | Phase A 出题 + accepted 语义 + on_accept | 保留(零改动) |
| 2 | 四门 gate / 断点续跑 / record_item 状态机 | 保留 |
| 3 | 11 个非 AR 题型 | 保留(不在 filter_task_types 中,不触发过滤) |
| 4 | AR 最终题库 | 有意变更 — 经 agent 对抗过滤 + 翻转检验 |
8. 配置参数(YAML)
| 参数 | 默认 | 说明 |
|---|---|---|
filter_task_types |
[Action Recognition] |
后置过滤作用的题型(filter 层配置,非 strategy) |
adversarial_max_rounds |
5 | 后置过滤迭代上限 |
adversarial_agent_max_steps |
40 | agent 试答步数上限 |
difficulty_warn_threshold |
0.85 | 批次 agent 正确率告警阈值 |
9. 测试策略
- 单元:翻转判定(构造 agent 对 P/Q 的答案 → 验证"答案不同=passed / 答案相同=filtered_no_flip",不判对错);作弊门 verdict(agent 答对=filtered_too_easy);迭代缺额计算 + 轮次上限 + seq_offset 防撞 ID。
- 集成:AR 后置过滤端到端(mock agent + mock 镜像 VLM),验证
accepted_questions.json→ 过滤 →accepted_questions_final.json;断点续跑(中断后从 adversarial_verdicts 未试答题继续,已判的不重跑)。 - 回归:11 个非 AR 题型不触发过滤(不在
filter_task_types中,现有测试全绿)。
10. 待实现时确认的细节
- 补生成"避开已用 source_nodes"的粒度:整题级去重(已用节点集合传入 sampler 的 used_node_ids)已足够,无需节点级黑名单持久化跨 run(每轮显式传入即可)。
- 镜像题 VLM 生成失败(无法造出合法 before/after 对)时:该题跳过翻转门、只经作弊门判定(记 flip_skipped,不算 filtered),避免因镜像生成失败误杀有效题。