feat(strategy): 无表单决策时由大模型推断草案并净化对外成稿约束

新增 strategy_decisions_substantive 与 JSON 载荷字段;STRATEGY_SYSTEM/USER_PREFIX 要求实质成稿、禁止技术泄露;build_strategy_draft_markdown(for_llm_input=True) 供归纳用弱化路径与字段名。规则下载稿默认行为不变。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-21 17:03:55 +08:00
parent 805e9b4de4
commit 4cf73e7602
4 changed files with 207 additions and 57 deletions

View File

@ -21,8 +21,6 @@ from .generate_group_summaries import (
PRICE_GROUPS_USER_PREFIX, PRICE_GROUPS_USER_PREFIX,
PROMO_GROUPS_SYSTEM, PROMO_GROUPS_SYSTEM,
PROMO_GROUPS_USER_PREFIX, PROMO_GROUPS_USER_PREFIX,
SCENARIO_GROUPS_SYSTEM,
SCENARIO_GROUPS_USER_PREFIX,
_join_chunked_group_markdown, _join_chunked_group_markdown,
generate_comment_group_summaries_llm, generate_comment_group_summaries_llm,
generate_comment_group_summaries_llm_chunked, generate_comment_group_summaries_llm_chunked,
@ -32,8 +30,6 @@ from .generate_group_summaries import (
generate_price_group_summaries_llm_chunked, generate_price_group_summaries_llm_chunked,
generate_promo_group_summaries_llm, generate_promo_group_summaries_llm,
generate_promo_group_summaries_llm_chunked, generate_promo_group_summaries_llm_chunked,
generate_scenario_group_summaries_llm,
generate_scenario_group_summaries_llm_chunked,
) )
from .generate_sections import ( from .generate_sections import (
BRIDGE_SECTIONS_SYSTEM, BRIDGE_SECTIONS_SYSTEM,
@ -49,6 +45,7 @@ from .generate_strategy import (
STRATEGY_USER_PREFIX, STRATEGY_USER_PREFIX,
generate_strategy_draft_markdown_llm, generate_strategy_draft_markdown_llm,
generate_strategy_opportunities_llm, generate_strategy_opportunities_llm,
strategy_decisions_substantive,
) )
from .llm_client import call_llm as _call_llm from .llm_client import call_llm as _call_llm
@ -64,8 +61,6 @@ __all__ = [
"PROMO_GROUPS_USER_PREFIX", "PROMO_GROUPS_USER_PREFIX",
"REPORT_SYSTEM", "REPORT_SYSTEM",
"REPORT_USER_PREFIX", "REPORT_USER_PREFIX",
"SCENARIO_GROUPS_SYSTEM",
"SCENARIO_GROUPS_USER_PREFIX",
"SENTIMENT_LLM_SYSTEM", "SENTIMENT_LLM_SYSTEM",
"STRATEGY_OPPORTUNITIES_SYSTEM", "STRATEGY_OPPORTUNITIES_SYSTEM",
"STRATEGY_OPPORTUNITIES_USER_PREFIX", "STRATEGY_OPPORTUNITIES_USER_PREFIX",
@ -83,10 +78,9 @@ __all__ = [
"generate_price_group_summaries_llm_chunked", "generate_price_group_summaries_llm_chunked",
"generate_promo_group_summaries_llm", "generate_promo_group_summaries_llm",
"generate_promo_group_summaries_llm_chunked", "generate_promo_group_summaries_llm_chunked",
"generate_scenario_group_summaries_llm",
"generate_scenario_group_summaries_llm_chunked",
"generate_section_bridges_llm", "generate_section_bridges_llm",
"generate_strategy_draft_markdown_llm", "generate_strategy_draft_markdown_llm",
"generate_strategy_opportunities_llm", "generate_strategy_opportunities_llm",
"split_competitor_report_for_bridges", "split_competitor_report_for_bridges",
"strategy_decisions_substantive",
] ]

View File

@ -12,6 +12,37 @@ from ..reporting.strategy_draft import (
) )
from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window_size 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: 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` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。 - **当 `strategy_decisions_substantive` 为 true 时**:业务已在 `strategy_decisions` 中填写的项(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。
- **表单中为空或占位(如 *待填*、*骨架占位*)的项**:结合 `structured_brief`、节选与数据摘录**补全为可执行表述**;补全须与数据方向一致。**例外**:「**策略范围与前提**」中若仍无依据,**允许**保留「待确认」并**列出** A~E 类目标选项,**禁止**用虚构本品角色或目标补满。 - **当 `strategy_decisions_substantive` 为 true** 而部分表单项仍为空或占位:结合监测摘要与节选**补全为可执行表述**,与数据方向一致。
- **成稿阶段避免**:反复「请业务决策」「待确认后再定」;**不确定时**须在 §2.1 表中用「类目/细类」列 +「待业务定类」「假设:」**分类**写清,**禁止**只写泛化一句带过。 - **当 `strategy_decisions_substantive` 为 false 时**:适用上文「业务决策未填写时的成稿义务」,**禁止**以「请先填表」类表述搪塞全篇。
- **成稿阶段避免**:反复「请业务决策」;不确定时在 §2.1 用「类目/细类」+「假设:」「待业务确认:」**写清**,**禁止**只写泛化一句。
**输出结构与阅读顺序(须与 `rules_draft_markdown` 章节一致,勿另起目录)**: **输出结构与阅读顺序(须与 `rules_draft_markdown` 章节一致,勿另起目录)**:
**策略范围与前提(生成前先对齐)** → **摘要** → **一、顾客是谁**(含人群与路径、细类讨论、本品聚焦)→ **二、产品价值与用户痛点**(**仅 §2.1 针对痛点要怎么做** 表,**勿**再设独立「痛点表/价值对表/负向归因」子节)→ **三、为什么要买「这款产品」**(**仅 §3.1 品类与时机**;**无 §3.2**,转化与价带应对已在 §2 表内则**勿重复**)→ **四、为什么要选「这个品牌」** → **五、与其它品牌有何不同** → **六、阶段目标与路径** → **七、品牌四线**(建设·打造·运营·体验)→ **八、战术支柱**(产品/定价/促销/渠道与传播)→ **九、风险、假设与待验证** → **十、下一步与节奏**(含业务备注)→ **附录**。 **策略范围与前提(生成前先对齐)** → **摘要** → **一、顾客是谁**(含人群与路径、细类讨论、本品聚焦)→ **二、产品价值与用户痛点**(**仅 §2.1 针对痛点要怎么做** 表,**勿**再设独立「痛点表/价值对表/负向归因」子节)→ **三、为什么要买「这款产品」**(**仅 §3.1 品类与时机**;**无 §3.2**,转化与价带应对已在 §2 表内则**勿重复**)→ **四、为什么要选「这个品牌」** → **五、与其它品牌有何不同** → **六、阶段目标与路径** → **七、品牌四线**(建设·打造·运营·体验)→ **八、战术支柱**(产品/定价/促销/渠道与传播)→ **九、风险、假设与待验证** → **十、下一步与节奏**(含业务备注)→ **附录**。
@ -105,7 +147,7 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**
- **§2.1 针对痛点要怎么做**(若底稿已有表头)须**填写实质内容**;全稿**动作总锚**为 §2.1。若无表,须在 **§二** 或 **§八** 用等价分条写清「痛点—动作—落地—验证」。 - **§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~2 条**可执行动作**,且能回扣 §2 中的优先痛点(非空泛「加强运营」);须**承接**上文「策略范围与前提」已定的边界。
- **一、顾客是谁**:**禁止**重复报告中的细类词频、分品类样本量展开、文本挖掘方法;用 **少量结论句**(谁搜、关心什么)即可,**1.3 本品聚焦**须写清「本期主攻人群/场景」与 **§2.1** 的对应关系。 - **一、顾客是谁**:**禁止**重复报告中的细类词频、分品类样本量展开、文本挖掘方法;用 **少量结论句**(谁搜、关心什么)即可,**1.3 本品聚焦**须写清「本期主攻人群/场景」与 **§2.1** 的对应关系。
- **二**:**仅 §2.1** 一张表:「类目/细类(本决策适用)| 用户痛点(简述)| 策略动作 | 具体怎么做 | 如何验证」。须覆盖监测已支撑的主要维度(**按类目分行**,口感/质地分线、分量/规格、信任与价格等,依数据取舍);**类目列 + 痛点简述列**遵守「§2 表」条款。**禁止**再写独立「痛点与证据表」「价值对表」「负向归因」子节(与 §二 重复的内容一律并入本表或删去)。 - **二**:**仅 §2.1** 一张表:「类目/细类(本决策适用)| 用户痛点(简述)| 策略动作 | 具体怎么做 | 如何验证」。须覆盖监测已支撑的主要维度(**按类目分行**,口感/质地分线、分量/规格、信任与价格等,依数据取舍);**类目列 + 痛点简述列**遵守「§2 表」条款。**禁止**再写独立「痛点与证据表」「价值对表」「负向归因」子节(与 §二 重复的内容一律并入本表或删去)。
@ -138,9 +180,11 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**
**输出**:仅 Markdown 正文(不要 ``` 围栏);须收束各小节与全文,勿中途截断。""" **输出**:仅 Markdown 正文(不要 ``` 围栏);须收束各小节与全文,勿中途截断。"""
STRATEGY_USER_PREFIX = ( STRATEGY_USER_PREFIX = (
"请基于以下 JSON 输出最终策略稿(Markdown)。" "请基于以下 JSON 输出最终策略稿(Markdown),正文须为对外可读正式文档,不得泄露 JSON 键名、字段名、源码路径或底稿中的编写提示语。\n"
"输出前自检:全文不得包含输入 JSON 中未出现的具体数字、品牌/店铺名、用户引语与活动规则;不确定处须写「假设」「输入未体现」或「待核对」。" "输出前自检:全文不得包含输入中未出现的具体数字、品牌/店铺名、用户引语与活动规则;不确定处须写「假设」「监测未体现」或「待业务核对」。\n"
"「策略范围与前提」一节:若 `strategy_decisions` 中本品角色、战场、成功标准、主推类目等大量为空,**不得**编造;须列「待确认」并给出**目标类型选项**供业务选择,例如:**A** 上市/首发前验证 **B** 份额追赶 **C** 利润与价盘防守 **D** 新场景/新人群拓展 **E** 其它(简述)——具体措辞见项目规划文档「策略生成-框架确定」之「启动前」。\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, generated_at_iso=generated_at_iso,
strategy_decisions=strategy_decisions, strategy_decisions=strategy_decisions,
report_config=report_config, report_config=report_config,
for_llm_input=True,
) )
excerpt_raw = (report_strategy_excerpt or "").strip() excerpt_raw = (report_strategy_excerpt or "").strip()
group_evidence_raw = (report_matrix_group_evidence_md or "").strip() group_evidence_raw = (report_matrix_group_evidence_md or "").strip()
@ -208,6 +253,9 @@ def generate_strategy_draft_markdown_llm(
"keyword": keyword, "keyword": keyword,
"generated_at_iso": generated_at_iso, "generated_at_iso": generated_at_iso,
"strategy_decisions": strategy_decisions, "strategy_decisions": strategy_decisions,
"strategy_decisions_substantive": strategy_decisions_substantive(
strategy_decisions
),
"business_notes": business_notes, "business_notes": business_notes,
"structured_brief": compact, "structured_brief": compact,
"rules_draft_markdown": rd, "rules_draft_markdown": rd,

View File

@ -162,14 +162,29 @@ def build_strategy_draft_markdown(
generated_at_iso: str = "", generated_at_iso: str = "",
strategy_decisions: dict[str, Any] | None = None, strategy_decisions: dict[str, Any] | None = None,
report_config: dict[str, Any] | None = None, report_config: dict[str, Any] | None = None,
for_llm_input: bool = False,
) -> str: ) -> str:
"""生成可下载的 Markdown:与「六主轴 + 品牌四线」示例稿同构的规则骨架,附录为数据速览。""" """生成可下载的 Markdown:与「六主轴 + 品牌四线」示例稿同构的规则骨架,附录为数据速览。
``for_llm_input=True`` 时供大模型归纳用:弱化源码/路径/任务 ID 等痕迹,减少对外成稿误复述。
"""
use_ch8_probe = report_uses_chapter8_text_mining_probe(report_config) use_ch8_probe = report_uses_chapter8_text_mining_probe(report_config)
d = strategy_decisions or {} d = strategy_decisions or {}
pos = _esc(d.get("positioning_choice") or "").strip() pos = _esc(d.get("positioning_choice") or "").strip()
kw = _esc(brief.get("keyword")) or _esc(keyword) or "—" kw = _esc(brief.get("keyword")) or _esc(keyword) or "—"
batch = _esc(brief.get("batch_label")) or "—" batch = _esc(brief.get("batch_label")) or "—"
if for_llm_input:
lines: list[str] = [ lines: list[str] = [
f"# 「{kw}」",
"",
]
if generated_at_iso:
lines.append(
f"> **生成时间**:{_esc(generated_at_iso)}(归纳用,**勿写入对外成稿正文**)"
)
lines.append("")
else:
lines = [
f"# 市场策略制定草稿 · 「{kw}」", f"# 市场策略制定草稿 · 「{kw}」",
"", "",
"> **骨架说明**:本页为规则骨架;结构与 [`docs/demo`](docs/demo) 示例一致。**全局禁止编造**见 `generate_strategy.py` 中 `STRATEGY_DATA_RULES`。", "> **骨架说明**:本页为规则骨架;结构与 [`docs/demo`](docs/demo) 示例一致。**全局禁止编造**见 `generate_strategy.py` 中 `STRATEGY_DATA_RULES`。",
@ -192,13 +207,24 @@ def build_strategy_draft_markdown(
def _scope_cell(val: str, placeholder: str) -> str: def _scope_cell(val: str, placeholder: str) -> str:
return val if val else f"*({placeholder})*" return val if val else f"*({placeholder})*"
scope_prelude = (
[
"*回答:**这份策略是针对什么、在什么边界里做的**——属「立项靶心」,不是执行摘要。表单已填则写成短句;未填保留占位,**勿**编造。*",
"*业务侧在动策略稿之前,应先对齐本节;可选目标类型见 `docs/planning/策略生成-框架确定.md`「启动前:须先明确的项」。*",
"",
]
if not for_llm_input
else [
"*监测任务边界与业务角色:表单已填则写成短句;未填时由归纳稿基于监测摘要提出**假设性**表述。*",
"*阶段目标类型可列 A~E 备选(上市验证 / 份额追赶 / 价盘防守 / 新场景或新人群 / 其它),并写明推荐倾向。*",
"",
]
)
lines.extend( lines.extend(
[ [
"## 策略范围与前提(生成前先对齐)", "## 策略范围与前提(生成前先对齐)",
"", "",
"*回答:**这份策略是针对什么、在什么边界里做的**——属「立项靶心」,不是执行摘要。表单已填则写成短句;未填保留占位,**勿**编造。*", *scope_prelude,
"*业务侧在动策略稿之前,应先对齐本节;可选目标类型见 `docs/planning/策略生成-框架确定.md`「启动前:须先明确的项」。*",
"",
"| 须明确项 | 填写或待确认 |", "| 须明确项 | 填写或待确认 |",
"|----------|----------------|", "|----------|----------------|",
f"| **监测任务(数据同源)** | 关键词「{kw}」;批次 **{batch}**;与同任务《竞品分析报告》一致 |", 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 []) else (list(raw_hints) if isinstance(raw_hints, list) else [])
) )
if hints: if hints:
lines.append("**摘要自动线索(`strategy_hints`)**") lines.append(
"**监测摘要自动线索**" if for_llm_input else "**摘要自动线索(`strategy_hints`)**"
)
lines.append("") lines.append("")
for h in hints: for h in hints:
lines.append(f"- {_esc(h)}") lines.append(f"- {_esc(h)}")
@ -335,7 +367,9 @@ def build_strategy_draft_markdown(
raw = brief.get("pc_search_raw") or {} raw = brief.get("pc_search_raw") or {}
if raw.get("result_count_consensus") is not None: if raw.get("result_count_consensus") is not None:
lines.append( 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: elif merged_n is not None:
lines.append(f"- **深入样本 SKU 数**:{_num(merged_n)}") lines.append(f"- **深入样本 SKU 数**:{_num(merged_n)}")
@ -344,9 +378,10 @@ def build_strategy_draft_markdown(
lines.append("") lines.append("")
if pst.get("n"): if pst.get("n"):
src = _esc(brief.get("price_stats_source")) or "—" 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( 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'))}。", 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: if use_ch8_probe:
pst_sig = brief.get("price_promotion_signals") or {} pst_sig = brief.get("price_promotion_signals") or {}
has_promo = isinstance(pst_sig, dict) and bool(pst_sig) has_promo = isinstance(pst_sig, dict) and bool(pst_sig)
promo_hint = (
"*促销与活动线索:须与摘要中的价差与活动信号及报告第六章已有归纳一致;无则勿编造具体满减门槛。*"
if for_llm_input
else "*促销与活动线索:须与摘要 `price_promotion_signals` 及第六章/第九章已有归纳一致;无则勿编造具体满减门槛。*"
)
lines.extend( lines.extend(
[ [
"", "",
"*促销与活动线索:须与摘要 `price_promotion_signals` 及第六章/第九章已有归纳一致;无则勿编造具体满减门槛。*" promo_hint
if has_promo if has_promo
else "*促销与价差:若摘要或价格信号有归纳则承接;无则勿编造。*", else "*促销与价差:若摘要或价格信号有归纳则承接;无则勿编造。*",
"", "",
@ -544,7 +584,11 @@ def build_strategy_draft_markdown(
"### 8.3 促销与活动策略", "### 8.3 促销与活动策略",
"", "",
"*须写促销**原则**(券/到手价/跟价节奏);**满减、满折、跨店**等:能引用的写清来源;监测未捕获具体门槛时写「待与运营/后台对齐」,**勿**整节留空,**勿**编造门槛数字。*", "*须写促销**原则**(券/到手价/跟价节奏);**满减、满折、跨店**等:能引用的写清来源;监测未捕获具体门槛时写「待与运营/后台对齐」,**勿**整节留空,**勿**编造门槛数字。*",
"*与 `price_promotion_signals`、报告第六章一致;勿虚构活动。*", (
"*与监测摘要中的促销与价差信号、报告第六章一致;勿虚构活动。*"
if for_llm_input
else "*与 `price_promotion_signals`、报告第六章一致;勿虚构活动。*"
),
"", "",
"### 8.4 渠道与传播", "### 8.4 渠道与传播",
"", "",
@ -602,6 +646,20 @@ def build_strategy_draft_markdown(
"scenario_filter_enabled": "场景筛选", "scenario_filter_enabled": "场景筛选",
} }
if isinstance(meta, dict) and meta: if isinstance(meta, dict) and meta:
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 = [] bits = []
for k in ("page_start", "page_to", "max_skus_config", "scenario_filter_enabled"): for k in ("page_start", "page_to", "max_skus_config", "scenario_filter_enabled"):
if k in meta: if k in meta:
@ -612,8 +670,21 @@ def build_strategy_draft_markdown(
raw = brief.get("pc_search_raw") or {} raw = brief.get("pc_search_raw") or {}
if raw.get("result_count_consensus") is not None: if raw.get("result_count_consensus") is not None:
lines.append( 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( lines.extend(
[ [
"", "",

View File

@ -4,10 +4,45 @@ from __future__ import annotations
from django.test import SimpleTestCase from django.test import SimpleTestCase
from pipeline.llm.generate_strategy import _omit_ch8_probe_wordchart_fields 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 from pipeline.reporting.strategy_draft import build_strategy_draft_markdown
class StrategyDraftTests(SimpleTestCase): 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: def test_build_contains_sections_and_notes(self) -> None:
brief = { brief = {
"schema_version": 1, "schema_version": 1,
@ -65,7 +100,7 @@ class StrategyDraftTests(SimpleTestCase):
self.assertIn("侧翼切入", md) self.assertIn("侧翼切入", md)
self.assertIn("做低糖配方", md) self.assertIn("做低糖配方", md)
self.assertIn("### 7.1 品牌建设", md) self.assertIn("### 7.1 品牌建设", md)
self.assertIn("- [x] 关注词/场景统计是否以偏概全", md) self.assertIn("- [x] 评论侧归纳是否以偏概全", md)
self.assertIn("- [ ] 价格带是否含大促", md) self.assertIn("- [ ] 价格带是否含大促", md)
self.assertIn("- [x] 列表集中度与深入样本品牌是否不一致", md) self.assertIn("- [x] 列表集中度与深入样本品牌是否不一致", md)
@ -92,7 +127,7 @@ class StrategyDraftTests(SimpleTestCase):
self.assertNotIn("子串统计命中约 **501**", md) self.assertNotIn("子串统计命中约 **501**", md)
self.assertNotIn("场景「控糖", 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 = { brief = {
"schema_version": 1, "schema_version": 1,
"keyword": "低GI", "keyword": "低GI",
@ -111,8 +146,10 @@ class StrategyDraftTests(SimpleTestCase):
brief=brief, brief=brief,
report_config={"chapter8_text_mining_probe": False}, report_config={"chapter8_text_mining_probe": False},
) )
self.assertIn("「口感」:子串统计命中约 **501** 次", md) self.assertIn("不再", md)
self.assertIn("场景「控糖/血糖相关」:约 **305** 条", md) self.assertIn("预设关注词", md)
self.assertNotIn("子串统计命中约 **501**", md)
self.assertNotIn("场景「控糖", md)
def test_shops_unique_sku_basis_rendered(self) -> None: def test_shops_unique_sku_basis_rendered(self) -> None:
brief = { brief = {