diff --git a/backend/pipeline/llm/generate_strategy.py b/backend/pipeline/llm/generate_strategy.py index 96ac9dd..871c831 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -8,6 +8,7 @@ from typing import Any from ..reporting.brief_compact import compact_brief_for_llm from ..reporting.strategy_draft import ( build_strategy_draft_markdown, + filter_strategy_hints_for_ch8_probe, report_uses_chapter8_text_mining_probe, ) from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window_size @@ -30,6 +31,11 @@ STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要** **矩阵**:若 `structured_brief` 含矩阵相关字段,须**呼应**细分类目与竞品矩阵结论,不得无故删光。 +**第八章文本挖掘探针(当 JSON 中 `chapter8_text_mining_probe` 为真时,硬性)**: +- **禁止**将「关注词子串命中次数」「预设场景分组条数/占比」当作评论侧或需求侧的主要论据(`structured_brief` 已省略这些字段;与报告正文 §8 主口径一致)。 +- 用户洞察、场景与传播结论须与宿主报告 **§8 文本挖掘** 及 `report_strategy_excerpt`(第九章)一致。 +- **促销与活动**:若 `report_strategy_excerpt` 或 `structured_brief.price_promotion_signals` 中出现满减、券、到手价差、促销形态等,成稿须在**价格/营销/渠道**相关段落**承接**,与报告 **第六章、第九章** 建议一致,**不得无故省略**;**禁止**编造具体满减门槛或补贴比例(仅复述输入中已有线索)。 + **输出**:仅 Markdown 正文(不要 ``` 围栏);须收束各小节与全文,勿中途截断。""" STRATEGY_USER_PREFIX = """请基于以下 JSON 输出最终策略稿(Markdown)。\n\n""" @@ -80,6 +86,10 @@ def generate_strategy_draft_markdown_llm( "usage_scenarios_by_matrix_group", ): compact.pop(k, None) + if isinstance(compact.get("strategy_hints"), list): + compact["strategy_hints"] = filter_strategy_hints_for_ch8_probe( + compact["strategy_hints"] + ) ex = ( _truncate_strategy_narrative(excerpt_raw, excerpt_max) if excerpt_raw @@ -98,11 +108,15 @@ def generate_strategy_draft_markdown_llm( "structured_brief": compact, "rules_draft_markdown": rd, "report_strategy_excerpt": ex, + "chapter8_text_mining_probe": bool( + report_uses_chapter8_text_mining_probe(report_config) + ), } if report_uses_chapter8_text_mining_probe(report_config): payload["structured_brief_omission_note"] = ( - "已启用第八章文本挖掘:structured_brief 已省略关注词/场景子串计数字段,避免与报告正文口径冲突。" - "请依报告 §8 与摘要其他字段,将底稿「用户与评论侧」一节写成具体执行要点(需求焦点、场景、传播),勿逐条编造子串命中列表。" + "已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」及「与条形图同源的 strategy_hints 句子」," + "避免与报告 §8 主口径冲突。**不得**再以词频或预设场景占比作为论据。" + "用户与评论侧须依报告 §8 文本挖掘归纳;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。" ) raw = json.dumps(payload, ensure_ascii=False) if len(raw) > 500_000: diff --git a/backend/pipeline/reporting/strategy_draft.py b/backend/pipeline/reporting/strategy_draft.py index 8672cab..61aa848 100644 --- a/backend/pipeline/reporting/strategy_draft.py +++ b/backend/pipeline/reporting/strategy_draft.py @@ -117,6 +117,28 @@ def _risk_line(checked: bool, text: str) -> str: return f"- {mark} {text}" +def filter_strategy_hints_for_ch8_probe(hints: Any) -> list[str]: + """ + 当报告以 **第八章文本挖掘** 为主呈现评论侧时,规则引擎的 ``strategy_hints`` 中仍可能含 + 「关注词出现较多」「预设场景占比」类句子(与 §8 主口径冲突)。此处剔除,避免进入策略底稿与 LLM。 + """ + if not isinstance(hints, list): + return [] + out: list[str] = [] + for h in hints: + s = _esc(h) if h is not None else "" + if not s.strip(): + continue + if "评价文本中「" in s and "等主题出现较多" in s: + continue + if "用途/场景中「" in s and "有效评价自述" in s: + continue + out.append(s) + return out if out else [ + "(与「关注词/预设场景条形图」相关的自动提示已省略;用户洞察请以报告 §8 文本挖掘与第九章节选为准。)" + ] + + def report_uses_chapter8_text_mining_probe(report_config: dict[str, Any] | None) -> bool: """ 与任务 ``report_config`` 中 ``chapter8_text_mining_probe`` 一致;未显式设置时默认 ``True`` @@ -353,12 +375,21 @@ def build_strategy_draft_markdown( lines.append("*摘要中无关注词/场景组,请结合评论侧分析补全本节。*") lines.append("") - hints = brief.get("strategy_hints") or [] + raw_hints = brief.get("strategy_hints") or [] + hints = ( + filter_strategy_hints_for_ch8_probe(raw_hints) + if use_ch8_probe + else (list(raw_hints) if isinstance(raw_hints, list) else []) + ) lines.extend( [ "## 七、机会与策略支柱", "", - "### 摘要提示(`strategy_hints`)", + ( + "### 摘要提示(`strategy_hints`,已按探针口径过滤)" + if use_ch8_probe + else "### 摘要提示(`strategy_hints`)" + ), "", ] ) @@ -385,6 +416,22 @@ def build_strategy_draft_markdown( "", ] ) + if use_ch8_probe: + pst_sig = brief.get("price_promotion_signals") or {} + has_promo = isinstance(pst_sig, dict) and bool(pst_sig) + lines.extend( + [ + "### 促销与活动(须与报告对齐)", + "", + "*评论侧需求与场景以报告 **§8 文本挖掘** 为准,**不以**关注词/场景子串统计为论据。*", + ( + "*促销、满减、券后价差等:须与报告 **第六章** 及摘要 `price_promotion_signals`、**第九章**节选一致;成稿须承接报告已写明的活动与价差线索。*" + if has_promo + else "*促销与价差:若报告 **第六章/第九章** 或摘要 `price_promotion_signals` 有归纳,成稿须承接;无则勿编造具体满减门槛。*" + ), + "", + ] + ) rk = bool(d.get("ack_risk_keywords")) rp = bool(d.get("ack_risk_price")) diff --git a/backend/pipeline/tests/test_strategy_draft.py b/backend/pipeline/tests/test_strategy_draft.py index f6b7d85..bec5310 100644 --- a/backend/pipeline/tests/test_strategy_draft.py +++ b/backend/pipeline/tests/test_strategy_draft.py @@ -133,3 +133,26 @@ class StrategyDraftTests(SimpleTestCase): self.assertIn("按去重 SKU", md) self.assertIn("120", md) self.assertIn("35.0%", md) + + def test_chapter8_probe_filters_strategy_hints_focus_scenario_lines(self) -> None: + brief = { + "schema_version": 1, + "keyword": "K", + "strategy_hints": [ + "评价文本中「口感、甜」等主题出现较多,可作为假设输入(非严格主题模型)。", + "用途/场景中「控糖/血糖相关」在约 72% 的有效评价自述中出现,可作为优先假设(词组规则)。", + "样本内品牌较分散,存在定位空间(需验证)。", + ], + "price_promotion_signals": {"rows_with_both_list_and_coupon": 10}, + } + md = build_strategy_draft_markdown( + job_id=1, + keyword="K", + brief=brief, + report_config={"chapter8_text_mining_probe": True}, + ) + self.assertNotIn("评价文本中「口感", md) + self.assertNotIn("用途/场景中「控糖", md) + self.assertIn("样本内品牌较分散", md) + self.assertIn("促销与活动(须与报告对齐)", md) + self.assertIn("price_promotion_signals", md)