fix(pipeline): report_config 布尔 null 时恢复默认 LLM 开关

JSON null 曾使 llm_matrix_group_summaries 等键存在但值为 None,合并默认配置时不被覆盖,bool(None) 关闭第五章矩阵与第六章促销归纳。校验阶段剥离 null,runner 侧统一 merge_report_config_with_defaults;简报构建同步。补充单测。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-22 16:22:27 +08:00
parent 31e8482442
commit 48dc40dfa7
3 changed files with 89 additions and 30 deletions

View File

@ -14,6 +14,7 @@ from django.conf import settings
from ..csv.schema import MERGED_FIELD_TO_CSV_HEADER
from ..models import PipelineJob
from ..serializers import REPORT_CONFIG_BOOL_KEYS
def merge_llm_supplement_with_rules_report(llm_md: str, rules_md: str) -> str:
@ -160,7 +161,10 @@ def use_chunked_group_summaries_llm(report_config: dict[str, Any] | None) -> boo
"yes",
):
return False
return bool(rc.get("llm_group_summaries_chunk_by_matrix", True))
ch = rc.get("llm_group_summaries_chunk_by_matrix")
if ch is None:
return True
return bool(ch)
def get_default_report_config() -> dict[str, Any]:
@ -186,6 +190,25 @@ def get_default_report_config() -> dict[str, Any]:
}
def merge_report_config_with_defaults(
report_config: dict[str, Any] | None,
) -> dict[str, Any]:
"""
合并 ``get_default_report_config``任务里常见仅含部分键JSON ``null`` 的布尔开关视为未设置
否则 ``bool(None)`` 会把第五章矩阵/第六章促销等 LLM 归纳整段关掉
"""
eff_rc: dict[str, Any] = (
dict(report_config) if isinstance(report_config, dict) else {}
)
for _bk in REPORT_CONFIG_BOOL_KEYS:
if eff_rc.get(_bk) is None:
eff_rc.pop(_bk, None)
for _k, _v in get_default_report_config().items():
if _k not in eff_rc:
eff_rc[_k] = _v
return eff_rc
def get_default_strategy_config() -> dict[str, Any]:
"""策略生成页独立默认(与 ``report_config`` 无关),供前端回填与 PATCH 合并。"""
return {
@ -225,13 +248,7 @@ def write_competitor_analysis_for_run_dir(
except json.JSONDecodeError:
meta = None
eff_rc: dict[str, Any] = (
dict(report_config) if isinstance(report_config, dict) else {}
)
# 任务仅保存部分调参时,补齐默认项(含各 llm_* 开关);显式写入的键不被覆盖
for _k, _v in get_default_report_config().items():
if _k not in eff_rc:
eff_rc[_k] = _v
eff_rc = merge_report_config_with_defaults(report_config)
all_tx = _flat_comment_texts(comment_rows)
suggest_path = run_dir / "keyword_suggest_llm.json"
suggest_record: dict[str, Any] = {
@ -823,6 +840,9 @@ def build_competitor_brief_for_job(
except json.JSONDecodeError:
pass
eff_final = merge_report_config_with_defaults(
eff if isinstance(eff, dict) else None
)
return jcr.build_competitor_brief(
run_dir=base,
keyword=kw,
@ -830,7 +850,7 @@ def build_competitor_brief_for_job(
search_export_rows=search_export_rows,
comment_rows=comment_rows,
meta=meta,
report_config=eff,
report_config=eff_final,
)

View File

@ -37,25 +37,10 @@ _REPORT_CONFIG_ALLOWED_KEYS = frozenset(
}
)
def validate_report_config_body(value: dict) -> dict:
if not isinstance(value, dict):
raise serializers.ValidationError("须为 JSON 对象")
value = dict(value)
value.pop("llm_section_bridges", None)
# 已废弃字段:静默丢弃,兼容旧任务 JSON
value.pop("comment_focus_words", None)
value.pop("comment_scenario_groups", None)
value.pop("llm_scenario_group_summaries", None)
extra = set(value.keys()) - _REPORT_CONFIG_ALLOWED_KEYS
if extra:
raise serializers.ValidationError(
f"未知字段:{', '.join(sorted(extra))}"
)
if "llm_comment_sentiment" in value and value["llm_comment_sentiment"] is not None:
if not isinstance(value["llm_comment_sentiment"], bool):
raise serializers.ValidationError("llm_comment_sentiment 须为 true 或 false")
for k in (
# 与 ``validate_report_config_body`` 中布尔校验一致;``null`` 视为未设置(须让默认 true/false 生效)
REPORT_CONFIG_BOOL_KEYS = frozenset(
{
"llm_comment_sentiment",
"llm_matrix_group_summaries",
"llm_price_group_summaries",
"llm_promo_group_summaries",
@ -66,8 +51,29 @@ def validate_report_config_body(value: dict) -> dict:
"chapter8_text_mining_probe_live_llm",
"chapter8_text_mining_probe_llm_chunked",
"chapter8_text_mining_probe_wordcloud",
):
if k in value and value[k] is not None and not isinstance(value[k], bool):
}
)
def validate_report_config_body(value: dict) -> dict:
if not isinstance(value, dict):
raise serializers.ValidationError("须为 JSON 对象")
value = dict(value)
for _bk in REPORT_CONFIG_BOOL_KEYS:
if value.get(_bk) is None:
value.pop(_bk, None)
value.pop("llm_section_bridges", None)
# 已废弃字段:静默丢弃,兼容旧任务 JSON
value.pop("comment_focus_words", None)
value.pop("comment_scenario_groups", None)
value.pop("llm_scenario_group_summaries", None)
extra = set(value.keys()) - _REPORT_CONFIG_ALLOWED_KEYS
if extra:
raise serializers.ValidationError(
f"未知字段:{', '.join(sorted(extra))}"
)
for k in REPORT_CONFIG_BOOL_KEYS:
if k in value and not isinstance(value[k], bool):
raise serializers.ValidationError(f"{k} 须为 true 或 false")
for k in (
"chapter8_probe_min_texts",

View File

@ -0,0 +1,33 @@
"""report_config 中布尔开关为 JSON null 时不应把 LLM 归纳整段关掉。"""
from __future__ import annotations
from unittest import mock
from pipeline.jd.runner import merge_report_config_with_defaults
from pipeline.serializers import validate_report_config_body
def test_validate_report_config_strips_null_bool_flags() -> None:
out = validate_report_config_body(
{
"llm_matrix_group_summaries": None,
"llm_promo_group_summaries": None,
}
)
assert "llm_matrix_group_summaries" not in out
assert "llm_promo_group_summaries" not in out
@mock.patch("pipeline.jd.runner.get_default_report_config")
def test_merge_report_config_null_bool_gets_default(mock_def: mock.MagicMock) -> None:
mock_def.return_value = {
"llm_matrix_group_summaries": True,
"llm_promo_group_summaries": True,
"llm_price_group_summaries": False,
}
merged = merge_report_config_with_defaults(
{"llm_matrix_group_summaries": None, "llm_promo_group_summaries": None}
)
assert merged["llm_matrix_group_summaries"] is True
assert merged["llm_promo_group_summaries"] is True
assert merged["llm_price_group_summaries"] is False