feat(报告): 移除第八章预设口语短语情感小节并重编号

删除评价正负面粗判(扇形图、口语短语条形图、摘要及同口径 LLM 块);文本挖掘/关注词路径统一为新的 8.2;brief 不再含 comment_sentiment_lexicon;默认关闭 llm_comment_sentiment;清理旧插图文件名;同步策略与全稿提示。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-21 10:29:26 +08:00
parent 7506f9795c
commit 23ddf115ee
11 changed files with 57 additions and 215 deletions

View File

@ -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 # 12 星 → 偏负向
_COMMENT_SCORE_POS_MIN = 4 # 45 星 → 偏正向3 星为中评,归入中性)

View File

@ -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()

View File

@ -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(
"- **正负面粗判规模**:本批存在有效「评分」时——**12 星**计为偏负向,**45 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;"
"无评分的条仍按关键词子串划分;「混合」仅在**无评分**且同条兼含正/负关键词时出现。"
)
_sec82_block.extend(
[
f"- **偏正向**{sentiment_lex.get('positive_only', 0)}"
+ ("(主要为 45 星)" if _sm_score else "(仅命中正向词表)")
+ ""
f"**偏负向**{sentiment_lex.get('negative_only', 0)}"
+ ("(主要为 12 星)" 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",
(
"正向评价里**最常出现的口语短语**(在 **45 星** 评价条内统计;条形图)"
if _sm_score
else "正向评价里**最常出现的口语短语**(在偏正向或混合评价条内统计;条形图)"
),
)
)
lines.extend(
_embed_chart(
run_dir,
"chart_negative_lexemes_bar.png",
(
"负向评价里**最常出现的口语短语**(在 **12 星** 评价条内统计;条形图)"
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** 的对照口径。二者均**不是**销量、库存或全渠道市场份额。",
],
}

View File

@ -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 @@
输出默认写入 ``<run_dir>/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()

View File

@ -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`(第九章)对齐,不得省略报告已写明的活动建议。"
)

View File

@ -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,

View File

@ -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():

View File

@ -26,11 +26,10 @@ REPORT_SYSTEM = """你是业务与产品读者顾问。输入 JSON 含 `keyword`
**请输出**仅输出将置于 8.5 小节 下的正文不要自造### 8.5」标题行):
- **Markdown** **8001500 **
- 建议用 ``####`` 组织:**执行摘要级要点**、**竞争与价盘**、**用户声量与负向事由**(须归纳用户在抱怨什么类型的问题,而非只堆关键词)、**后续可验证动作(假设)**(不写第九章目录或重复策略章内容);
- 若有 `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"""

View File

@ -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章策略与机会提示策略与机会建议策略与机会**不要**自造 ``##`` 一级标题;

View File

@ -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 = (
["偏正向(45星)", "偏负向(12星)", "关键词混合", "中性/空"]
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,
(
"45星语境 · 正向口语短语命中条数"
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,
(
"12星语境 · 负向口语短语命中条数"
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):

View File

@ -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"【细类ASKU1品名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: