fix(question_gen): resolve pipeline integration issues from final review

1. Apply postprocess shuffle result (pp.options, pp.answer) to final
   GeneratedQuestion output instead of using original candidate values.

2. Record dedup rejection in store via new mark_item_rejected() method,
   preventing items from staying as 'accepted' after dedup rejects them.

3. Add .flatten() to embed_fn outputs in _is_duplicate and embed_pool
   append to handle 2D (1,D) arrays from embedding implementations.

4. Validate exactly 4 options in _validate_parsed_fields (was >= 2),
   matching the A-D answer constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 00:14:12 -04:00
parent eecb86e27a
commit 4fb7a61f8b
4 changed files with 86 additions and 14 deletions
+3 -3
View File
@@ -252,9 +252,9 @@ def _validate_parsed_fields(data: dict) -> _ValidatedFields:
answer = str(data["answer"]).strip().upper()
difficulty = str(data["difficulty"]).strip().lower()
# 校验 options
if not isinstance(options_raw, list) or len(options_raw) < 2:
msg = f"options 字段必须是至少 2 个选项的列表,实际: {options_raw}"
# 校验 options(answer 约束为 A-D,因此必须恰好 4 个选项)
if not isinstance(options_raw, list) or len(options_raw) != 4:
msg = f"options 字段必须恰好包含 4 个选项,实际数量: {len(options_raw) if isinstance(options_raw, list) else type(options_raw).__name__}"
raise ValueError(msg)
# 校验 answer
+16 -8
View File
@@ -245,7 +245,7 @@ def _is_duplicate(
if not embed_pool:
return False
query_vec = embed_fn(question_text)
query_vec = embed_fn(question_text).flatten()
query_norm = np.linalg.norm(query_vec)
if query_norm == 0:
return False
@@ -266,11 +266,18 @@ def _is_duplicate(
# ---------------------------------------------------------------------------
def _to_generated_question(candidate: CandidateQuestion) -> GeneratedQuestion:
def _to_generated_question(
candidate: CandidateQuestion,
*,
options: tuple[str, ...] | None = None,
answer: str | None = None,
) -> GeneratedQuestion:
"""将 CandidateQuestion 转换为 GeneratedQuestion。
参数:
candidate: 门控通过的候选题目。
options: 洗牌后的选项元组(若为 None 则使用 candidate 原始选项)。
answer: 重映射后的答案字母(若为 None 则使用 candidate 原始答案)。
返回:
GeneratedQuestion 实例(difficulty_steps 初始为 None)。
@@ -280,8 +287,8 @@ def _to_generated_question(candidate: CandidateQuestion) -> GeneratedQuestion:
video_id=candidate.video_id,
task_type=candidate.task_type,
question=candidate.question,
options=candidate.options,
answer=candidate.answer,
options=options if options is not None else candidate.options,
answer=answer if answer is not None else candidate.answer,
source_nodes=candidate.source_nodes,
difficulty=candidate.difficulty,
skill_target=candidate.skill_target,
@@ -464,6 +471,7 @@ async def _process_one_slot(
# Phase 7: 去重检测
if _is_duplicate(candidate.question, embed_pool, embed_fn, config.dedup_threshold):
prev_reason = "duplicate detected by embedding similarity"
store.mark_item_rejected(item_id, prev_reason)
logger.info(
"slot {} 重复题被拒绝 (attempt {}/{})",
slot.slot_id,
@@ -472,10 +480,10 @@ async def _process_one_slot(
)
continue
# Phase 8: 通过全部检查 → 接受
result = _to_generated_question(candidate)
# 将题目 embedding 加入池
embed_pool.append(embed_fn(candidate.question))
# Phase 8: 通过全部检查 → 接受(使用洗牌后的选项和答案)
result = _to_generated_question(candidate, options=pp.options, answer=pp.answer)
# 将题目 embedding 加入池flatten 确保 1D
embed_pool.append(embed_fn(candidate.question).flatten())
# 标记使用的节点
used_node_ids.update(candidate.source_nodes)
+19
View File
@@ -330,6 +330,25 @@ class QuestionGenStore:
if cursor.rowcount == 0:
raise ValueError(f"item_id 不存在: {item_id}")
def mark_item_rejected(self, item_id: str, reason: str) -> None:
"""将已记录的 item 标记为 rejected(用于门控外的拒绝场景,如去重)。
Parameters
----------
item_id : str
题目唯一 ID。
reason : str
拒绝原因描述。
"""
cursor = self._conn.execute(
"UPDATE question_gen_items SET final_status='rejected', gate_reject_reason=? "
"WHERE item_id=?",
(reason, item_id),
)
self._conn.commit()
if cursor.rowcount == 0:
raise ValueError(f"item_id 不存在: {item_id}")
def update_difficulty(self, item_id: str, difficulty_steps: int) -> None:
"""更新重量抽检产出的 Agent 步数。