Files
Video-Tree-TRM5/research-wiki/designs/2026-07-14-adversarial-question-gen-phaseB-design.md

193 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: adversarial-question-gen-phaseB
title: 出题质量提升 Phase B — Agent 对抗过滤(独立后置过滤层)
type: design
created: 2026-07-14
status: 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 照常逐题 acceptedPhase 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. 架构与数据流
```mermaid
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` | 原题 payloadquestion+options+answerhash,防 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 不动)**
1. 从原题 `source_nodes` 重建素材(Phase B 已持有树);
2. 现场调 VLM 生成镜像题(同素材、翻转 flip_axis:`"X 之前"``"X 之后"`),正解天然相反;
3. 原题 P 的 agent 预测**复用作弊门那次结果**(不重跑,避免同一 agent 非确定性引入噪声);仅新跑镜像题 Q;同一 `pair_id`
4. 按上表判定(比较 P/Q 所选是否指向同一语义内容,不判对错)。
5. 镜像生成失败(造不出合法 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 AuditPhase 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",不判对错);作弊门 verdictagent 答对=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),避免因镜像生成失败误杀有效题。