From 23ddf115eec44a4afb8032dc2a0cd56646f28363 Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Tue, 21 Apr 2026 10:29:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8A=A5=E5=91=8A):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=AC=AC=E5=85=AB=E7=AB=A0=E9=A2=84=E8=AE=BE=E5=8F=A3=E8=AF=AD?= =?UTF-8?q?=E7=9F=AD=E8=AF=AD=E6=83=85=E6=84=9F=E5=B0=8F=E8=8A=82=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除评价正负面粗判(扇形图、口语短语条形图、摘要及同口径 LLM 块);文本挖掘/关注词路径统一为新的 8.2;brief 不再含 comment_sentiment_lexicon;默认关闭 llm_comment_sentiment;清理旧插图文件名;同步策略与全稿提示。 Made-with: Cursor --- .../pipeline/competitor_report/constants.py | 2 +- .../competitor_report/consumer_feedback.py | 2 +- .../pipeline/competitor_report/jd_report.py | 137 ++---------------- .../demos/chapter8_text_mining_probe.py | 14 +- .../demos/dump_strategy_llm_input_md.py | 2 +- .../demos/run_report_llm_chapters_demo.py | 13 +- backend/pipeline/jd/runner.py | 12 +- .../pipeline/llm/generate_competitor_full.py | 9 +- backend/pipeline/llm/generate_strategy.py | 4 +- backend/pipeline/reporting/charts.py | 62 +------- .../pipeline/tests/test_competitor_brief.py | 15 +- 11 files changed, 57 insertions(+), 215 deletions(-) diff --git a/backend/pipeline/competitor_report/constants.py b/backend/pipeline/competitor_report/constants.py index 6196935..2954ae7 100644 --- a/backend/pipeline/competitor_report/constants.py +++ b/backend/pipeline/competitor_report/constants.py @@ -61,7 +61,7 @@ _COMMENT_CSV_SKU = COMMENT_CSV_COLUMNS[0] _COMMENT_CSV_BODY = COMMENT_CSV_COLUMNS[3] _COMMENT_CSV_SCORE = COMMENT_CSV_COLUMNS[7] # 「评分」→ commentScore -# 评价星级与 §8.2 分桶:先按评分筛正负,再在对应子集内统计口语短语(无评分时回退关键词) +# 评价星级分桶(comment_sentiment 模块):仍用于可选的情感 LLM 载荷;报告正文与 brief 已不再输出同口径图 _COMMENT_SCORE_NEG_MAX = 2 # 1~2 星 → 偏负向 _COMMENT_SCORE_POS_MIN = 4 # 4~5 星 → 偏正向(3 星为中评,归入中性) diff --git a/backend/pipeline/competitor_report/consumer_feedback.py b/backend/pipeline/competitor_report/consumer_feedback.py index e8a4a9f..4cc351f 100644 --- a/backend/pipeline/competitor_report/consumer_feedback.py +++ b/backend/pipeline/competitor_report/consumer_feedback.py @@ -20,7 +20,7 @@ def _comment_lines_with_product_context( sku_header: str, title_h: str, ) -> list[str]: - """与 ``comment_rows`` 顺序对齐:带细类/SKU/品名/店铺前缀,供 §8.2 大模型抽样。""" + """与 ``comment_rows`` 顺序对齐:带细类/SKU/品名/店铺前缀,供可选的情感/评论 LLM 载荷抽样。""" sku_meta: dict[str, tuple[str, str, str]] = {} for row in merged_rows: sku = _cell(row, sku_header).strip() diff --git a/backend/pipeline/competitor_report/jd_report.py b/backend/pipeline/competitor_report/jd_report.py index 178b8cd..608472e 100644 --- a/backend/pipeline/competitor_report/jd_report.py +++ b/backend/pipeline/competitor_report/jd_report.py @@ -58,9 +58,7 @@ from pipeline.competitor_report.price_promo import ( # noqa: E402 _markdown_price_promotion_section, ) from pipeline.competitor_report.comment_sentiment import ( # noqa: E402 - build_comment_sentiment_llm_payload, _comment_keyword_hits, - _comment_sentiment_lexicon, _iter_comment_text_units, _iter_comment_text_units_and_scores, _merge_comment_previews, @@ -225,7 +223,6 @@ def build_competitor_markdown( comment_texts, comment_scores = _iter_comment_text_units_and_scores( comment_rows, merged_rows ) - sentiment_lex = _comment_sentiment_lexicon(comment_texts, comment_scores) scen_counts, scen_n_texts = _comment_scenario_counts( comment_texts, scenario_groups ) @@ -292,9 +289,9 @@ def build_competitor_markdown( "- **评价主题词**:对评价正文做**预设词表子串计数**,非分词主题模型,适合扫方向,**需抽样人工验证**。", "- **用途/场景**:对每条评价独立判断是否命中预设场景词;一条可计入多个场景,统计的是「提及该场景的评价条数」而非用户数。", ( - "- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;**第八章第三节**另用分词、词频与主题模型等对评论做**补充分析**(与**第八章第二节**条形图口径不同、互为补充),可选词云并由大模型归纳要点。" + "- **用户画像(第八章)**:**不再**使用星级子集内**预设口语短语**条形图、正负面扇形图及同口径摘要;**第八章第二节**为分词、词频、共现与主题等**文本补充分析**(可选词云及大模型解读),结论须结合抽样原文理解。" if _ch8_probe_sec - else "- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;关注词与场景**仅按细类**以**同图左右并列**展示(左为关注词命中次数,右为场景占有效文本 **%**);见**第八章第三节**。" + else "- **用户画像(第八章)**:关注词与场景**仅按细类**以**同图左右并列**展示(左为关注词命中次数,右为场景占有效文本 **%**);见**第八章第二节**。" ), "- **细类划分(第五至第八章)**:**仅**依据合并表中的**商品详情页类目路径**;该信息缺失或无法读出细类名称的 SKU **不参与**竞品矩阵与按细类评价统计(相关评价条亦**不进入**按细类图表)。", "- **检索结果规模**:来自京东 PC 搜索返回的「结果条数」类指标,表示平台侧申报的匹配数量级,**不等于**动销、库存或独立 SKU 数。", @@ -389,7 +386,7 @@ def build_competitor_markdown( ) if multi_feedback_cat and (hits or scen_n_texts > 0): exec_bullets.append( - "评价侧写(关注词、用途/场景)已按**第五章同一细类划分**分节,见**第八章第三节**(同图并列)。" + "评价侧写(关注词、用途/场景)已按**第五章同一细类划分**分节,见**第八章第二节**(同图并列)。" ) elif hits: top3 = "、".join(f"「{w}」({n})" for w, n in hits.most_common(3)) @@ -743,13 +740,7 @@ def build_competitor_markdown( ] ) - _sm_score = sentiment_lex.get("method") == "score_then_lexeme" - _sec82_title = ( - "### 8.2 评价正负面粗判(评分优先 + 关键词回退)" - if _sm_score - else "### 8.2 评价正负面粗判(关键词规则)" - ) - _sec82_block: list[str] = [ + _sec8_open: list[str] = [ "---", "", "## 八、消费者反馈与用户画像(按细分类目)", @@ -758,123 +749,30 @@ def build_competitor_markdown( "", "- **细类划分**:与**第五章「竞品矩阵」**相同,**仅**依据合并表中的**商品详情页类目路径**解析为「饼干 / 西式糕点 / …」等(规则见第五章开头说明)。", "- **归因**:每条评价按其 SKU 对应到深入样本,再映射到该 SKU 所属细类;SKU 不在合并表中的评价单独归入说明性分组;**在合并表中但该 SKU 缺少类目路径或读不出细类名称的,该评价不进入按细类统计**(与第五章**同一条排除规则**)。", - "- **正负面粗判(第八章第二节)**:若评价含有效「评分」列则**先按星级**粗分正负与中评,再在对应子集内统计口语短语;无评分时仍按关键词子串;若任务开启**大模型评价情感分析**,可附**大模型对抽样原文的主题归因**,与条形图互补。", ( - "- **文本补充分析(第八章第三节)**:本任务已用中文分词与统计工具做了开放词表分析(词频、关键词突出度、词对共现、主题归纳等,可选词云),与**第八章第二节**规则词表条形图**不同**、**互补**;**不再**输出原「关注词次数 + 场景占比」左右并列条图。" + "- **评论文本补充分析(第八章第二节)**:本任务已用中文分词与统计工具做了开放词表分析(词频、关键词突出度、词对共现、主题归纳等,可选词云),**不再**在报告中使用「星级子集内预设口语短语」条形图、正负面扇形图及同口径摘要;**不再**输出原「关注词次数 + 场景占比」左右并列条图。" if _ch8_probe_sec - else "- **关注词与使用场景(第八章第三节)**:对组内评价正文做关注词子串计数(左栏条形图);对每条有效文本独立扫描**本次任务生效的场景词组**(来自报告调参或系统默认),一条可属多场景,右栏为**占该细类有效文本比例 %**(多标签下可相加 **>** 100%)。二者在 **同一张图左右并列**,与第五章矩阵细类一一对应。" + else "- **关注词与使用场景(第八章第二节)**:对组内评价正文做关注词子串计数(左栏条形图);对每条有效文本独立扫描**本次任务生效的场景词组**(来自报告调参或系统默认),一条可属多场景,右栏为**占该细类有效文本比例 %**(多标签下可相加 **>** 100%)。二者在 **同一张图左右并列**,与第五章矩阵细类一一对应。" ), "", - _sec82_title, - "", - f"- **有效文本条数**:{sentiment_lex.get('text_units', 0)}(与第八章第一节**归因规则**一致)。", ] - if _sm_score: - _sec82_block.append( - "- **正负面粗判规模**:本批存在有效「评分」时——**1~2 星**计为偏负向,**4~5 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;" - "无评分的条仍按关键词子串划分;「混合」仅在**无评分**且同条兼含正/负关键词时出现。" - ) - _sec82_block.extend( - [ - f"- **偏正向**:{sentiment_lex.get('positive_only', 0)} 条" - + ("(主要为 4~5 星)" if _sm_score else "(仅命中正向词表)") - + ";" - f"**偏负向**:{sentiment_lex.get('negative_only', 0)} 条" - + ("(主要为 1~2 星)" if _sm_score else "(仅命中负向词表)") - + ";" - f"**混合**:{sentiment_lex.get('mixed_positive_and_negative', 0)} 条" - + ("(无评分且同条兼含正/负关键词)" if _sm_score else "(同条兼含正/负词)") - + ";" - f"**中性或空文本**:{sentiment_lex.get('neutral_or_empty', 0)} 条" - + ("(含 3 星中评及无关键词命中)" if _sm_score else "") - + "。", - "- **说明**:" - + ( - "星级与正文可能不一致(如五星长文吐槽);口语短语条形图仅在对应星级子集内统计;正式结论请**人工抽样**阅读原文。" - if _sm_score - else "词表为方向性粗判,讽刺、省略与错别字会导致误判;正式结论请**人工抽样**阅读原文。" - ), - ] - ) - lines.extend(_sec82_block) - _scope = (sentiment_lex.get("lexeme_scope_note") or "").strip() - if _scope: - lines.append(f"- **词根统计说明**:{_scope}") - lines.extend(["", ""]) - lines.extend( - _embed_chart( - run_dir, - "chart_sentiment_overview_pie.png", - "评价正负面粗判规模(扇形图;与上表条数一致)", - ) - ) - lines.extend( - _embed_chart( - run_dir, - "chart_positive_lexemes_bar.png", - ( - "正向评价里**最常出现的口语短语**(在 **4~5 星** 评价条内统计;条形图)" - if _sm_score - else "正向评价里**最常出现的口语短语**(在偏正向或混合评价条内统计;条形图)" - ), - ) - ) - lines.extend( - _embed_chart( - run_dir, - "chart_negative_lexemes_bar.png", - ( - "负向评价里**最常出现的口语短语**(在 **1~2 星** 评价条内统计;条形图)" - if _sm_score - else "负向评价里**最常出现的口语短语**(在偏负向或混合评价条内统计;条形图)" - ), - ) - ) - pos_h = sentiment_lex.get("positive_tone_lexeme_hits") or [] - neg_h = sentiment_lex.get("negative_tone_lexeme_hits") or [] - if pos_h: - frag = ";".join( - f"「{x.get('word', '')}」{x.get('texts_matched', 0)} 条" - for x in pos_h[:6] - if isinstance(x, dict) - ) - lines.append(f"- **正向语境高频短语(摘要)**:{frag}。") - if neg_h: - frag_n = ";".join( - f"「{x.get('word', '')}」{x.get('texts_matched', 0)} 条" - for x in neg_h[:6] - if isinstance(x, dict) - ) - lines.append(f"- **负向语境高频短语(摘要)**:{frag_n}。") - _llm_s = (llm_sentiment_section_md or "").strip() - if _llm_s: - lines.extend( - [ - "", - "#### 大模型深入解读(主题归因,与词频统计互补)", - "", - "> **说明**:基于与上节**同一套评分优先或关键词归类规则**抽样的评价原文,由大模型归纳**用户在说什么**(尤其是负向的具体事由),与上列条数、条形图**互补**;引文以原评论为准。", - "", - _llm_s, - ] - ) - lines.append("") + lines.extend(_sec8_open) if _ch8_probe_sec: lines.extend( [ - "### 8.3 评论文本补充分析(词频、关键词与共现、主题归纳)", + "### 8.2 评论文本补充分析(词频、关键词与共现、主题归纳)", "", - "> **说明**:与**第八章第二节**口语短语条形图(规则词表)**口径不同**、**互补**;**不再**输出本章原「关注词 + 场景」左右并列条图;插图位于本批次报告附图文件夹中。", + "> **说明**:开放词表统计与分词主题探索;**勿**与已废弃的预设口语短语条形图混读;插图位于本批次报告附图文件夹中。", "", _ch8_probe_sec, "", ] ) else: - # 仅当未嵌入第八章第三节补充分析(_ch8_probe_sec 为空)时:原「关注词 + 场景」条图与逐细类段落 + # 仅当未嵌入第八章第二节探针补充分析(_ch8_probe_sec 为空)时:原「关注词 + 场景」条图与逐细类段落 lines.extend( [ - "### 8.3 关注词与使用场景(按细类)", + "### 8.2 关注词与使用场景(按细类)", "", "每细类一张**左右并列图**(与报告附图文件夹中的 ``chart_focus_and_scenarios_bar__*.png`` 同源):" "**左**为配置关注词子串命中次数(同一评价可出现多次,为次数而非去重条数);" @@ -931,9 +829,9 @@ def build_competitor_markdown( lines.extend( [ "", - "#### 使用场景要点归纳(大模型,与第八章第三节右栏图表互补)", + "#### 使用场景要点归纳(大模型,与第八章第二节右栏图表互补)", "", - "> **说明**:与第八章第三节**相同**的预设场景词组与子串命中规则;**各场景条数与占比以正文图右栏为准**。", + "> **说明**:与第八章第二节**相同**的预设场景词组与子串命中规则;**各场景条数与占比以正文图右栏为准**。", "", _llm_sg, "", @@ -945,9 +843,9 @@ def build_competitor_markdown( lines.extend( [ "", - "#### 细类评价与关注词要点归纳(大模型,与第八章第三节左栏图表互补)", + "#### 细类评价与关注词要点归纳(大模型,与第八章第二节左栏图表互补)", "", - "> **说明**:归纳各细类反馈主题与配置关注词命中;**次数与第八章第三节图左栏以正文为准**。", + "> **说明**:归纳各细类反馈主题与配置关注词命中;**次数与第八章第二节图左栏以正文为准**。", "", _llm_cg, "", @@ -1090,9 +988,6 @@ def build_competitor_brief( comment_texts, comment_scores = _iter_comment_text_units_and_scores( comment_rows, merged_rows ) - comment_sentiment_lexicon = _comment_sentiment_lexicon( - comment_texts, comment_scores - ) scen_counts, scen_n_texts = _comment_scenario_counts( comment_texts, scenario_groups ) @@ -1311,11 +1206,9 @@ def build_competitor_brief( "strategy_hints": hints, "matrix_by_group": matrix_groups, "consumer_feedback_by_matrix_group": feedback_by_group, - "comment_sentiment_lexicon": comment_sentiment_lexicon, "notes": [ "与在线分析报告各章**计数规则**一致;关注词与场景以任务中的分析规则为准(子串命中统计,非深度主题模型)。", "价格来自页面展示字段抽取,含促销与规格差异;促销与标价对齐等为启发式摘录,仅供对照。", - "评价语气为关键词粗判,非深度学习情感模型。", "「集中度」中:默认按**列表行**计数(同一 SKU 多页曝光会重复计);`shops_from_list.unique_sku_basis` 为按**去重 SKU** 的对照口径。二者均**不是**销量、库存或全渠道市场份额。", ], } diff --git a/backend/pipeline/demos/chapter8_text_mining_probe.py b/backend/pipeline/demos/chapter8_text_mining_probe.py index 0a32de9..502357e 100644 --- a/backend/pipeline/demos/chapter8_text_mining_probe.py +++ b/backend/pipeline/demos/chapter8_text_mining_probe.py @@ -2,7 +2,7 @@ 第八章「评论文本补充分析」独立脚本(**不修改**主报告核心逻辑)。 流程(按细类分组):清洗(中文分词 + 停用词)→ **词云图(可选)** → 词频 / 关键词突出度 → 词对共现 → 主题归纳 -→ 规则化叙事小结 → 文末可选 **专用 LLM**(结构化 JSON + ``PROBE_TEXT_MINING_SYSTEM`` + ``_call_llm``;**非** ``COMMENT_GROUPS_SYSTEM``、**非**第八章第二节情感)。 +→ 规则化叙事小结 → 文末可选 **专用 LLM**(结构化 JSON + ``PROBE_TEXT_MINING_SYSTEM`` + ``_call_llm``;**非** ``COMMENT_GROUPS_SYSTEM``、**非**已废弃的星级子集预设口语短语情感条形图)。 依赖(请自行安装):: @@ -17,7 +17,7 @@ 输出:默认写入 ``/chapter8_text_mining_probe.md``。 -嵌入竞品报告:流水线默认开启(``get_default_report_config`` 中 ``chapter8_text_mining_probe``: true);若任务显式关闭则为 false。开启时会生成本稿并调用 ``markdown_embed_body_for_competitor_report`` 写入 ``competitor_analysis.md`` 的 **第八章第三节**,替代原「关注词 + 场景」条图及对应两段大模型;**第八章第二节与「大模型深入解读(主题归因…)」保留**。 +嵌入竞品报告:流水线默认开启(``get_default_report_config`` 中 ``chapter8_text_mining_probe``: true);若任务显式关闭则为 false。开启时会生成本稿并调用 ``markdown_embed_body_for_competitor_report`` 写入 ``competitor_analysis.md`` 的 **第八章第二节(评论文本补充分析)**,替代原「关注词 + 场景」条图及对应两段大模型;**不再**嵌入原「评价正负面粗判」预设口语短语扇形图/条形图及同口径大模型块。 """ from __future__ import annotations @@ -91,10 +91,10 @@ _STOP_BASIC: frozenset[str] = frozenset( """.split() ) -# --- 评论文本补充分析 · 专用 LLM(与正式报告 ``COMMENT_GROUPS_SYSTEM`` / 第八章第二节均不同)--- +# --- 评论文本补充分析 · 专用 LLM(与正式报告 ``COMMENT_GROUPS_SYSTEM`` / 已废弃的预设口语短语情感口径均不同)--- PROBE_TEXT_MINING_SYSTEM = """你是用户研究与文本挖掘方向的助手。 -输入 JSON 为「第八章第三节评论文本补充分析」的**专用**结果(``schema_version``=1),**不是**正式竞品报告里的关注词规则统计、也不是第八章第二节情感 Lexicon。其中的数字与词表来自**中文分词 + 统计工具**(词频、关键词突出度、共现、主题归纳),与业务侧子串计数**口径不同**。 +输入 JSON 为「第八章第二节评论文本补充分析」的**专用**结果(``schema_version``=1),**不是**正式竞品报告里的关注词规则统计、也不是已废弃的星级子集预设口语短语 Lexicon。其中的数字与词表来自**中文分词 + 统计工具**(词频、关键词突出度、共现、主题归纳),与业务侧子串计数**口径不同**。 每个 ``groups`` 元素含: - ``probe_status``:``ok`` 表示该细类已完成分词与统计;``skipped`` 表示样本过少等未下钻。 @@ -551,7 +551,7 @@ def build_markdown( "## 8.0 说明", "", "本稿为**独立补充分析**,流程为:清洗评论 → 词云(可选)→ 词频与关键词 → 词对共现 → 主题归纳 → 文字小结;" - "文末可选用**专用提示词**由大模型解读(与第八章第二节所用口径不同)。" + "文末可选用**专用提示词**由大模型解读(与已废弃的预设口语短语情感口径不同)。" "**细类划分与 SKU 归因**与主报告一致;其余为中文分词与统计工具做的开放词表分析,**不替代**正式报告中的规则统计。", "", "---", @@ -696,7 +696,7 @@ def build_markdown( "keyword": kw, "probe_note": ( "中文分词 + 停用词;关键词突出度/共现/主题归纳为统计库;" - "与正式报告的关注词规则统计、第八章第二节情感口径均不同;" + "与正式报告的关注词规则统计、已废弃的预设口语短语情感口径均不同;" "评价摘录字段合并自 build_comment_groups_llm_payload,供语境对照;" "「使用场景」若出现,须仅能从本 JSON 内统计与摘录推断,不接入正式场景分组。" ), @@ -730,7 +730,7 @@ def build_markdown( def markdown_embed_body_for_competitor_report(full_probe_md: str) -> str: """ - 将独立补充分析稿转为可嵌入 ``build_competitor_markdown`` 的 **第八章第三节正文**(不含 ``### 8.3`` 标题行): + 将独立补充分析稿转为可嵌入 ``build_competitor_markdown`` 的 **第八章第二节正文**(不含 ``### 8.2`` 标题行;由 ``jd_report`` 统一加标题): 从 ``## 8.0 说明`` 起至文末,并把 ``## …`` 降为 ``#### …``,避免与宿主 ``## 八、`` 冲突。 """ lines = (full_probe_md or "").splitlines() diff --git a/backend/pipeline/demos/dump_strategy_llm_input_md.py b/backend/pipeline/demos/dump_strategy_llm_input_md.py index 2a5d359..05f90ef 100644 --- a/backend/pipeline/demos/dump_strategy_llm_input_md.py +++ b/backend/pipeline/demos/dump_strategy_llm_input_md.py @@ -196,7 +196,7 @@ def main() -> int: if report_uses_chapter8_text_mining_probe(rc): payload["structured_brief_omission_note"] = ( "已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top、" - "`strategy_hints`、`comment_sentiment_lexicon`(规则词表/条形图同源),避免与报告 §8 文本挖掘主口径冲突。**不得**再以这类子串计数或预设场景占比作为论据。" + "`strategy_hints` 等;全量 brief 已**不再**包含 ``comment_sentiment_lexicon``。**不得**再以星级子集预设口语短语计数或预设场景占比作为论据。" "用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。" ) diff --git a/backend/pipeline/demos/run_report_llm_chapters_demo.py b/backend/pipeline/demos/run_report_llm_chapters_demo.py index d25350d..097f700 100644 --- a/backend/pipeline/demos/run_report_llm_chapters_demo.py +++ b/backend/pipeline/demos/run_report_llm_chapters_demo.py @@ -3,9 +3,9 @@ - §5 后:``generate_matrix_group_summaries_llm`` - §6 后:``generate_price_group_summaries_llm``、``generate_promo_group_summaries_llm`` -- §8.2:``generate_comment_sentiment_analysis_llm`` +- (可选、默认关闭)``generate_comment_sentiment_analysis_llm``:已不再嵌入报告正文 - §8末细类评价:``generate_comment_group_summaries_llm`` -- §8.3 右栏后使用场景:``generate_scenario_group_summaries_llm`` +- §8.2 关注词/场景路径下右栏后使用场景:``generate_scenario_group_summaries_llm`` - §9 策略与机会:``generate_strategy_opportunities_llm``(``build_competitor_brief`` + 可选 ``chapter_llm_narratives`` 与各章归纳对齐) - §8.5 类全文补充(独立长文):``generate_competitor_report_markdown_llm`` @@ -38,6 +38,9 @@ if str(JCR_ROOT) not in sys.path: sys.path.insert(0, str(JCR_ROOT)) from pipeline.competitor_report import jd_report as jcr # noqa: E402 +from pipeline.competitor_report.comment_sentiment import ( # noqa: E402 + build_comment_sentiment_llm_payload, +) import jd_keyword_pipeline as kpl # noqa: E402 from pipeline.csv.schema import MERGED_FIELD_TO_CSV_HEADER # noqa: E402 @@ -199,7 +202,7 @@ def main() -> None: attr_units = list(comment_units) def _sent() -> str: - pl = jcr.build_comment_sentiment_llm_payload( + pl = build_comment_sentiment_llm_payload( comment_units, scores=comment_scores, attributed_texts=attr_units, @@ -210,7 +213,7 @@ def main() -> None: return generate_comment_sentiment_analysis_llm(pl) _run_one( - "§8.2 评价正负面(llm_comment_sentiment)", + "已弃用嵌入 · 评价情感 LLM 演示(llm_comment_sentiment)", _sent, live=args.live, preview_chars=args.preview_chars, @@ -333,7 +336,7 @@ def main() -> None: return generate_scenario_group_summaries_llm(pl_sg, keyword=keyword) _run_one( - "§8.3 使用场景归纳(scenario_groups)", + "§8.2 路径 · 使用场景归纳(scenario_groups)", _sg, live=args.live, preview_chars=args.preview_chars, diff --git a/backend/pipeline/jd/runner.py b/backend/pipeline/jd/runner.py index 068a0d3..dc1b331 100644 --- a/backend/pipeline/jd/runner.py +++ b/backend/pipeline/jd/runner.py @@ -34,7 +34,7 @@ def merge_llm_supplement_with_rules_report(llm_md: str, rules_md: str) -> str: marker = "\n---\n\n## 九、策略与机会提示(假设清单,待验证)" insert = ( "\n\n---\n\n" - "### 8.5 大模型深度补充(与第二章至第八章第三节正文中的定量内容互补)\n\n" + "### 8.5 大模型深度补充(与第二章至第八章第二节正文中的定量内容互补)\n\n" "> **说明**:本段位于**第八章末**;**竞品矩阵、价盘表、统计图与第八章第二、三节等各节正文以相应章节为准**," "此处为跨小节语义整合,便于衔接第九章。\n\n" f"{sup.strip()}\n" @@ -50,7 +50,7 @@ def merge_llm_supplement_with_rules_report(llm_md: str, rules_md: str) -> str: if alt in body and marker not in body: return body.replace( alt, - "\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第三节正文中的定量内容互补)\n\n" + "\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第二节正文中的定量内容互补)\n\n" "> **说明**:位于第八章末;**矩阵与图表以正文为准**。\n\n" f"{sup.strip()}\n" + alt, @@ -59,7 +59,7 @@ def merge_llm_supplement_with_rules_report(llm_md: str, rules_md: str) -> str: app = "\n## 附录 A:数据留存说明" if app in body: tail = ( - "\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第三节正文中的定量内容互补)\n\n" + "\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第二节正文中的定量内容互补)\n\n" f"{sup.strip()}\n" ) return body.replace(app, tail + app, 1) @@ -167,7 +167,7 @@ def get_default_report_config() -> dict[str, Any]: """与 ``pipeline.competitor_report.jd_report`` 模块常量一致的默认报告调参(供前端回填)。""" jcr, _ = _jd_crawler_modules() return { - "llm_comment_sentiment": True, + "llm_comment_sentiment": False, "llm_matrix_group_summaries": True, "llm_comment_group_summaries": True, "llm_scenario_group_summaries": True, @@ -728,10 +728,6 @@ def write_competitor_analysis_for_run_dir( from ..llm.generate import generate_strategy_opportunities_llm _strategy_narratives: dict[str, str] = {} - if (llm_sentiment_md or "").strip(): - _strategy_narratives["sec8_2_sentiment_theme_attribution"] = ( - llm_sentiment_md - ) if (llm_matrix_md or "").strip(): _strategy_narratives["sec5_matrix_group_summaries"] = llm_matrix_md if (llm_price_md or "").strip(): diff --git a/backend/pipeline/llm/generate_competitor_full.py b/backend/pipeline/llm/generate_competitor_full.py index d3ed678..a916d65 100644 --- a/backend/pipeline/llm/generate_competitor_full.py +++ b/backend/pipeline/llm/generate_competitor_full.py @@ -26,11 +26,10 @@ REPORT_SYSTEM = """你是业务与产品读者顾问。输入 JSON 含 `keyword` **请输出**(仅输出将置于 8.5 小节 下的正文,不要自造「### 8.5」标题行): - **Markdown**,约 **800~1500 字**; - 建议用 ``####`` 组织:**执行摘要级要点**、**竞争与价盘**、**用户声量与负向事由**(须归纳用户在抱怨什么类型的问题,而非只堆关键词)、**后续可验证动作(假设)**(不写第九章目录或重复策略章内容); -- 若有 `comment_sentiment_lexicon`,概括正/负向粗判局限;**负向**写清事由类型(口感、价格、物流等); -- **归因与引语(硬性)**:`consumer_feedback_by_matrix_group` 与 `comment_sentiment_lexicon` 等均为**跨 SKU/跨店铺的关键词子串或条数统计**,**不能**单独据此推断「某一店铺某一单品」的结论。 - - **具体体验句式(含口感、包装等)**须以正文 **第八章第二节** 中带 ``【细类|SKU|品名|店铺】`` 前缀的抽样为准;本段**不要**新增无前缀、无店铺/品名/SKU 指向的「」引语。 - - 若写口感、包装、物流、价格等**聚合**维度,须**写明统计范围**(如「在已合并的评价文本中,『物流』『价格』类关键词命中较多,为全样本子串计数」);可结合 `matrix_overview_for_llm` 谈细类结构;**可一句**引导读者「见第八章第二节按店铺/品名的负向举例」。 - - 若 第八章第二节 已归纳带店铺与 SKU 的负向主题,本段**只做执行摘要级收束**,勿重复编造新引文。 +- **归因与引语(硬性)**:`consumer_feedback_by_matrix_group` 等为**跨 SKU/跨店铺的关键词子串或条数统计**,**不能**单独据此推断「某一店铺某一单品」的结论。 + - **具体体验句式(含口感、包装等)**须以正文 **第八章** 中带 ``【细类|SKU|品名|店铺】`` 前缀的抽样或文本挖掘归纳为准;本段**不要**新增无前缀、无店铺/品名/SKU 指向的「」引语。 + - 若写口感、包装、物流、价格等**聚合**维度,须**写明统计范围**;可结合 `matrix_overview_for_llm` 谈细类结构;**可一句**引导读者「见第八章按店铺/品名的评价归纳」。 + - 若第八章已归纳带店铺与 SKU 的负向主题,本段**只做执行摘要级收束**,勿重复编造新引文。 - 语气专业、中文;某类信息在输入中缺失时**一句带过数据缺口**即可,**禁止**输出「本段未提供该项」等套话占位。""" REPORT_USER_PREFIX = """请根据以下 JSON 撰写上文所述 8.5 小节 嵌入段落(Markdown 正文,勿加 ### 8.5 标题)。\n\n""" diff --git a/backend/pipeline/llm/generate_strategy.py b/backend/pipeline/llm/generate_strategy.py index e4cbecc..308a14f 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -220,7 +220,7 @@ def generate_strategy_draft_markdown_llm( if report_uses_chapter8_text_mining_probe(report_config): payload["structured_brief_omission_note"] = ( "已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top、" - "``strategy_hints``、``comment_sentiment_lexicon``(规则词表/条形图同源),避免与报告 §8 文本挖掘主口径冲突。**不得**再以这类子串计数、短语条形图或预设场景占比作为论据。" + "``strategy_hints`` 等;报告已**不再**输出 ``comment_sentiment_lexicon``(星级子集预设口语短语)及同口径图。**不得**再以这类子串计数、短语条形图或预设场景占比作为论据。" "用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。" ) raw = json.dumps(payload, ensure_ascii=False) @@ -290,7 +290,7 @@ STRATEGY_OPPORTUNITIES_SYSTEM = ( - **定性主题**(各细类讨论焦点、正负向体验、场景与关注词归纳、配料/卖点叙事、促销形态描述等)须与 ``prior_chapter_llm_narratives`` 中已出现的表述**方向一致**,**禁止**另写一套与节选**明显矛盾**的品类判断、品牌举例或用户痛点主题。 - **定量与可核验事实**(价带分位数、店铺/品牌占比、条数、评论统计字段等)**以** ``competitor_brief`` **为准**;若节选与 brief 数字冲突,**采纳 brief**,且勿复述与数字冲突的节选句。 - 若某键未出现在 ``prior_chapter_llm_narratives`` 或内容为空,则该维度**不得**编造与可能存在的报告其他章冲突的细节;仅依据 ``competitor_brief`` 或明确写「输入中未体现」。 -- **转化与体验**小节:正负向体验线索须**优先呼应** ``sec8_2_sentiment_theme_attribution`` 与 **第八章第三节 侧**节选(``sec8_3_comment_focus_summaries`` 或 ``sec8_3_text_mining_probe``,视何者存在);**禁止**将节选未提及的具体抱怨/品类问题写成**主要结论**;可写「假设:待结合业务验证」。 +- **转化与体验**小节:正负向体验线索须**优先呼应** **第八章第二节 侧**节选(``sec8_3_comment_focus_summaries`` 或 ``sec8_3_text_mining_probe``,视何者存在;内部键名仍沿用 ``sec8_3_*``);**禁止**将节选未提及的具体抱怨/品类问题写成**主要结论**;可写「假设:待结合业务验证」。 **标题与措辞(硬性)**: - **禁止**在正文开头或任何位置重复宿主已有章名/小节名,包括但不限于:「第九章」「第9章」「九、」「策略与机会提示」「策略与机会建议」「策略与机会」等;**不要**自造 ``##`` 一级标题; diff --git a/backend/pipeline/reporting/charts.py b/backend/pipeline/reporting/charts.py index 021760c..561b2a3 100644 --- a/backend/pipeline/reporting/charts.py +++ b/backend/pipeline/reporting/charts.py @@ -225,6 +225,10 @@ _OBSOLETE_REPORT_ASSETS: frozenset[str] = frozenset( "chart_focus_keywords_pie.png", "chart_comment_focus_global_bar.png", "chart_usage_scenarios_global_bar.png", + "chart_sentiment_overview_pie.png", + "chart_sentiment.png", + "chart_positive_lexemes_bar.png", + "chart_negative_lexemes_bar.png", } ) @@ -265,7 +269,7 @@ def generate_report_charts( """生成扇形/条形 PNG。返回已写入的文件名列表(不含路径)。 若 ``report_config["chapter8_text_mining_probe"]`` 为真,**不**生成 ``chart_focus_and_scenarios_bar__*.png`` - (与竞品报告 §8.3 文本挖掘探针互斥,避免无效产出)。 + (与竞品报告 §8.2 文本挖掘探针互斥,避免无效产出)。 """ _setup_matplotlib_cjk() import matplotlib.pyplot as plt @@ -634,62 +638,6 @@ def generate_report_charts( n_texts=n_texts, ) - sent = brief.get("comment_sentiment_lexicon") or {} - if isinstance(sent, dict): - _score_mode = (sent.get("method") or "") == "score_then_lexeme" - pie_labs = ( - ["偏正向(4~5星)", "偏负向(1~2星)", "关键词混合", "中性/空"] - if _score_mode - else ["偏正向", "偏负向", "正负混合", "中性/空"] - ) - pie_vals = [ - float(sent.get("positive_only") or 0), - float(sent.get("negative_only") or 0), - float(sent.get("mixed_positive_and_negative") or 0), - float(sent.get("neutral_or_empty") or 0), - ] - pl = [a for a, b in zip(pie_labs, pie_vals) if b > 0] - pv = [b for b in pie_vals if b > 0] - save_pie(pl, pv, "评价语气四象限占比", "chart_sentiment_overview_pie.png") - save_bar_h( - pl, - pv, - "评价正负面粗判(条数)", - "chart_sentiment.png", - "条数", - ) - - pos_h = sent.get("positive_tone_lexeme_hits") or [] - neg_h = sent.get("negative_tone_lexeme_hits") or [] - plx, pvx = _label_count_pairs( - pos_h, key_label="word", key_count="texts_matched", cap=16 - ) - save_bar_h( - plx, - pvx, - ( - "4~5星语境 · 正向口语短语命中条数" - if _score_mode - else "正向/混合语境 · 正向口语短语命中条数" - ), - "chart_positive_lexemes_bar.png", - "条数", - ) - nlx, nvx = _label_count_pairs( - neg_h, key_label="word", key_count="texts_matched", cap=16 - ) - save_bar_h( - nlx, - nvx, - ( - "1~2星语境 · 负向口语短语命中条数" - if _score_mode - else "负向/混合语境 · 负向口语短语命中条数" - ), - "chart_negative_lexemes_bar.png", - "条数", - ) - matrix_groups = brief.get("matrix_by_group") or [] if isinstance(matrix_groups, list): for gi, block in enumerate(matrix_groups): diff --git a/backend/pipeline/tests/test_competitor_brief.py b/backend/pipeline/tests/test_competitor_brief.py index 60aa257..aa600dd 100644 --- a/backend/pipeline/tests/test_competitor_brief.py +++ b/backend/pipeline/tests/test_competitor_brief.py @@ -7,6 +7,10 @@ from pathlib import Path from django.test import SimpleTestCase from pipeline.competitor_report import jd_report as jcr +from pipeline.competitor_report.comment_sentiment import ( + _comment_sentiment_lexicon, + build_comment_sentiment_llm_payload, +) from pipeline.csv.schema import infer_total_sales_from_sales_floor from pipeline.reporting.charts import _cn_volume_int @@ -30,8 +34,7 @@ class BuildCompetitorBriefTests(SimpleTestCase): self.assertEqual(out["scope"]["merged_sku_count"], 0) self.assertIsInstance(out["strategy_hints"], list) self.assertEqual(out["matrix_by_group"], []) - self.assertIn("comment_sentiment_lexicon", out) - self.assertEqual(out["comment_sentiment_lexicon"].get("text_units"), 0) + self.assertNotIn("comment_sentiment_lexicon", out) import json json.dumps(out) @@ -39,7 +42,7 @@ class BuildCompetitorBriefTests(SimpleTestCase): def test_comment_sentiment_llm_payload_has_semantic_pool(self) -> None: texts = ["口感软硬适中很好吃", "太差了不建议"] attr = [f"【细类:A|SKU:1|品名:x|店铺:y】{t}" for t in texts] - pl = jcr.build_comment_sentiment_llm_payload( + pl = build_comment_sentiment_llm_payload( texts, attributed_texts=attr, shuffle_seed="unit-test-seed", @@ -52,18 +55,18 @@ class BuildCompetitorBriefTests(SimpleTestCase): def test_comment_sentiment_score_then_lexeme(self) -> None: texts = ["很好吃", "太差了", "一般般"] scores = [5, 1, 3] - lex = jcr._comment_sentiment_lexicon(texts, scores) + lex = _comment_sentiment_lexicon(texts, scores) self.assertEqual(lex.get("method"), "score_then_lexeme") self.assertEqual(lex.get("positive_only"), 1) self.assertEqual(lex.get("negative_only"), 1) self.assertEqual(lex.get("neutral_or_empty"), 1) - pl = jcr.build_comment_sentiment_llm_payload(texts, scores=scores) + pl = build_comment_sentiment_llm_payload(texts, scores=scores) self.assertEqual(pl.get("sentiment_bucket_method"), "score_then_lexeme") def test_comment_sentiment_all_scores_missing_falls_back_keyword(self) -> None: texts = ["好吃推荐", "差评"] scores = [None, None] - lex = jcr._comment_sentiment_lexicon(texts, scores) + lex = _comment_sentiment_lexicon(texts, scores) self.assertEqual(lex.get("method"), "keyword_lexicon") def test_custom_focus_words_in_report_config(self) -> None: