From 805e9b4de4ed1a17fc76a5aee4385e2da093da01 Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Tue, 21 Apr 2026 16:59:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(report):=20=E9=BB=98=E8=AE=A4=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E7=AC=AC=E4=B9=9D=E7=AB=A0=E7=AD=96=E7=95=A5=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=B9=B6=E6=94=B9=E4=B8=BA=E8=AF=BB=E8=80=85?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 默认 report_config.llm_strategy_opportunities 为 false;第九章无大模型正文时输出固定说明并指向按细类「策略制定」。strategy 节选仅在 strategy_opportunities_llm.json 含 markdown 时返回,避免固定说明进入策略稿对齐。同步调整单测与 strategy_hints 占位文案。 Made-with: Cursor --- .../pipeline/competitor_report/jd_report.py | 39 ++++++++++++------- backend/pipeline/jd/runner.py | 3 +- .../reporting/report_strategy_excerpt.py | 3 ++ backend/pipeline/reporting/strategy_draft.py | 37 ++++-------------- .../tests/test_report_strategy_excerpt.py | 13 ++++++- 5 files changed, 49 insertions(+), 46 deletions(-) diff --git a/backend/pipeline/competitor_report/jd_report.py b/backend/pipeline/competitor_report/jd_report.py index ef99c5c..0dccf0a 100644 --- a/backend/pipeline/competitor_report/jd_report.py +++ b/backend/pipeline/competitor_report/jd_report.py @@ -3,7 +3,7 @@ 关键词 → 调用 ``jd_keyword_pipeline`` 全链路采集 → 生成 **标准化竞品分析报告**(Markdown)。 报告结构对齐常见竞品分析框架:研究范围与方法、执行摘要、**整体市场观察(列表可见度参考)**、 -市场与竞争结构、**按细分类目分组的竞品对比矩阵**、价格分析(含规则化价差/活动信号与可选 **细类价盘·促销** 大模型归纳)、**按细分类目的消费者反馈与用户画像**、**策略与机会提示**(以大模型归纳为主,可选)与附录;并明确数据边界。 +市场与竞争结构、**按细分类目分组的竞品对比矩阵**、价格分析(含规则化价差/活动信号与可选 **细类价盘·促销** 大模型归纳)、**按细分类目的消费者反馈与用户画像**、**策略与机会提示**(固定读者说明,指向产品中按细类「策略制定」;不再默认生成本章大模型长文)与附录;并明确数据边界。 若运行配置中提供了外部市场规模摘录(``EXTERNAL_MARKET_TABLE_ROWS``),则追加对应表格小节;否则不输出占位行。 依赖:全量抓取时与 ``crawler_copy/jd_pc_search/jd_keyword_pipeline.py`` 相同(Node、h5st、Playwright、``common/jd_cookie.txt``)。 @@ -120,6 +120,22 @@ OVERRIDE_MAX_SKUS: int | None = None OVERRIDE_PAGE_START: int | None = None OVERRIDE_PAGE_TO: int | None = None +# 报告正文面向读者:不写配置键名、文件名、环境变量名或原始异常栈。 +_CH8_PROBE_ENABLE_HINT = ( + "请在任务配置中开启「第八章评论文本探针」并重新生成报告,以输出词频、共现与主题等开放词表分析。" +) +def _strategy_opportunities_reader_fixed_lines() -> list[str]: + """ + 第九章不再默认输出全任务大模型长文时的固定读者说明(与产品「策略制定」按细类生成分工)。 + """ + return [ + "本任务**监测与归纳**见前文各章(执行摘要、整体观察、集中度、竞品矩阵、价格与促销、消费者反馈等)。" + "**可执行的市场策略稿**请在产品中通过「**策略制定**」**按矩阵细类**逐类生成:与同任务数据及第五章划分对齐,避免仅凭全关键词检索池做一句泛化结论。", + "", + "本报告**默认不再**附加全任务大模型「策略与机会」长文;若个别历史批次仍含该正文,以当时任务配置与落稿为准。", + "", + ] + def build_competitor_markdown( *, @@ -264,7 +280,7 @@ def build_competitor_markdown( ( "- **用户画像(第八章)**:**不再**使用星级子集内**预设口语短语**条形图、正负面扇形图及同口径摘要;**第八章第二节**为上述**文本补充分析**(探针)。" if _ch8_probe_sec - else "- **用户画像(第八章)**:若未启用探针,第二节仅说明方法并列出按细类评价条数;**请开启** `chapter8_text_mining_probe` 以生成开放词表统计与插图。" + else f"- **用户画像(第八章)**:若未启用探针,第二节仅说明方法并列出按细类评价条数;{_CH8_PROBE_ENABLE_HINT}" ), "- **细类划分(第五至第八章)**:**仅**依据合并表中的**商品详情页类目路径**;该信息缺失或无法读出细类名称的 SKU **不参与**竞品矩阵与按细类评价统计(相关评价条亦**不进入**按细类图表)。", "- **检索结果规模**:来自京东 PC 搜索返回的「结果条数」类指标,表示平台侧申报的匹配数量级,**不等于**动销、库存或独立 SKU 数。", @@ -364,7 +380,7 @@ def build_competitor_markdown( ) else: exec_bullets.append( - "评价已按**第五章同一细类**归组;**第八章第二节**定量主题分析依赖探针,可在报告调参中开启 `chapter8_text_mining_probe`。" + f"评价已按**第五章同一细类**归组;**第八章第二节**定量主题分析依赖探针,{_CH8_PROBE_ENABLE_HINT}" ) if api_rc is not None: exec_bullets.append( @@ -608,7 +624,7 @@ def build_competitor_markdown( ) if not (run_dir / "report_assets" / mx_chart).is_file(): lines.append( - f"*(尚未生成 ``report_assets/{mx_chart}``:请确认已执行报告出图流程,或重新生成报告。)*" + "*(本细类矩阵价量散点图尚未生成:请确认已执行报告出图流程,或重新生成报告。)*" ) lines.append("") lines.append("") @@ -723,7 +739,7 @@ def build_competitor_markdown( ( "- **评论文本补充分析(第八章第二节)**:本任务已用中文分词与统计工具做了开放词表分析(词频、关键词突出度、词对共现、主题归纳等,可选词云),**不再**在报告中使用「星级子集内预设口语短语」条形图、正负面扇形图及同口径摘要;**不再**使用「预设关注词 + 预设场景词组」子串计数与并列条图。" if _ch8_probe_sec - else "- **评论文本补充分析(第八章第二节)**:本任务**未**嵌入探针正文;**不再**输出预设关注词/场景子串统计图。请在 `report_config` 中开启 `chapter8_text_mining_probe` 并重跑以生成开放词表统计与插图。" + else f"- **评论文本补充分析(第八章第二节)**:本任务**未**嵌入探针正文;**不再**输出预设关注词/场景子串统计图。{_CH8_PROBE_ENABLE_HINT}" ), "", ] @@ -744,8 +760,8 @@ def build_competitor_markdown( [ "### 8.2 评论文本补充分析(未启用探针时的占位)", "", - "> **说明**:本版本**不再**生成「预设关注词 + 预设场景」子串统计与 ``chart_focus_and_scenarios_bar__*.png``。" - "开放词表、词频、共现与主题等请开启 ``chapter8_text_mining_probe`` 后重跑。", + "> **说明**:本版本**不再**生成「预设关注词 + 预设场景」子串统计及旧版并列条形图。" + f"开放词表、词频、共现与主题等分析需{_CH8_PROBE_ENABLE_HINT}", "", ] ) @@ -805,12 +821,7 @@ def build_competitor_markdown( ] ) else: - lines.extend( - [ - "未生成本节大模型正文:请在任务 `report_config` 中开启 `llm_strategy_opportunities` 并重跑产物,或检查 run 目录下 `strategy_opportunities_llm.json` 是否报错。", - "", - ] - ) + lines.extend(_strategy_opportunities_reader_fixed_lines()) lines.extend( [ @@ -1083,7 +1094,7 @@ def build_competitor_brief( "notes": [ "与在线分析报告各章**计数规则**一致;**不再**输出预设关注词/场景子串统计,评论侧主题以第八章文本挖掘探针(若启用)为准。", "价格来自页面展示字段抽取,含促销与规格差异;促销与标价对齐等为启发式摘录,仅供对照。", - "「集中度」中:默认按**列表行**计数(同一 SKU 多页曝光会重复计);`shops_from_list.unique_sku_basis` 为按**去重 SKU** 的对照口径。二者均**不是**销量、库存或全渠道市场份额。", + "「集中度」中:默认按**列表行**计数(同一 SKU 多页曝光会重复计);另有按**去重 SKU** 计数的对照口径。二者均**不是**销量、库存或全渠道市场份额。", ], } return _sanitize_json_numbers(out) diff --git a/backend/pipeline/jd/runner.py b/backend/pipeline/jd/runner.py index ed5e7cd..e1a0782 100644 --- a/backend/pipeline/jd/runner.py +++ b/backend/pipeline/jd/runner.py @@ -172,7 +172,8 @@ def get_default_report_config() -> dict[str, Any]: "llm_comment_group_summaries": True, "llm_price_group_summaries": True, "llm_promo_group_summaries": True, - "llm_strategy_opportunities": True, + # 全任务第九章大模型长文已弃用:可执行策略由「策略制定」按矩阵细类生成(见 jd_report 第九章固定说明)。 + "llm_strategy_opportunities": False, "llm_group_summaries_chunk_by_matrix": True, "chapter8_text_mining_probe": True, "chapter8_text_mining_probe_live_llm": True, diff --git a/backend/pipeline/reporting/report_strategy_excerpt.py b/backend/pipeline/reporting/report_strategy_excerpt.py index 1bb93f3..0333c3c 100644 --- a/backend/pipeline/reporting/report_strategy_excerpt.py +++ b/backend/pipeline/reporting/report_strategy_excerpt.py @@ -57,6 +57,9 @@ def load_report_strategy_excerpt( if md: s = md if len(md) <= cap else md[: cap - 80].rstrip() + "\n\n…(已截断)\n" return s, "json_markdown" + # 已存在落盘记录但无大模型正文时,不再回退截取 competitor_analysis.md: + # 第九章现为固定读者说明,避免误入策略稿 ``report_strategy_excerpt``。 + return "", "none" md_path = root / "competitor_analysis.md" if md_path.is_file(): diff --git a/backend/pipeline/reporting/strategy_draft.py b/backend/pipeline/reporting/strategy_draft.py index 5b9fe5f..48752ab 100644 --- a/backend/pipeline/reporting/strategy_draft.py +++ b/backend/pipeline/reporting/strategy_draft.py @@ -135,7 +135,7 @@ def filter_strategy_hints_for_ch8_probe(hints: Any) -> list[str]: continue out.append(s) return out if out else [ - "(与「关注词/预设场景条形图」相关的自动提示已省略;用户洞察请以报告 §8 文本挖掘与第九章节选为准。)" + "(与「关注词/预设场景条形图」相关的自动提示已省略;用户洞察请以报告 §8 文本挖掘及第五至第八章细类归纳为准;可执行策略以「策略制定」按细类生成为准。)" ] @@ -143,7 +143,8 @@ def report_uses_chapter8_text_mining_probe(report_config: dict[str, Any] | None) """ 与任务 ``report_config`` 中 ``chapter8_text_mining_probe`` 一致;未显式设置时默认 ``True`` (与 ``jd.runner.get_default_report_config`` 一致)。 - 为 ``True`` 时,策略稿「用户与评论侧」一节不再逐条列举关注词/场景子串命中,以免与当前报告正文口径冲突。 + 用于 §1.2 文案分支及对 ``strategy_hints`` 的过滤:开启探针时与子串命中枚举相关的自动线索会被压掉; + 关闭时 §1.2 仍说明「简报不附带预设关注词/场景子串统计」,评论侧以报告第八章探针(若启用)与原文为准。 """ if not isinstance(report_config, dict): return True @@ -254,29 +255,9 @@ def build_strategy_draft_markdown( ] ) else: - ckw = brief.get("comment_focus_keywords") or [] - usc = brief.get("usage_scenarios") or [] - lines.append("*下列为关注词/场景**统计摘录**(仅底稿审计用);**成稿删除逐条枚举**,只保留对策略有用的一两句结论,避免与同任务竞品分析报告重复。*") - lines.append("") - if ckw: - for item in ckw[:8]: - if isinstance(item, dict): - w = _esc(item.get("word")) - c = _num(item.get("count")) - lines.append( - f"- 「{w}」:子串统计命中约 **{c}** 次(口径同报告关注词)。" - ) - if usc: - for item in usc[:6]: - if isinstance(item, dict): - sc = _esc(item.get("scenario")) - cn = _num(item.get("count")) - sh = _pct(item.get("share_of_text_units")) - lines.append( - f"- 场景「{sc}」:约 **{cn}** 条,约占 **{sh}** 文本单元(预设场景分组)。" - ) - if not ckw and not usc: - lines.append("*摘要中无关注词/场景组,请结合评论侧分析补全本节。*") + lines.append( + "*简报中**不再**附带预设关注词/场景子串统计;评论侧请依据同任务《竞品分析报告》**第八章第二节**(文本挖掘探针,若已启用)及抽样原文撰写本节。*" + ) lines.append("") mix = brief.get("category_mix_top") or [] @@ -575,11 +556,7 @@ def build_strategy_draft_markdown( rk = bool(d.get("ack_risk_keywords")) rp = bool(d.get("ack_risk_price")) rc = bool(d.get("ack_risk_concentration")) - rk_kw = ( - "评论侧归纳是否以偏概全?(需原评论抽样)" - if use_ch8_probe - else "关注词/场景统计是否以偏概全?(需原评论抽样)" - ) + rk_kw = "评论侧归纳是否以偏概全?(需原评论抽样)" lines.extend( [ "## 九、风险、假设与待验证", diff --git a/backend/pipeline/tests/test_report_strategy_excerpt.py b/backend/pipeline/tests/test_report_strategy_excerpt.py index aa38c43..87a56e0 100644 --- a/backend/pipeline/tests/test_report_strategy_excerpt.py +++ b/backend/pipeline/tests/test_report_strategy_excerpt.py @@ -48,11 +48,22 @@ def test_load_prefers_json_markdown(tmp_path: Path) -> None: assert "JSON 内第九章" in text -def test_load_falls_back_to_competitor_md(tmp_path: Path) -> None: +def test_load_no_fallback_when_json_exists_without_markdown(tmp_path: Path) -> None: + """存在 strategy_opportunities_llm.json 但无 markdown 时不再截取 competitor_analysis(第九章可为固定说明)。""" (tmp_path / "strategy_opportunities_llm.json").write_text( json.dumps({"schema_version": 1, "ok": True}, ensure_ascii=False), encoding="utf-8", ) + (tmp_path / "competitor_analysis.md").write_text( + f"{CHAPTER_NINE_HEADING}(假设清单,待验证)\n\n从 MD 截取。\n\n## 附录 A\n", + encoding="utf-8", + ) + text, src = load_report_strategy_excerpt(tmp_path) + assert src == "none" + assert text == "" + + +def test_load_falls_back_to_competitor_md_when_no_json(tmp_path: Path) -> None: (tmp_path / "competitor_analysis.md").write_text( f"{CHAPTER_NINE_HEADING}(假设清单,待验证)\n\n从 MD 截取。\n\n## 附录 A\n", encoding="utf-8",