From 48dc40dfa7a66535424d51f85d477bc020958de6 Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Wed, 22 Apr 2026 16:22:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(pipeline):=20report=5Fconfig=20=E5=B8=83?= =?UTF-8?q?=E5=B0=94=20null=20=E6=97=B6=E6=81=A2=E5=A4=8D=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=20LLM=20=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON null 曾使 llm_matrix_group_summaries 等键存在但值为 None,合并默认配置时不被覆盖,bool(None) 关闭第五章矩阵与第六章促销归纳。校验阶段剥离 null,runner 侧统一 merge_report_config_with_defaults;简报构建同步。补充单测。 Made-with: Cursor --- backend/pipeline/jd/runner.py | 38 +++++++++++---- backend/pipeline/serializers.py | 48 +++++++++++-------- .../tests/test_report_config_bool_null.py | 33 +++++++++++++ 3 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 backend/pipeline/tests/test_report_config_bool_null.py diff --git a/backend/pipeline/jd/runner.py b/backend/pipeline/jd/runner.py index e1a0782..21f3b24 100644 --- a/backend/pipeline/jd/runner.py +++ b/backend/pipeline/jd/runner.py @@ -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, ) diff --git a/backend/pipeline/serializers.py b/backend/pipeline/serializers.py index 52f2117..0f18d6f 100644 --- a/backend/pipeline/serializers.py +++ b/backend/pipeline/serializers.py @@ -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", diff --git a/backend/pipeline/tests/test_report_config_bool_null.py b/backend/pipeline/tests/test_report_config_bool_null.py new file mode 100644 index 0000000..a31d552 --- /dev/null +++ b/backend/pipeline/tests/test_report_config_bool_null.py @@ -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