diff --git a/backend/pipeline/competitor_report/llm_group_payloads.py b/backend/pipeline/competitor_report/llm_group_payloads.py index 3a2f442..3cc7fa0 100644 --- a/backend/pipeline/competitor_report/llm_group_payloads.py +++ b/backend/pipeline/competitor_report/llm_group_payloads.py @@ -220,12 +220,11 @@ def build_promo_groups_llm_payload( def build_comment_groups_llm_payload( *, feedback_groups: list[tuple[str, list[dict[str, str]], list[str]]], - focus_words: tuple[str, ...], merged_rows: list[dict[str, str]], sku_header: str, title_h: str, ) -> list[dict[str, Any]]: - """供 ``generate_comment_group_summaries_llm``。""" + """供 ``generate_comment_group_summaries_llm``:仅含评价文本单元与短摘录,不含关注词子串计数摘要。""" if not feedback_groups: return [] sku_meta: dict[str, tuple[str, str, str]] = {} @@ -245,10 +244,6 @@ def build_comment_groups_llm_payload( for gname, cr, tu in feedback_groups: if not tu and not cr: continue - gh = _group_keyword_hits(cr, tu, focus_words=focus_words) - focus_hit_lines = [ - f"「{w}」{n} 次" for w, n in gh.most_common(14) if n > 0 - ] snippets: list[str] = [] for row in cr[:48]: txt = _cell(row, _COMMENT_CSV_BODY, "tagCommentContent") @@ -277,7 +272,6 @@ def build_comment_groups_llm_payload( "group": gname, "comment_flat_rows": f"评价行 {len(cr)};有效文本单元 {len(tu)}", "effective_text_lines": eff, - "focus_hit_lines": focus_hit_lines, "sample_text_snippets": snippets, } ) diff --git a/backend/pipeline/demos/chapter8_text_mining_probe.py b/backend/pipeline/demos/chapter8_text_mining_probe.py index 502357e..0b3ee29 100644 --- a/backend/pipeline/demos/chapter8_text_mining_probe.py +++ b/backend/pipeline/demos/chapter8_text_mining_probe.py @@ -100,14 +100,14 @@ PROBE_TEXT_MINING_SYSTEM = """你是用户研究与文本挖掘方向的助手 - ``probe_status``:``ok`` 表示该细类已完成分词与统计;``skipped`` 表示样本过少等未下钻。 - ``word_freq_top`` / ``tfidf_top`` / ``cooccurrence_top``:统计型特征(开放词表)。 - ``lda``:无监督自动归纳的主题词;**仅为探索**,同一词可出现在多主题,**禁止**当作严格品类或固定标签。 -- ``focus_hit_lines`` / ``sample_text_snippets``:与正式管线摘取方式**类似**,仅用于**对照语境**;若与关键词突出度焦点冲突,以**整句原文**为准,并在段末或「使用注意」中可点明「统计与语义可能不一致」。 +- ``sample_text_snippets``:与正式报告管线同源的评价短摘录(若有),仅用于**对照语境**;若与关键词突出度焦点冲突,以**整句原文**为准,并在段末或「使用注意」中可点明「统计与语义可能不一致」。 **任务**:对 ``groups`` 中**每一项**输出对应 Markdown(**顺序与输入一致**): - 对 ``probe_status == "ok"``:以 ``#### `` + 与该条 ``group`` 字段**完全一致**的细类名作为小节标题(勿用 ``##`` 一级标题);每段约 **100~260 字**。 -- 内容须包含:①用 **1~2 句**概括该细类评论**主要讨论焦点**(综合词频与关键词突出度,**不要罗列具体数字**);② **1~2 句**说明共现词对**暗示**哪些维度常一起出现(**非因果**);③若 ``lda.topics`` 非空,**1~2 句**说明主题粗分侧重点,并**明确**算法无监督、**不**与矩阵细类一一对应;④用 **1~2 句**体现 ``sample_text_snippets`` / ``focus_hit_lines`` 中的**用户语气与关切**(以**转述**为主);若必须引用原文,**全小节合计**仅 **一处**极短引号内容(**≤40 字**,**不要**输出 ``【细类…SKU…店铺…】`` 等长前缀);若无可用摘录则写明;⑤ **使用场景(仅从评论推断)**:用 **0~2 句**概括**何时、何地、何人、如何搭配**等(如早餐、加餐、控糖人群、配牛奶等)——**只能**依据本细类 ``word_freq``/``tfidf``/``cooccurrence``/``lda`` 与摘录中**已出现或可合理概括**的信息;**禁止**套用正式报告「场景分组」或其它外部场景分类;若统计与摘录中**均无**场景线索,**一句**写明「评论中未体现清晰使用场景」即可。 +- 内容须包含:①用 **1~2 句**概括该细类评论**主要讨论焦点**(综合词频与关键词突出度,**不要罗列具体数字**);② **1~2 句**说明共现词对**暗示**哪些维度常一起出现(**非因果**);③若 ``lda.topics`` 非空,**1~2 句**说明主题粗分侧重点,并**明确**算法无监督、**不**与矩阵细类一一对应;④用 **1~2 句**体现 ``sample_text_snippets`` 中的**用户语气与关切**(以**转述**为主);若必须引用原文,**全小节合计**仅 **一处**极短引号内容(**≤40 字**,**不要**输出 ``【细类…SKU…店铺…】`` 等长前缀);若无可用摘录则写明;⑤ **使用场景(仅从评论推断)**:用 **0~2 句**概括**何时、何地、何人、如何搭配**等(如早餐、加餐、控糖人群、配牛奶等)——**只能**依据本细类 ``word_freq``/``tfidf``/``cooccurrence``/``lda`` 与摘录中**已出现或可合理概括**的信息;**禁止**套用正式报告「场景分组」或其它外部场景分类;若统计与摘录中**均无**场景线索,**一句**写明「评论中未体现清晰使用场景」即可。 - 对 ``probe_status == "skipped"``:该小节仅 **一句**说明原因。 -**禁止**:编造数据中未出现的品牌、价格、医学功效或疗效承诺;不要把 ``keyword`` 监测词写进「用户原话」;不要输出 Markdown 表格;不要声称本段与「正式报告第八章末」完全同源——本任务为**补充分析解读**。**禁止**把输入里的 ``sample_text_snippets`` / ``focus_hit_lines`` **逐条罗列**、**多条整段复制**到输出(那不是归纳,是重复贴评论)。 +**禁止**:编造数据中未出现的品牌、价格、医学功效或疗效承诺;不要把 ``keyword`` 监测词写进「用户原话」;不要输出 Markdown 表格;不要声称本段与「正式报告第八章末」完全同源——本任务为**补充分析解读**。**禁止**把输入里的 ``sample_text_snippets`` **逐条罗列**、**多条整段复制**到输出(那不是归纳,是重复贴评论)。 全文末可另起一段 **「使用注意」**(简短):点明开放词表统计与人工阅读差异、主题归纳局限、小样本细类不可靠。 @@ -381,23 +381,6 @@ def _narrative_stub( return "\n".join(lines) -def _effective_focus_words(run_dir: Path) -> tuple[str, ...]: - """与 ``runner.write_competitor_analysis_for_run_dir`` 一致:优先 ``effective_report_config.json``。""" - p = run_dir / "effective_report_config.json" - if p.is_file(): - try: - eff = json.loads(p.read_text(encoding="utf-8")) - except json.JSONDecodeError: - eff = None - if isinstance(eff, dict): - fw_src = eff.get("comment_focus_words") or list(jcr.COMMENT_FOCUS_WORDS) - fw_tuple = tuple( - str(x).strip() for x in fw_src if str(x).strip() - ) or jcr.COMMENT_FOCUS_WORDS - return fw_tuple - return jcr.COMMENT_FOCUS_WORDS - - def _truncate_probe_payload(payload: dict[str, Any]) -> dict[str, Any]: """压缩摘录长度,避免单次 JSON 顶满上下文。""" out: dict[str, Any] = dict(payload) @@ -407,6 +390,7 @@ def _truncate_probe_payload(payload: dict[str, Any]) -> dict[str, Any]: groups.append(g) continue g2 = dict(g) + g2.pop("focus_hit_lines", None) sn = g2.get("sample_text_snippets") if isinstance(sn, list): # 条数略减,降低模型「照抄罗列」倾向;仍以转述为主见系统提示 @@ -421,7 +405,6 @@ def _merge_snippets_from_comment_groups( *, merged_rows: list[dict[str, str]], comment_rows: list[dict[str, str]], - run_dir: Path, ) -> None: """把正式 ``build_comment_groups_llm_payload`` 中的摘录并入补充分析行(原地修改)。""" sku_h = MERGED_FIELD_TO_CSV_HEADER["sku_id"] @@ -431,10 +414,8 @@ def _merge_snippets_from_comment_groups( comment_rows=comment_rows, sku_header=sku_h, ) - fw = _effective_focus_words(run_dir) pl = jcr.build_comment_groups_llm_payload( feedback_groups=fb, - focus_words=fw, merged_rows=merged_rows, sku_header=sku_h, title_h=title_h, @@ -448,9 +429,6 @@ def _merge_snippets_from_comment_groups( if not src: continue row["comment_flat_rows"] = src.get("comment_flat_rows") - fh = src.get("focus_hit_lines") - if isinstance(fh, list): - row["focus_hit_lines"] = [str(x) for x in fh[:8]] sn = src.get("sample_text_snippets") if isinstance(sn, list): row["sample_text_snippets"] = [str(x)[:220] for x in sn[:8]] @@ -689,15 +667,14 @@ def build_markdown( probe_rows, merged_rows=merged, comment_rows=comments, - run_dir=run_dir, ) llm_payload: dict[str, Any] = { "schema_version": 1, "keyword": kw, "probe_note": ( "中文分词 + 停用词;关键词突出度/共现/主题归纳为统计库;" - "与正式报告的关注词规则统计、已废弃的预设口语短语情感口径均不同;" - "评价摘录字段合并自 build_comment_groups_llm_payload,供语境对照;" + "与正式报告第八章第二节图表中的关注词计数等规则统计、已废弃的预设口语短语情感口径均不同;" + "评价短摘录合并自 build_comment_groups_llm_payload(与正式管线同源,不含关注词子串计数摘要),供语境对照;" "「使用场景」若出现,须仅能从本 JSON 内统计与摘录推断,不接入正式场景分组。" ), "groups": probe_rows, diff --git a/backend/pipeline/demos/run_report_llm_chapters_demo.py b/backend/pipeline/demos/run_report_llm_chapters_demo.py index 097f700..cc3a870 100644 --- a/backend/pipeline/demos/run_report_llm_chapters_demo.py +++ b/backend/pipeline/demos/run_report_llm_chapters_demo.py @@ -354,13 +354,8 @@ def main() -> None: comment_rows=comment_rows, sku_header=sku_h, ) - fw_src = eff_rc.get("comment_focus_words") or list(jcr.COMMENT_FOCUS_WORDS) - fw_tuple = tuple( - str(x).strip() for x in fw_src if str(x).strip() - ) or jcr.COMMENT_FOCUS_WORDS pl_cg = jcr.build_comment_groups_llm_payload( feedback_groups=fb, - focus_words=fw_tuple, merged_rows=merged, sku_header=sku_h, title_h=title_h, @@ -374,7 +369,7 @@ def main() -> None: return generate_comment_group_summaries_llm(pl_cg, keyword=keyword) _run_one( - "§8 细类评价与关注词(comment_groups)", + "§8 细类评价要点(comment_groups)", _cg, live=args.live, preview_chars=args.preview_chars, diff --git a/backend/pipeline/jd/runner.py b/backend/pipeline/jd/runner.py index dc1b331..d0eebd0 100644 --- a/backend/pipeline/jd/runner.py +++ b/backend/pipeline/jd/runner.py @@ -679,13 +679,8 @@ def write_competitor_analysis_for_run_dir( comment_rows=comment_rows, sku_header=sku_h, ) - fw_src = eff_rc.get("comment_focus_words") or list(jcr.COMMENT_FOCUS_WORDS) - fw_tuple = tuple( - str(x).strip() for x in fw_src if str(x).strip() - ) or jcr.COMMENT_FOCUS_WORDS pl_cg = jcr.build_comment_groups_llm_payload( feedback_groups=fb_cg, - focus_words=fw_tuple, merged_rows=merged_rows, sku_header=sku_h, title_h=title_h, diff --git a/backend/pipeline/llm/generate_group_summaries.py b/backend/pipeline/llm/generate_group_summaries.py index 14b160d..c9662bc 100644 --- a/backend/pipeline/llm/generate_group_summaries.py +++ b/backend/pipeline/llm/generate_group_summaries.py @@ -50,15 +50,14 @@ def generate_matrix_group_summaries_llm( COMMENT_GROUPS_SYSTEM = """你是用户研究与品类顾问。输入为 JSON:``keyword`` 与 ``groups``。 -每个 group 含 ``group``(与 第五章矩阵一致的细分类目名)、``comment_flat_rows``、``effective_text_lines``、 -``focus_hit_lines``(关注词子串命中摘要,与 **第八章第二节**(关注词与场景路径)左栏同源)、``sample_text_snippets``(评价短摘录,已截断)。 -摘录行通常以 ``【细类:…|SKU:…|品名:…|店铺:…】`` 开头:细类可与本 group 名对照,**品名/SKU/店铺**表示该句具体出自哪条链接;归纳时若引用原话,**须交代是「哪家店、哪条 SKU、哪款品名」上的反馈**,勿只写「有用户说口感差」而不指代产品。 -关注词命中为子串统计,可能与句意不一致;**请以整句语义**判断褒贬(如「软硬适中」「没那么甜」常为满意表述,不得据此写成质地问题)。 +每个 group 含 ``group``(与 第五章矩阵一致的细分类目名)、``comment_flat_rows``、``effective_text_lines``(该细类下从评价中抽取的短文本单元)、``sample_text_snippets``(带 SKU/品名/店铺前缀的评价短摘录,已截断)。 +摘录行通常以 ``【细类:…|SKU:…|品名:…|店铺:…】`` 开头:**品名/SKU/店铺**表示该句具体出自哪条链接;归纳时若引用原话,**须交代是「哪家店、哪条 SKU、哪款品名」上的反馈**,勿只写「有用户说口感差」而不指代产品。 +请**以整句语义**判断褒贬(如「软硬适中」「没那么甜」常为满意表述),不得仅凭片段词就写成负面结论。 请**为每个细类**输出一小段 Markdown(全部 groups 都要写,顺序与输入一致): - 以 ``#### `` + 与该 group 字段**完全一致**的细类名作为小节标题(不要使用 ``##`` 一级标题); -- 每段约 **100~220 字**中文:归纳该细类下**消费者在讨论什么**(口感、价格、物流、功效疑虑等)、**关注词命中反映的诉求**;勿编造摘录中未出现的品牌、医学结论; -- **去重与可证(本章仅评论侧)**:本段**只**依据评价/关注词摘录,**禁止**把 ``keyword``、品类常识或商品标题卖点套话写成「用户评价」;**禁止**各细类段首复用同一句总括(如「整体上满足了消费者对低 GI、高蛋白、便携性的需求」);每段开头句式须**有变化**,并至少一句体现**该细类与相邻细类在讨论焦点上的差异**。**利益/诉求词**(低 GI、高蛋白、便携、代餐、控糖等)**仅当**在 ``sample_text_snippets``、``effective_text_lines`` 或 ``focus_hit_lines`` 的**原文**中可子串命中或可明确同义(便携↔随身、小包装、单片、独立装等)时才写;若上述字段中**未**出现「蛋白」「便携」「随身」「小包装」「单片」等,则**不得**写「高蛋白」「便携性」等;**禁止**为凑齐常见卖点组合而脑补未在输入中出现的词。 +- 每段约 **100~220 字**中文:归纳该细类下**消费者在讨论什么**(口感、价格、物流、功效疑虑等);勿编造摘录中未出现的品牌、医学结论; +- **去重与可证(本章仅评论侧)**:本段**只**依据 ``sample_text_snippets`` 与 ``effective_text_lines`` 中的**原文**,**禁止**把 ``keyword``、品类常识或商品标题卖点套话写成「用户评价」;**禁止**各细类段首复用同一句总括(如「整体上满足了消费者对低 GI、高蛋白、便携性的需求」);每段开头句式须**有变化**,并至少一句体现**该细类与相邻细类在讨论焦点上的差异**。**利益/诉求词**(低 GI、高蛋白、便携、代餐、控糖等)**仅当**在上述字段的**原文**中可子串命中或可明确同义(便携↔随身、小包装、单片、独立装等)时才写;若**未**出现「蛋白」「便携」「随身」「小包装」「单片」等,则**不得**写「高蛋白」「便携性」等;**禁止**为凑齐常见卖点组合而脑补未在输入中出现的词。 - **禁止**输出 Markdown 表格、禁止逐条复述全部评价; - 若 ``effective_text_lines`` 很少,明确写「样本较少,归纳供启发」。 @@ -66,7 +65,7 @@ COMMENT_GROUPS_SYSTEM = """你是用户研究与品类顾问。输入为 JSON: COMMENT_GROUPS_USER_PREFIX = ( - "请根据以下 JSON 撰写竞品报告第八章末「细类评论与关注词要点归纳」正文(Markdown)。\n\n" + "请根据以下 JSON 撰写竞品报告第八章末「细类评论要点归纳」正文(Markdown)。\n\n" ) @@ -85,7 +84,6 @@ def generate_comment_group_summaries_llm( eff_max: int, sn_n: int, sn_max: int, - fh_n: int, ) -> dict[str, Any]: g2: dict[str, Any] = { "group": g.get("group"), @@ -101,11 +99,6 @@ def generate_comment_group_summaries_llm( g2["sample_text_snippets"] = [str(x)[:sn_max] for x in sn[:sn_n]] else: g2["sample_text_snippets"] = [] - fh = g.get("focus_hit_lines") - if isinstance(fh, list): - g2["focus_hit_lines"] = [str(x) for x in fh[:fh_n]] - else: - g2["focus_hit_lines"] = [] return g2 ctx = llm_context_window_size() @@ -115,24 +108,24 @@ def generate_comment_group_summaries_llm( est = estimate_chat_input_tokens(system, user_p) return est < 15_500 - levels: list[tuple[int, int, int, int, int]] = [ - (14, 260, 10, 200, 10), - (12, 220, 8, 180, 8), - (10, 180, 8, 160, 6), - (8, 150, 6, 140, 6), - (6, 120, 5, 120, 5), - (5, 100, 4, 100, 4), - (4, 80, 3, 80, 3), - (3, 70, 3, 70, 3), - (3, 50, 2, 60, 2), + levels: list[tuple[int, int, int, int]] = [ + (14, 260, 10, 200), + (12, 220, 8, 180), + (10, 180, 8, 160), + (8, 150, 6, 140), + (6, 120, 5, 120), + (5, 100, 4, 100), + (4, 80, 3, 80), + (3, 70, 3, 70), + (3, 50, 2, 60), ] user = "" chosen = levels[-1] for level in levels: chosen = level - eff_n, eff_max, sn_n, sn_max, fh_n = level + eff_n, eff_max, sn_n, sn_max = level trimmed = [ - _compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max, fh_n=fh_n) + _compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max) for g in groups if isinstance(g, dict) ] @@ -144,9 +137,9 @@ def generate_comment_group_summaries_llm( break else: tail = "\n\n…(JSON 已截断以适配上下文;仅依据可见字段撰写。)\n" - eff_n, eff_max, sn_n, sn_max, fh_n = chosen + eff_n, eff_max, sn_n, sn_max = chosen trimmed = [ - _compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max, fh_n=fh_n) + _compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max) for g in groups if isinstance(g, dict) ] diff --git a/backend/pipeline/tests/test_comment_group_summaries_llm.py b/backend/pipeline/tests/test_comment_group_summaries_llm.py index 9a69dd0..9912856 100644 --- a/backend/pipeline/tests/test_comment_group_summaries_llm.py +++ b/backend/pipeline/tests/test_comment_group_summaries_llm.py @@ -1,4 +1,4 @@ -"""§8 末「细类评论与关注词要点归纳」大模型:单细类烟测(mock 网关,不调真实 API)。""" +"""§8 末「细类评论要点归纳」大模型:单细类烟测(mock 网关,不调真实 API)。""" from __future__ import annotations import json @@ -44,7 +44,6 @@ def _payload_single_category_biscuit() -> tuple[list[dict], str, str]: ) pl = jcr.build_comment_groups_llm_payload( feedback_groups=fb, - focus_words=jcr.COMMENT_FOCUS_WORDS, merged_rows=merged, sku_header=sku_h, title_h=title_h, @@ -68,7 +67,7 @@ class CommentGroupSummariesLlmTests(SimpleTestCase): mock_llm.assert_called_once() _sys, user = mock_llm.call_args[0] - self.assertIn("细类评论与关注词要点归纳", user) + self.assertIn("细类评论要点归纳", user) self.assertIn("低GI测试", user) raw = user.split("正文(Markdown)。\n\n", 1)[-1] data = json.loads(raw.strip())