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 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( def _parse_v2_response(
raw: str, raw: str,
video_id: str, video_id: str,
@@ -233,7 +292,7 @@ def _parse_v2_response(
流程: 流程:
1. 提取 JSON(处理 markdown 包裹)。 1. 提取 JSON(处理 markdown 包裹)。
2. json_repair 修复常见格式错误。 2. json_repair 修复常见格式错误。
3. 解析并校验必填字段。 3. 校验必填字段(委托 _validate_parsed_fields
4. 构造 CandidateQuestion 实例。 4. 构造 CandidateQuestion 实例。
参数: 参数:
@@ -250,13 +309,11 @@ def _parse_v2_response(
异常: 异常:
ValueError: JSON 解析失败或缺少必填字段或字段值非法。 ValueError: JSON 解析失败或缺少必填字段或字段值非法。
""" """
# Phase 1: 提取 JSON 文本 # Phase 1: 提取 + 修复 JSON
json_text = _extract_json_from_text(raw) json_text = _extract_json_from_text(raw)
# Phase 2: json_repair 修复
repaired = repair_json(json_text, return_objects=False) repaired = repair_json(json_text, return_objects=False)
# Phase 3: 解析 # Phase 2: 解析为字典
try: try:
data = json.loads(repaired) data = json.loads(repaired)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
@@ -267,38 +324,10 @@ def _parse_v2_response(
msg = f"VLM 响应顶层不是 JSON 对象: type={type(data).__name__}" msg = f"VLM 响应顶层不是 JSON 对象: type={type(data).__name__}"
raise ValueError(msg) raise ValueError(msg)
# Phase 4: 校验必填字段 # Phase 3: 字段校验
missing = [f for f in ("question", "options", "answer", "difficulty") if f not in data] fields = _validate_parsed_fields(data)
if missing:
msg = f"VLM 响应缺少必填字段: {', '.join(missing)}"
raise ValueError(msg)
question_text = str(data["question"]) # Phase 4: 构造 CandidateQuestion
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
question_id = f"{video_id}_{task_type}_{seq:04d}" question_id = f"{video_id}_{task_type}_{seq:04d}"
return CandidateQuestion( return CandidateQuestion(
@@ -306,11 +335,11 @@ def _parse_v2_response(
video_id=video_id, video_id=video_id,
task_type=task_type, task_type=task_type,
skill_target=skill_target, skill_target=skill_target,
question=question_text, question=fields.question,
options=options, options=fields.options,
answer=answer, answer=fields.answer,
source_nodes=source_nodes, source_nodes=source_nodes,
difficulty=difficulty, difficulty=fields.difficulty,
) )