From 4cf73e7602b4807e07e9c0fb4b83b7b3e731facd Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Tue, 21 Apr 2026 17:03:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(strategy):=20=E6=97=A0=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E5=86=B3=E7=AD=96=E6=97=B6=E7=94=B1=E5=A4=A7=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=8E=A8=E6=96=AD=E8=8D=89=E6=A1=88=E5=B9=B6=E5=87=80=E5=8C=96?= =?UTF-8?q?=E5=AF=B9=E5=A4=96=E6=88=90=E7=A8=BF=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 strategy_decisions_substantive 与 JSON 载荷字段;STRATEGY_SYSTEM/USER_PREFIX 要求实质成稿、禁止技术泄露;build_strategy_draft_markdown(for_llm_input=True) 供归纳用弱化路径与字段名。规则下载稿默认行为不变。 Made-with: Cursor --- backend/pipeline/llm/generate.py | 10 +- backend/pipeline/llm/generate_strategy.py | 62 +++++++- backend/pipeline/reporting/strategy_draft.py | 147 +++++++++++++----- backend/pipeline/tests/test_strategy_draft.py | 45 +++++- 4 files changed, 207 insertions(+), 57 deletions(-) diff --git a/backend/pipeline/llm/generate.py b/backend/pipeline/llm/generate.py index 36abd41..046a7ba 100644 --- a/backend/pipeline/llm/generate.py +++ b/backend/pipeline/llm/generate.py @@ -21,8 +21,6 @@ from .generate_group_summaries import ( PRICE_GROUPS_USER_PREFIX, PROMO_GROUPS_SYSTEM, PROMO_GROUPS_USER_PREFIX, - SCENARIO_GROUPS_SYSTEM, - SCENARIO_GROUPS_USER_PREFIX, _join_chunked_group_markdown, generate_comment_group_summaries_llm, generate_comment_group_summaries_llm_chunked, @@ -32,8 +30,6 @@ from .generate_group_summaries import ( generate_price_group_summaries_llm_chunked, generate_promo_group_summaries_llm, generate_promo_group_summaries_llm_chunked, - generate_scenario_group_summaries_llm, - generate_scenario_group_summaries_llm_chunked, ) from .generate_sections import ( BRIDGE_SECTIONS_SYSTEM, @@ -49,6 +45,7 @@ from .generate_strategy import ( STRATEGY_USER_PREFIX, generate_strategy_draft_markdown_llm, generate_strategy_opportunities_llm, + strategy_decisions_substantive, ) from .llm_client import call_llm as _call_llm @@ -64,8 +61,6 @@ __all__ = [ "PROMO_GROUPS_USER_PREFIX", "REPORT_SYSTEM", "REPORT_USER_PREFIX", - "SCENARIO_GROUPS_SYSTEM", - "SCENARIO_GROUPS_USER_PREFIX", "SENTIMENT_LLM_SYSTEM", "STRATEGY_OPPORTUNITIES_SYSTEM", "STRATEGY_OPPORTUNITIES_USER_PREFIX", @@ -83,10 +78,9 @@ __all__ = [ "generate_price_group_summaries_llm_chunked", "generate_promo_group_summaries_llm", "generate_promo_group_summaries_llm_chunked", - "generate_scenario_group_summaries_llm", - "generate_scenario_group_summaries_llm_chunked", "generate_section_bridges_llm", "generate_strategy_draft_markdown_llm", "generate_strategy_opportunities_llm", "split_competitor_report_for_bridges", + "strategy_decisions_substantive", ] diff --git a/backend/pipeline/llm/generate_strategy.py b/backend/pipeline/llm/generate_strategy.py index 308a14f..d1e9ac7 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -12,6 +12,37 @@ from ..reporting.strategy_draft import ( ) from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window_size +# 与策略生成表单 POST 字段一致:任一则视为业务已提供「实质决策」,否则由模型基于数据推断草案。 +_STRATEGY_DECISION_SUBSTANTIVE_KEYS: tuple[str, ...] = ( + "product_role", + "battlefield_one_line", + "audience_segment", + "time_horizon", + "success_criteria", + "non_goals", + "positioning_choice", + "competitive_stance", + "pillar_product", + "pillar_price", + "pillar_channel", + "pillar_comm", + "marketing_strategy", + "general_strategy", + "competitor_reference", + "resource_notes", +) + + +def strategy_decisions_substantive(strategy_decisions: dict[str, Any] | None) -> bool: + """是否填写了至少一项策略表单文本字段(用于 LLM 是否「自动推断」全稿)。""" + if not isinstance(strategy_decisions, dict): + return False + for k in _STRATEGY_DECISION_SUBSTANTIVE_KEYS: + v = strategy_decisions.get(k) + if isinstance(v, str) and v.strip(): + return True + return False + def _omit_ch8_probe_wordchart_fields(compact: dict[str, Any]) -> None: """ @@ -89,10 +120,21 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要** **落实范围**:上文「全局禁止编造」适用于**摘要、一至十、附录**的每一句话与表格每一格;**不得**因章节不同而放宽。 +**对外成稿与禁止技术泄露(硬性)**: +- 正文须为**可直接对业务或合作方阅读**的正式策略文档(对外前仍须按需脱敏)。**禁止**出现:反引号代码体、JSON 键名、英文字段名、内部数据结构名、源码或仓库路径、类文件名、「任务 ID」「工作台」「规则骨架」等系统痕迹;**禁止**照抄底稿中以 *成稿:*、*回答:*、*占位*、*骨架* 开头的**元说明句**,须改写为正式业务表述。 +- **一级标题**用文书式,例如 `# 「{{keyword}}」市场策略建议书(草案)`,其中 `keyword` 取自本消息 JSON 的同名字段;**勿**使用「草稿」「底稿」「归纳用」等对内用词。 +- **附录**中的采集范围等信息用**中文短句**(如「列表页约采集第 3~10 页」),**禁止** `page_start=` 等键值对或英文键名。 + +**业务决策未填写时的成稿义务(当 JSON 中 `strategy_decisions_substantive` 为 false 时)**: +- 视为未提交表单决策:须基于监测摘要、细类报告节选、底稿数据表及 `business_notes`(若有)**主动推断**一套连贯的**假设性**策略;在「策略范围与前提」写清推断前提(「假设:」「待业务确认:」),对阶段目标类型给出 **A~E 类选项及你从数据中归纳的推荐倾向**(不得只列选项而无立场)。 +- **§2.1 针对痛点要怎么做**须含**多行实质内容**,覆盖监测已支撑的主要细类与痛点,**禁止**整表留白或满篇 *(待填)*。 +- 仍须遵守全局禁止编造:数字、品牌、店铺、用户原话、活动规则仅可来自输入依据;无依据处用假设语气。 + **决策边界(硬性)**: -- **业务已在 `strategy_decisions` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。 -- **表单中为空或占位(如 *待填*、*骨架占位*)的项**:结合 `structured_brief`、节选与数据摘录**补全为可执行表述**;补全须与数据方向一致。**例外**:「**策略范围与前提**」中若仍无依据,**允许**保留「待确认」并**列出** A~E 类目标选项,**禁止**用虚构本品角色或目标补满。 -- **成稿阶段避免**:反复「请业务决策」「待确认后再定」;**不确定时**须在 §2.1 表中用「类目/细类」列 +「待业务定类」「假设:」**分类**写清,**禁止**只写泛化一句带过。 +- **当 `strategy_decisions_substantive` 为 true 时**:业务已在 `strategy_decisions` 中填写的项(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。 +- **当 `strategy_decisions_substantive` 为 true** 而部分表单项仍为空或占位:结合监测摘要与节选**补全为可执行表述**,与数据方向一致。 +- **当 `strategy_decisions_substantive` 为 false 时**:适用上文「业务决策未填写时的成稿义务」,**禁止**以「请先填表」类表述搪塞全篇。 +- **成稿阶段避免**:反复「请业务决策」;不确定时在 §2.1 用「类目/细类」+「假设:」「待业务确认:」**写清**,**禁止**只写泛化一句。 **输出结构与阅读顺序(须与 `rules_draft_markdown` 章节一致,勿另起目录)**: **策略范围与前提(生成前先对齐)** → **摘要** → **一、顾客是谁**(含人群与路径、细类讨论、本品聚焦)→ **二、产品价值与用户痛点**(**仅 §2.1 针对痛点要怎么做** 表,**勿**再设独立「痛点表/价值对表/负向归因」子节)→ **三、为什么要买「这款产品」**(**仅 §3.1 品类与时机**;**无 §3.2**,转化与价带应对已在 §2 表内则**勿重复**)→ **四、为什么要选「这个品牌」** → **五、与其它品牌有何不同** → **六、阶段目标与路径** → **七、品牌四线**(建设·打造·运营·体验)→ **八、战术支柱**(产品/定价/促销/渠道与传播)→ **九、风险、假设与待验证** → **十、下一步与节奏**(含业务备注)→ **附录**。 @@ -105,7 +147,7 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要** - **§2.1 针对痛点要怎么做**(若底稿已有表头)须**填写实质内容**;全稿**动作总锚**为 §2.1。若无表,须在 **§二** 或 **§八** 用等价分条写清「痛点—动作—落地—验证」。 **分节要求(与底稿章节一一对应,勿省略)**: -- **策略范围与前提**:回答「**这份策略是针对什么做的**」(监测任务、本品角色、战场、主推类目、**本阶段目标类型**、时间、成功标准)。与 `strategy_decisions`、`business_notes` 对齐;表格占位未填时**禁止**编造决策,应保留「待确认」并**列出**可选目标类型(见 `STRATEGY_USER_PREFIX` 与规划文档「启动前」);可写「建议选项:…」供业务勾选。**禁止**与后文 §2.1、§六 自相矛盾。 +- **策略范围与前提**:回答「**这份策略是针对什么做的**」(监测任务、本品角色、战场、主推类目、**本阶段目标类型**、时间、成功标准)。与业务表单及备注对齐;`strategy_decisions_substantive` 为 false 时须写清假设前提与推荐目标类型(可含 A~E 选项),为 true 时未填项不得与已填决策矛盾。**禁止**与后文 §2.1、§六 自相矛盾。 - **摘要**:除范围样本外,**阶段重点**须含 1~2 条**可执行动作**,且能回扣 §2 中的优先痛点(非空泛「加强运营」);须**承接**上文「策略范围与前提」已定的边界。 - **一、顾客是谁**:**禁止**重复报告中的细类词频、分品类样本量展开、文本挖掘方法;用 **少量结论句**(谁搜、关心什么)即可,**1.3 本品聚焦**须写清「本期主攻人群/场景」与 **§2.1** 的对应关系。 - **二**:**仅 §2.1** 一张表:「类目/细类(本决策适用)| 用户痛点(简述)| 策略动作 | 具体怎么做 | 如何验证」。须覆盖监测已支撑的主要维度(**按类目分行**,口感/质地分线、分量/规格、信任与价格等,依数据取舍);**类目列 + 痛点简述列**遵守「§2 表」条款。**禁止**再写独立「痛点与证据表」「价值对表」「负向归因」子节(与 §二 重复的内容一律并入本表或删去)。 @@ -138,9 +180,11 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要** **输出**:仅 Markdown 正文(不要 ``` 围栏);须收束各小节与全文,勿中途截断。""" STRATEGY_USER_PREFIX = ( - "请基于以下 JSON 输出最终策略稿(Markdown)。" - "输出前自检:全文不得包含输入 JSON 中未出现的具体数字、品牌/店铺名、用户引语与活动规则;不确定处须写「假设」「输入未体现」或「待核对」。" - "「策略范围与前提」一节:若 `strategy_decisions` 中本品角色、战场、成功标准、主推类目等大量为空,**不得**编造;须列「待确认」并给出**目标类型选项**供业务选择,例如:**A** 上市/首发前验证 **B** 份额追赶 **C** 利润与价盘防守 **D** 新场景/新人群拓展 **E** 其它(简述)——具体措辞见项目规划文档「策略生成-框架确定」之「启动前」。\n\n" + "请基于以下 JSON 输出最终策略稿(Markdown),正文须为对外可读正式文档,不得泄露 JSON 键名、字段名、源码路径或底稿中的编写提示语。\n" + "输出前自检:全文不得包含输入中未出现的具体数字、品牌/店铺名、用户引语与活动规则;不确定处须写「假设」「监测未体现」或「待业务核对」。\n" + "若 JSON 中 `strategy_decisions_substantive` 为 false:你须基于监测摘要与细类报告节选**主动推断**完整策略草案(含 §2.1 多行实质内容)," + "在「策略范围与前提」标明假设前提,并对阶段目标给出 A~E 类型选项及**推荐倾向**;禁止全文停留在待填占位。\n" + "若 `strategy_decisions_substantive` 为 true:已填表单项视为已定须落实;空项结合数据补全,并与后文一致。\n\n" ) @@ -171,6 +215,7 @@ def generate_strategy_draft_markdown_llm( generated_at_iso=generated_at_iso, strategy_decisions=strategy_decisions, report_config=report_config, + for_llm_input=True, ) excerpt_raw = (report_strategy_excerpt or "").strip() group_evidence_raw = (report_matrix_group_evidence_md or "").strip() @@ -208,6 +253,9 @@ def generate_strategy_draft_markdown_llm( "keyword": keyword, "generated_at_iso": generated_at_iso, "strategy_decisions": strategy_decisions, + "strategy_decisions_substantive": strategy_decisions_substantive( + strategy_decisions + ), "business_notes": business_notes, "structured_brief": compact, "rules_draft_markdown": rd, diff --git a/backend/pipeline/reporting/strategy_draft.py b/backend/pipeline/reporting/strategy_draft.py index 48752ab..dc98eaf 100644 --- a/backend/pipeline/reporting/strategy_draft.py +++ b/backend/pipeline/reporting/strategy_draft.py @@ -162,22 +162,37 @@ def build_strategy_draft_markdown( generated_at_iso: str = "", strategy_decisions: dict[str, Any] | None = None, report_config: dict[str, Any] | None = None, + for_llm_input: bool = False, ) -> str: - """生成可下载的 Markdown:与「六主轴 + 品牌四线」示例稿同构的规则骨架,附录为数据速览。""" + """生成可下载的 Markdown:与「六主轴 + 品牌四线」示例稿同构的规则骨架,附录为数据速览。 + + ``for_llm_input=True`` 时供大模型归纳用:弱化源码/路径/任务 ID 等痕迹,减少对外成稿误复述。 + """ use_ch8_probe = report_uses_chapter8_text_mining_probe(report_config) d = strategy_decisions or {} pos = _esc(d.get("positioning_choice") or "").strip() kw = _esc(brief.get("keyword")) or _esc(keyword) or "—" batch = _esc(brief.get("batch_label")) or "—" - lines: list[str] = [ - f"# 市场策略制定草稿 · 「{kw}」", - "", - "> **骨架说明**:本页为规则骨架;结构与 [`docs/demo`](docs/demo) 示例一致。**全局禁止编造**见 `generate_strategy.py` 中 `STRATEGY_DATA_RULES`。", - "", - ] - if generated_at_iso: - lines.append(f"> **生成时间**:{_esc(generated_at_iso)} · **任务 ID**:{job_id}") - lines.append("") + if for_llm_input: + lines: list[str] = [ + f"# 「{kw}」", + "", + ] + if generated_at_iso: + lines.append( + f"> **生成时间**:{_esc(generated_at_iso)}(归纳用,**勿写入对外成稿正文**)" + ) + lines.append("") + else: + lines = [ + f"# 市场策略制定草稿 · 「{kw}」", + "", + "> **骨架说明**:本页为规则骨架;结构与 [`docs/demo`](docs/demo) 示例一致。**全局禁止编造**见 `generate_strategy.py` 中 `STRATEGY_DATA_RULES`。", + "", + ] + if generated_at_iso: + lines.append(f"> **生成时间**:{_esc(generated_at_iso)} · **任务 ID**:{job_id}") + lines.append("") scope = brief.get("scope") or {} merged_n = scope.get("merged_sku_count") @@ -192,13 +207,24 @@ def build_strategy_draft_markdown( def _scope_cell(val: str, placeholder: str) -> str: return val if val else f"*({placeholder})*" + scope_prelude = ( + [ + "*回答:**这份策略是针对什么、在什么边界里做的**——属「立项靶心」,不是执行摘要。表单已填则写成短句;未填保留占位,**勿**编造。*", + "*业务侧在动策略稿之前,应先对齐本节;可选目标类型见 `docs/planning/策略生成-框架确定.md`「启动前:须先明确的项」。*", + "", + ] + if not for_llm_input + else [ + "*监测任务边界与业务角色:表单已填则写成短句;未填时由归纳稿基于监测摘要提出**假设性**表述。*", + "*阶段目标类型可列 A~E 备选(上市验证 / 份额追赶 / 价盘防守 / 新场景或新人群 / 其它),并写明推荐倾向。*", + "", + ] + ) lines.extend( [ "## 策略范围与前提(生成前先对齐)", "", - "*回答:**这份策略是针对什么、在什么边界里做的**——属「立项靶心」,不是执行摘要。表单已填则写成短句;未填保留占位,**勿**编造。*", - "*业务侧在动策略稿之前,应先对齐本节;可选目标类型见 `docs/planning/策略生成-框架确定.md`「启动前:须先明确的项」。*", - "", + *scope_prelude, "| 须明确项 | 填写或待确认 |", "|----------|----------------|", f"| **监测任务(数据同源)** | 关键词「{kw}」;批次 **{batch}**;与同任务《竞品分析报告》一致 |", @@ -296,7 +322,11 @@ def build_strategy_draft_markdown( "", "*本节**仅**用下表写清**针对痛点要怎么做**(**类目** + 痛点简述 + 动作 + 落地 + 验证)。**不再**单设「痛点表 / 价值对表 / 负向归因」子节,避免与 §三、§八重复。*", "", - "*「用户痛点(简述)」须与 `structured_brief` / 策略线索 / 报告节选**可核对**;**禁止**编造「用户反馈『……』」式引语,除非原句已出现在上述输入中。*", + ( + "*「用户痛点(简述)」须与监测摘要 / 策略线索 / 报告节选**可核对**;**禁止**编造「用户反馈『……』」式引语,除非原句已出现在上述输入中。*" + if for_llm_input + else "*「用户痛点(简述)」须与 `structured_brief` / 策略线索 / 报告节选**可核对**;**禁止**编造「用户反馈『……』」式引语,除非原句已出现在上述输入中。*" + ), "", "*「类目/细类」列:写明本行决策**适用于哪一类**(如饼干/面包/全检索池);多细类须**分行**,**禁止**用一句「全站」覆盖彼此冲突的策略;类目未定可写「待业务定类」并附分类假设。*", "", @@ -317,7 +347,9 @@ def build_strategy_draft_markdown( else (list(raw_hints) if isinstance(raw_hints, list) else []) ) if hints: - lines.append("**摘要自动线索(`strategy_hints`)**") + lines.append( + "**监测摘要自动线索**" if for_llm_input else "**摘要自动线索(`strategy_hints`)**" + ) lines.append("") for h in hints: lines.append(f"- {_esc(h)}") @@ -335,7 +367,9 @@ def build_strategy_draft_markdown( raw = brief.get("pc_search_raw") or {} if raw.get("result_count_consensus") is not None: lines.append( - f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}(非销售额)" + f"- **平台申报检索规模**:{_num(raw.get('result_count_consensus'))}(站内匹配条数量级,非销售额)" + if for_llm_input + else f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}(非销售额)" ) elif merged_n is not None: lines.append(f"- **深入样本 SKU 数**:{_num(merged_n)}") @@ -344,9 +378,10 @@ def build_strategy_draft_markdown( lines.append("") if pst.get("n"): src = _esc(brief.get("price_stats_source")) or "—" + src_disp = "本监测样本" if for_llm_input and src == "strategy_scope_matrix_group_skus" else src lines.extend( [ - f"- **价格摘录(可支撑购买理由与锚点)**:来源 {src},n = {_num(pst.get('n'))};" + f"- **价格摘录(可支撑购买理由与锚点)**:来源 {src_disp},n = {_num(pst.get('n'))};" f"区间 {_num(pst.get('min'))}~{_num(pst.get('max'))};中位数 {_num(pst.get('median'))}。", "", ] @@ -517,10 +552,15 @@ 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) + promo_hint = ( + "*促销与活动线索:须与摘要中的价差与活动信号及报告第六章已有归纳一致;无则勿编造具体满减门槛。*" + if for_llm_input + else "*促销与活动线索:须与摘要 `price_promotion_signals` 及第六章/第九章已有归纳一致;无则勿编造具体满减门槛。*" + ) lines.extend( [ "", - "*促销与活动线索:须与摘要 `price_promotion_signals` 及第六章/第九章已有归纳一致;无则勿编造具体满减门槛。*" + promo_hint if has_promo else "*促销与价差:若摘要或价格信号有归纳则承接;无则勿编造。*", "", @@ -544,7 +584,11 @@ def build_strategy_draft_markdown( "### 8.3 促销与活动策略", "", "*须写促销**原则**(券/到手价/跟价节奏);**满减、满折、跨店**等:能引用的写清来源;监测未捕获具体门槛时写「待与运营/后台对齐」,**勿**整节留空,**勿**编造门槛数字。*", - "*与 `price_promotion_signals`、报告第六章一致;勿虚构活动。*", + ( + "*与监测摘要中的促销与价差信号、报告第六章一致;勿虚构活动。*" + if for_llm_input + else "*与 `price_promotion_signals`、报告第六章一致;勿虚构活动。*" + ), "", "### 8.4 渠道与传播", "", @@ -602,27 +646,54 @@ def build_strategy_draft_markdown( "scenario_filter_enabled": "场景筛选", } if isinstance(meta, dict) and meta: - bits = [] - for k in ("page_start", "page_to", "max_skus_config", "scenario_filter_enabled"): - if k in meta: - label = meta_labels.get(k, k) - bits.append(f"{label}={_esc(meta.get(k))}") - if bits: - lines.append(f"- **采集参数快照**:{'; '.join(bits)}") + if for_llm_input: + bits_llm: list[str] = [] + ps, pt = meta.get("page_start"), meta.get("page_to") + if ps is not None and pt is not None: + bits_llm.append(f"列表页约第 {_esc(ps)}~{_esc(pt)} 页") + elif ps is not None: + bits_llm.append(f"列表自第 {_esc(ps)} 页起采集") + if meta.get("max_skus_config") is not None: + bits_llm.append(f"深入样本上限约 {_num(meta.get('max_skus_config'))} 个 SKU") + if meta.get("scenario_filter_enabled"): + bits_llm.append("已启用场景筛选") + if bits_llm: + lines.append(f"- **采集范围**:{';'.join(bits_llm)}") + else: + bits = [] + for k in ("page_start", "page_to", "max_skus_config", "scenario_filter_enabled"): + if k in meta: + label = meta_labels.get(k, k) + bits.append(f"{label}={_esc(meta.get(k))}") + if bits: + lines.append(f"- **采集参数快照**:{'; '.join(bits)}") raw = brief.get("pc_search_raw") or {} if raw.get("result_count_consensus") is not None: lines.append( - f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}" + f"- **平台申报检索规模**:{_num(raw.get('result_count_consensus'))}" + if for_llm_input + else f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}" + ) + if for_llm_input: + lines.extend( + [ + "", + "*可与同任务《竞品分析报告》及本批次数据表对照核验。*", + "", + "---", + "", + ] + ) + else: + lines.extend( + [ + "", + "*同目录含本批次 CSV 与分析产出,可对照使用。*", + "", + "---", + "", + "*本稿由工作台「市场策略制定」生成;与同任务结构化分析数据一致。*", + "", + ] ) - lines.extend( - [ - "", - "*同目录含本批次 CSV 与分析产出,可对照使用。*", - "", - "---", - "", - "*本稿由工作台「市场策略制定」生成;与同任务结构化分析数据一致。*", - "", - ] - ) return "\n".join(lines) diff --git a/backend/pipeline/tests/test_strategy_draft.py b/backend/pipeline/tests/test_strategy_draft.py index ca4de2f..5e4811e 100644 --- a/backend/pipeline/tests/test_strategy_draft.py +++ b/backend/pipeline/tests/test_strategy_draft.py @@ -4,10 +4,45 @@ from __future__ import annotations from django.test import SimpleTestCase from pipeline.llm.generate_strategy import _omit_ch8_probe_wordchart_fields +from pipeline.llm.generate_strategy import strategy_decisions_substantive from pipeline.reporting.strategy_draft import build_strategy_draft_markdown class StrategyDraftTests(SimpleTestCase): + def test_strategy_decisions_substantive(self) -> None: + self.assertFalse(strategy_decisions_substantive(None)) + self.assertFalse(strategy_decisions_substantive({})) + self.assertFalse( + strategy_decisions_substantive( + {"ack_risk_keywords": True, "product_role": " "} + ) + ) + self.assertTrue( + strategy_decisions_substantive({"product_role": "新品"}) + ) + + def test_for_llm_input_omits_dev_traces(self) -> None: + brief = { + "schema_version": 1, + "keyword": "K", + "batch_label": "b1", + "scope": {"merged_sku_count": 2}, + "strategy_hints": ["线索1"], + "meta": {"page_start": 1, "page_to": 3, "max_skus_config": 100}, + } + md = build_strategy_draft_markdown( + job_id=7, + keyword="K", + brief=brief, + generated_at_iso="2026-01-01", + for_llm_input=True, + ) + self.assertNotIn("任务 ID", md) + self.assertNotIn("generate_strategy.py", md) + self.assertNotIn("strategy_hints", md) + self.assertIn("监测摘要自动线索", md) + self.assertIn("列表页约第 1~3 页", md) + def test_build_contains_sections_and_notes(self) -> None: brief = { "schema_version": 1, @@ -65,7 +100,7 @@ class StrategyDraftTests(SimpleTestCase): self.assertIn("侧翼切入", md) self.assertIn("做低糖配方", md) self.assertIn("### 7.1 品牌建设", md) - self.assertIn("- [x] 关注词/场景统计是否以偏概全", md) + self.assertIn("- [x] 评论侧归纳是否以偏概全", md) self.assertIn("- [ ] 价格带是否含大促", md) self.assertIn("- [x] 列表集中度与深入样本品牌是否不一致", md) @@ -92,7 +127,7 @@ class StrategyDraftTests(SimpleTestCase): self.assertNotIn("子串统计命中约 **501**", md) self.assertNotIn("场景「控糖", md) - def test_legacy_report_shows_focus_scenario_bullets(self) -> None: + def test_non_probe_path_no_preset_focus_enumeration(self) -> None: brief = { "schema_version": 1, "keyword": "低GI", @@ -111,8 +146,10 @@ class StrategyDraftTests(SimpleTestCase): brief=brief, report_config={"chapter8_text_mining_probe": False}, ) - self.assertIn("「口感」:子串统计命中约 **501** 次", md) - self.assertIn("场景「控糖/血糖相关」:约 **305** 条", md) + self.assertIn("不再", md) + self.assertIn("预设关注词", md) + self.assertNotIn("子串统计命中约 **501**", md) + self.assertNotIn("场景「控糖", md) def test_shops_unique_sku_basis_rendered(self) -> None: brief = {