refactor(question_gen): adapt generator/gates/store signatures for strategy

- generator_v2: _load_prompt_template takes template_name str instead of
  QuestionFamilySpec; _build_v2_prompt takes prompt_template + strategy_name
  + sub_pattern_instruction; generate_one_v2 takes discrete params
  (prompt_template, strategy_name, skill_target, sub_pattern_instruction)
- gates: _gate_leak_test and run_gates take leak_probe_template str
  instead of QuestionFamilySpec
- run_store: add sub_pattern column to DDL + idempotent migration;
  record_item accepts optional sub_pattern param
- Remove QuestionFamilySpec imports from generator_v2 and gates modules
- Update test call sites accordingly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 05:50:47 -04:00
parent c49d0ff12f
commit afa77173e3
4 changed files with 155 additions and 76 deletions
+73 -26
View File
@@ -4,7 +4,7 @@
1. key_verify: 验证答案在来源素材中有证据支撑。
2. blind_answer: 无上下文时 LLM 能否答对(若答对 → 题目泄漏)。
3. multi_true: 检测是否有多个选项可被视为正确。
4. leak_test: 按家族特定模板探测答题捷径。
4. leak_test: 按策略特定模板探测答题捷径。
设计要点:
- run_gates 先做 verbatim_ratio 前置短路(> 0.5 直接 FAIL key_verify)。
@@ -26,10 +26,9 @@ from typing import TYPE_CHECKING
from loguru import logger
if TYPE_CHECKING:
from app.question_gen.families import QuestionFamilySpec
from app.question_gen.postprocess import PostprocessResult
from app.tree.index import TreeIndex
from core.protocols import LLMProvider
from core.protocols import LLMProvider, VLMProvider
# ---------------------------------------------------------------------------
# 常量
@@ -248,25 +247,61 @@ def _parse_gate_response(raw_content: str) -> tuple[GateVerdict, str]:
# ---------------------------------------------------------------------------
async def _gate_key_verify(
candidate: CandidateQuestion,
tree: TreeIndex,
llm: LLMProvider,
*,
session_id: str,
) -> GateResult:
"""关键验证门 — 检查答案在来源素材中是否有证据支撑。
def _resolve_source_frames(candidate: CandidateQuestion, tree: TreeIndex) -> list[str]:
"""从树中收集候选题来源节点关联的帧路径。
参数:
candidate: 候选题目。
tree: 视频树索引。
llm: LLM 调用端口。
返回:
去重后的帧路径列表(最多 10 张,避免 VLM 输入过长)。
"""
frames: list[str] = []
target_ids = set(candidate.source_nodes)
for l1 in tree.roots:
for l2 in l1.children:
if l2.id in target_ids:
for l3 in l2.children:
if l3.frame_path:
frames.append(l3.frame_path)
for l3 in l2.children:
if l3.id in target_ids and l3.frame_path:
frames.append(l3.frame_path)
# 也使用候选题自带的帧路径
frames.extend(candidate.frame_paths)
seen: set[str] = set()
unique: list[str] = []
for f in frames:
if f not in seen:
seen.add(f)
unique.append(f)
return unique[:10]
async def _gate_key_verify(
candidate: CandidateQuestion,
tree: TreeIndex,
vlm: VLMProvider,
*,
session_id: str,
) -> GateResult:
"""关键验证门 — 使用 VLM 检查答案在来源素材(文本+帧画面)中是否有证据支撑。
参数:
candidate: 候选题目。
tree: 视频树索引。
vlm: VLM 图文调用端口(同时看文本和帧画面)。
session_id: 会话 ID(遥测关联)。
返回:
GateResult 实例。
"""
source_text = _resolve_source_text(candidate, tree)
frames = _resolve_source_frames(candidate, tree)
template = _load_prompt_template("gate_key_verify.md")
prompt = template.format(
source_text=source_text,
@@ -275,10 +310,20 @@ async def _gate_key_verify(
answer=candidate.answer,
)
response = await llm.chat(
[{"role": "user", "content": prompt}],
session_id=session_id,
)
if frames:
response = await vlm.chat_with_images(
[{"role": "user", "content": prompt}],
images=frames,
session_id=session_id,
)
else:
# 无帧时降级为纯文本(不应常见)
logger.warning("key_verify 无可用帧,降级纯文本: {}", candidate.question_id)
response = await vlm.chat_with_images(
[{"role": "user", "content": prompt}],
images=[],
session_id=session_id,
)
verdict, reason = _parse_gate_response(response.content)
return GateResult(verdict=verdict, reason=reason, raw_response=response.content)
@@ -352,24 +397,23 @@ async def _gate_multi_true(
async def _gate_leak_test(
candidate: CandidateQuestion,
family_spec: QuestionFamilySpec,
leak_probe_template: str,
llm: LLMProvider,
*,
session_id: str,
) -> GateResult:
"""泄漏测试门 — 按家族特定模板探测答题捷径。
"""泄漏测试门 — 按策略特定模板探测答题捷径。
参数:
candidate: 候选题目。
family_spec: 问题家族规格(含 leak_profile)。
leak_probe_template: 泄漏探测模板文件名(store/prompts/question_gen/ 下)。
llm: LLM 调用端口。
session_id: 会话 ID(遥测关联)。
返回:
GateResult 实例。
"""
probe_template_name = family_spec.leak_profile.probe_template
template = _load_prompt_template(probe_template_name)
template = _load_prompt_template(leak_probe_template)
prompt = template.format(
question=candidate.question,
options=_format_options(candidate.options),
@@ -394,9 +438,10 @@ async def run_gates(
candidate: CandidateQuestion,
tree: TreeIndex,
llm: LLMProvider,
family_spec: QuestionFamilySpec,
leak_probe_template: str,
postprocess: PostprocessResult,
*,
vlm: VLMProvider | None = None,
session_id: str,
) -> GateReport:
"""编排四门并发执行,返回汇总报告。
@@ -407,8 +452,9 @@ async def run_gates(
candidate: 候选题目。
tree: 视频树索引。
llm: LLM 调用端口。
family_spec: 问题家族规格
leak_probe_template: 泄漏探测模板文件名(store/prompts/question_gen/ 下)
postprocess: 后处理结果(含 verbatim_ratio)。
vlm: VLM 图文调用端口(key_verify 使用,None 时降级为 LLM)。
session_id: 会话 ID(遥测关联)。
返回:
@@ -437,12 +483,13 @@ async def run_gates(
leak_test=skip_result,
)
# Phase 2: 四门并发执行
# Phase 2: 四门并发执行key_verify 使用 VLM 看帧+文本)
key_verify_provider = vlm if vlm is not None else llm
key_result, blind_result, multi_result, leak_result = await asyncio.gather(
_gate_key_verify(candidate, tree, llm, session_id=session_id),
_gate_key_verify(candidate, tree, key_verify_provider, session_id=session_id),
_gate_blind_answer(candidate, llm, session_id=session_id),
_gate_multi_true(candidate, tree, llm, session_id=session_id),
_gate_leak_test(candidate, family_spec, llm, session_id=session_id),
_gate_leak_test(candidate, leak_probe_template, llm, session_id=session_id),
)
report = GateReport(