"""Action Recognition 特化出题策略。 靶向 Agent 在动作识别类题目上的 6 种典型失败机制,通过加权随机 SubPattern 选择为 VLM 出题提供聚焦指令。 与 BaseTaskTypeStrategy 完全自包含,不依赖 QuestionFamilySpec。 采样从 L3 提升至 L2(跨段推理需要更广视野),3 个 SubPattern 进一步覆盖至 L1 以测试全局追踪能力。 """ from __future__ import annotations from typing import TYPE_CHECKING, Any from app.question_gen.families import SamplingConstraint from app.question_gen.strategy import SubPattern if TYPE_CHECKING: import random # --------------------------------------------------------------------------- # 6 个 SubPattern 定义 # --------------------------------------------------------------------------- _PREMATURE_EVIDENCE_ANCHORING = SubPattern( name="premature_evidence_anchoring", weight=0.20, sampling_level_override=1, constraint_override=None, instruction=( "设计一道需要考察视频中完整证据链的动作识别题。" "正确答案的关键证据出现在视频后半段或跨多个片段," "但视频前段包含一个看似合理的局部匹配——" "Agent 若仅凭首条匹配停止搜索就会出错。" ), positive_examples=[ { "question": "视频中厨师最终采用了哪种烹饪方式?", "answer": "B. 蒸", "why": "厨师先演示了炒(前段),但最终菜品使用蒸制(后段)," "锚定首段证据的 Agent 会错选'炒'。", }, ], negative_examples=[ { "question": "视频开头厨师在做什么?", "why": "答案仅需前段信息,不会触发过早锚定失败。", }, ], distractor_rules=( "将视频前段真实出现的局部匹配动作设为强干扰项——它真实发生," "仅在【时点】这一单一维度上与正解不同(前段 vs 最终结论段)。" "其余干扰项亦须是视频中真实发生的动作,严禁缺席事件。" ), ) _TEMPORAL_REASONING_FAILURE = SubPattern( name="temporal_reasoning_failure", weight=0.20, sampling_level_override=1, constraint_override=None, instruction=( "设计一道要求正确排序或定位第 N 次出现的动作识别题。" "题目需要 Agent 追踪事件的时间先后顺序," "或准确识别某动作在视频中第几次出现。" "打乱时序或错误计数即会答错。" ), positive_examples=[ { "question": "运动员第三次尝试起跳前做了什么准备动作?", "answer": "C. 深蹲热身", "why": "需要准确定位'第三次'起跳而非其他次," "时序推理失败的 Agent 会混淆不同次尝试的准备动作。", }, ], negative_examples=[ { "question": "运动员在视频中做了什么?", "why": "不涉及时序排序或计数,Agent 无需追踪顺序。", }, ], distractor_rules=( "干扰项必须是视频中真实发生的事件,仅在【事件时序/顺序】这一单一维度上与正解不同——" "即同一组真实事件的错误排列或错误的第 N 次定位。" "严禁使用视频中未出现的缺席事件作为干扰项。" ), ) _SEMANTIC_RIGIDITY = SubPattern( name="semantic_rigidity", weight=0.15, sampling_level_override=None, constraint_override=None, instruction=( "设计一道动作识别题,正确选项使用与视频原始描述不同的同义表达。" "Agent 需理解语义等价而非依赖字面匹配——" "例如视频字幕说'奔跑',正确选项写作'快速移动'。" ), positive_examples=[ { "question": "工人对墙面进行了什么操作?", "answer": "A. 涂覆保护层", "why": "视频中字幕描述为'刷漆',正确答案改写为'涂覆保护层'," "依赖字面匹配的 Agent 会因找不到完全一致的表述而错选。", }, ], negative_examples=[ { "question": "工人在刷漆吗?", "why": "选项直接复用视频原文,不考察语义理解。", }, ], distractor_rules=( "干扰项须基于视频真实内容,仅在【表述/语义】这一单一维度上做文章:" "保留一个复用视频原始字幕字面、但在题干限定下语义为假的选项作为陷阱," "其余选项描述真实动作的不同同义表述。严禁凭空编造缺席动作。" ), ) _FINE_GRAINED_VISUAL_ACTION = SubPattern( name="fine_grained_visual_action", weight=0.15, sampling_level_override=None, constraint_override=None, instruction=( "设计一道需要区分细粒度动作方式的识别题。" "题目聚焦于 HOW(怎么做)而非 WHAT(做什么)," "如区分'搅拌'与'翻炒'、'拧'与'拉'等视觉上相似但方式不同的动作。" ), positive_examples=[ { "question": "维修人员是如何拆卸螺丝的?", "answer": "D. 用扳手逆时针旋转", "why": "需要区分拆卸的具体方式(扳手 vs 螺丝刀、顺时针 vs 逆时针)," "粗粒度识别只能判断'在拆螺丝',无法区分方式。", }, ], negative_examples=[ { "question": "维修人员在做什么?", "why": "只需粗粒度动作识别('拆螺丝'),不考察具体方式。", }, ], distractor_rules=( "四个选项须是【同一大类动作】的不同执行方式,全部为视频中真实可见的做法," "仅在【执行方式】这一单一维度上不同(如顺/逆时针、扳手/螺丝刀)。" "严禁使用明显不相关或视频中未出现的动作作为干扰项。" ), ) _CROSS_SEGMENT_ENTITY_TRACKING = SubPattern( name="cross_segment_entity_tracking", weight=0.15, sampling_level_override=1, constraint_override=None, instruction=( "设计一道需要跨视频段落追踪同一实体动作的识别题。" "目标实体在不同片段中外观、称呼或上下文发生变化," "Agent 需要将多段信息合并才能正确回答关于该实体的动作问题。" ), positive_examples=[ { "question": "穿红色外套的人在视频中总共完成了哪些动作?", "answer": "B. 先讲解、后示范、最后总结", "why": "该人物在前段穿外套讲解,中段脱外套示范,后段重新穿上总结," "无法跨段追踪的 Agent 会遗漏某段动作。", }, { "question": "主持人在节目不同环节中分别做了什么?", "answer": "A. 开场介绍、采访嘉宾、总结点评", "why": "主持人在不同场景切换中持续出现,需要跨段聚合。", }, ], negative_examples=[ { "question": "视频第一个片段中的人在做什么?", "why": "仅需单段信息,不考察跨段追踪。", }, ], distractor_rules=( "干扰项须是视频中【另一真实实体】在相应片段真实做过的动作," "仅在【动作主体】这一单一维度上与正解不同。" "严禁编造任何实体未做过的缺席动作。" ), ) _EVIDENCE_GAP_CONFABULATION = SubPattern( name="evidence_gap_confabulation", weight=0.15, sampling_level_override=None, constraint_override=None, instruction=( "设计一道动作识别题,视频中存在证据空缺(如遮挡、跳切、画外音)。" "正确答案承认信息不足或基于间接证据推断," "而非凭空编造因果链。Agent 若虚构缺失证据即会出错。" ), positive_examples=[ { "question": "画面切走后,演讲者下一步做了什么?", "answer": "C. 无法从视频中直接确定", "why": "画面跳切导致该动作无直接视觉证据," "倾向于虚构的 Agent 会编造一个看似合理的动作。", }, ], negative_examples=[ { "question": "画面中演讲者正在做什么?", "why": "动作在画面中可见,不存在证据空缺。", }, ], distractor_rules=( "正解仅陈述视频中可观测的事实或诚实承认证据不足;" "干扰项在【因果完整性】这一单一维度上越界——补上一段视频未展示的因果链," "但其前提元素仍取自视频真实内容(诱导 Agent 顺势编造),而非完全凭空的缺席事件。" ), ) AR_SUB_PATTERNS: tuple[SubPattern, ...] = ( _PREMATURE_EVIDENCE_ANCHORING, _TEMPORAL_REASONING_FAILURE, _SEMANTIC_RIGIDITY, _FINE_GRAINED_VISUAL_ACTION, _CROSS_SEGMENT_ENTITY_TRACKING, _EVIDENCE_GAP_CONFABULATION, ) # 预计算:名称列表 + 权重列表(避免每次 select 重新构建) _AR_PATTERN_NAMES: list[str] = [sp.name for sp in AR_SUB_PATTERNS] _AR_PATTERN_WEIGHTS: list[float] = [sp.weight for sp in AR_SUB_PATTERNS] _AR_PATTERN_BY_NAME: dict[str, SubPattern] = {sp.name: sp for sp in AR_SUB_PATTERNS} # --------------------------------------------------------------------------- # ActionRecognitionStrategy # --------------------------------------------------------------------------- _SAMPLING_CONSTRAINT = SamplingConstraint( min_subtitles=3, min_l3_nodes=5, require_frames=True, cross_l2_span=True, ) class ActionRecognitionStrategy: """Action Recognition 特化出题策略。 自包含实现,不依赖 BaseTaskTypeStrategy 或 QuestionFamilySpec。 靶向 6 种典型失败机制,通过加权随机 SubPattern 引导 VLM 出题。 属性: task_type: 题型名 — "Action Recognition"。 strategy_name: 策略标识 — "ACTION_RECOGNITION"。 skill_target: 目标失败机制 — "M1_AR"。 sampling_level: 采样层级 — 2(L2,从 L3 升级以获得跨段视野)。 sampling_constraint: 采样约束(min_subtitles=3, min_l3_nodes=5, require_frames, cross_l2_span)。 prompt_template: 出题模板 — "action_recognition.md"。 leak_probe_template: 泄漏探测模板 — "gate_leak_retrieval.md"。 """ @property def task_type(self) -> str: """返回题型名。""" return "Action Recognition" @property def strategy_name(self) -> str: """返回策略标识名。""" return "ACTION_RECOGNITION" @property def skill_target(self) -> str: """返回目标失败机制编号。""" return "M1_AR" @property def sampling_level(self) -> int: """返回采样层级(L2)。""" return 2 @property def sampling_constraint(self) -> SamplingConstraint: """返回采样约束。""" return _SAMPLING_CONSTRAINT @property def prompt_template(self) -> str: """返回出题 prompt 模板文件名。""" return "action_recognition.md" @property def leak_probe_template(self) -> str: """返回泄漏探测模板文件名。""" return "gate_leak_retrieval.md" @property def uses_grounded_selector(self) -> bool: """AR 启用候选池 + VLM 视觉打分 selector。""" return True def select_sub_pattern(self, rng: random.Random) -> SubPattern: """按权重随机选择一个 SubPattern。 参数: rng: 随机数生成器(确保可复现)。 返回: 选中的 SubPattern 实例(永不为 None)。 """ chosen_name = rng.choices( _AR_PATTERN_NAMES, weights=_AR_PATTERN_WEIGHTS, k=1, )[0] return _AR_PATTERN_BY_NAME[chosen_name] def build_prompt_context(self, material: Any, sub_pattern: SubPattern | None) -> dict: """构建 prompt 上下文字典。 参数: material: 采样素材(当前未使用,留给管线扩展)。 sub_pattern: 选中的子模式。 返回: 包含 family_name, prompt_template, sub_pattern 的字典。 """ return { "family_name": self.strategy_name, "prompt_template": self.prompt_template, "sub_pattern": sub_pattern.name if sub_pattern is not None else None, } def extra_gates(self, candidate: Any) -> list: """返回额外门控列表(当前为空)。 参数: candidate: 候选题目。 返回: 空列表。 """ return []