refactor(question_gen): extract _validate_parsed_fields to reduce CC

Split field validation logic out of _parse_v2_response into a dedicated
_validate_parsed_fields helper. This brings _parse_v2_response from CC=11
(grade C) down to CC=3 (grade A). The extracted validator is CC=9 (grade B).
No grade-C functions remain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 23:43:35 -04:00
parent cf51d2de9d
commit 6d6eb8e3a3
+69 -40
View File
@@ -220,6 +220,65 @@ def _extract_json_from_text(raw: str) -> str:
return content
@dataclass(frozen=True)
class _ValidatedFields:
"""字段校验通过后的中间结构。"""
question: str
options: tuple[str, ...]
answer: str
difficulty: str
def _validate_parsed_fields(data: dict) -> _ValidatedFields:
"""校验 VLM 响应 JSON 的必填字段并规范化。
参数:
data: 已解析的 JSON 字典。
返回:
_ValidatedFields 实例(字段已规范化)。
异常:
ValueError: 缺少必填字段或字段值非法。
"""
missing = [f for f in ("question", "options", "answer", "difficulty") if f not in data]
if missing:
msg = f"VLM 响应缺少必填字段: {', '.join(missing)}"
raise ValueError(msg)
question_text = str(data["question"])
options_raw = data["options"]
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}"
raise ValueError(msg)
# 校验 answer
if answer not in _VALID_ANSWERS:
msg = f"answer 字段值 '{answer}' 非法,必须为 A/B/C/D 之一"
raise ValueError(msg)
# 校验 difficulty(宽容处理:非法值回退为 medium)
if difficulty not in _VALID_DIFFICULTIES:
logger.warning(
"difficulty '{}' 不在预设范围 {},回退为 'medium'",
difficulty,
_VALID_DIFFICULTIES,
)
difficulty = "medium"
return _ValidatedFields(
question=question_text,
options=tuple(str(o) for o in options_raw),
answer=answer,
difficulty=difficulty,
)
def _parse_v2_response(
raw: str,
video_id: str,
@@ -233,7 +292,7 @@ def _parse_v2_response(
流程:
1. 提取 JSON(处理 markdown 包裹)。
2. json_repair 修复常见格式错误。
3. 解析并校验必填字段。
3. 校验必填字段(委托 _validate_parsed_fields
4. 构造 CandidateQuestion 实例。
参数:
@@ -250,13 +309,11 @@ def _parse_v2_response(
异常:
ValueError: JSON 解析失败或缺少必填字段或字段值非法。
"""
# Phase 1: 提取 JSON 文本
# Phase 1: 提取 + 修复 JSON
json_text = _extract_json_from_text(raw)
# Phase 2: json_repair 修复
repaired = repair_json(json_text, return_objects=False)
# Phase 3: 解析
# Phase 2: 解析为字典
try:
data = json.loads(repaired)
except json.JSONDecodeError as e:
@@ -267,38 +324,10 @@ def _parse_v2_response(
msg = f"VLM 响应顶层不是 JSON 对象: type={type(data).__name__}"
raise ValueError(msg)
# Phase 4: 校验必填字段
missing = [f for f in ("question", "options", "answer", "difficulty") if f not in data]
if missing:
msg = f"VLM 响应缺少必填字段: {', '.join(missing)}"
raise ValueError(msg)
# Phase 3: 字段校验
fields = _validate_parsed_fields(data)
question_text = str(data["question"])
options_raw = data["options"]
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}"
raise ValueError(msg)
options = tuple(str(o) for o in options_raw)
# 校验 answer
if answer not in _VALID_ANSWERS:
msg = f"answer 字段值 '{answer}' 非法,必须为 A/B/C/D 之一"
raise ValueError(msg)
# 校验 difficulty
if difficulty not in _VALID_DIFFICULTIES:
logger.warning(
"difficulty '{}' 不在预设范围 {},回退为 'medium'",
difficulty,
_VALID_DIFFICULTIES,
)
difficulty = "medium"
# Phase 5: 构造 CandidateQuestion
# Phase 4: 构造 CandidateQuestion
question_id = f"{video_id}_{task_type}_{seq:04d}"
return CandidateQuestion(
@@ -306,11 +335,11 @@ def _parse_v2_response(
video_id=video_id,
task_type=task_type,
skill_target=skill_target,
question=question_text,
options=options,
answer=answer,
question=fields.question,
options=fields.options,
answer=fields.answer,
source_nodes=source_nodes,
difficulty=difficulty,
difficulty=fields.difficulty,
)