mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-24 17:21:40 +08:00
feat(pipeline): 策略骨架缩短并强化策略生成决策边界
- 规则稿:章节重排为短骨架,去重复假设套话;探针模式三条占位,传统模式改为统计摘录句式。 - LLM:STRATEGY_SYSTEM 明确表单已定项不可反写,空项由数据补全,成稿禁止二次决策话术。 - API/前端:新增目标客群、主要对标、资源备注字段并传入 strategy_decisions。 - OpenAPI 与单测同步。 Made-with: Cursor
This commit is contained in:
parent
932c3d6aeb
commit
16d64565a3
@ -12,17 +12,24 @@ 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
|
||||||
|
|
||||||
STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要**与业务侧填写的**决策字段**,把「规则底稿」润色为可读的策略 Markdown。
|
STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要**与业务侧填写的**决策字段**,把「规则底稿」写成**短、可执行**的策略 Markdown 成稿。
|
||||||
|
|
||||||
**规则**:
|
**输入**:`rules_draft_markdown`(规则骨架,与同任务数据一致)、`structured_brief`(摘要子集)、`strategy_decisions`、`business_notes`;可选 `report_strategy_excerpt`(与同任务宿主报告**第九章**「策略与机会」正文同源)。
|
||||||
- 输入 JSON 含 `rules_draft_markdown`(规则引擎生成的底稿,与同任务数据一致)、`structured_brief`(摘要子集)、`strategy_decisions`、`business_notes` 等;可选 `report_strategy_excerpt`(与同任务宿主报告**第九章**「策略与机会」正文同源,来自已落盘产物)。
|
|
||||||
- **与报告第九章对齐(硬性,当 `report_strategy_excerpt` 非空时)**:润色后的全文在**战略方向与主要判断**上须与该节选**一致**,不得写出与之**明显矛盾**的结论(如定价档位、差异化主线、主要风险判断);若 `business_notes` 或 `strategy_decisions` 与节选冲突,须在正文适当位置简短说明「与报告第九章策略归纳不一致之处见业务备注」类表述,且**不得**把节选与 `structured_brief` 中均未出现的数字当作既定事实。
|
**决策边界(硬性)**:
|
||||||
- **当 `report_strategy_excerpt` 为空或仅为占位说明**:表示报告侧未生成或未重跑第九章大模型正文,仅依据 `rules_draft_markdown` 与 `structured_brief` 润色,**不得编造**与可能存在的报告第九章结论。
|
- **业务已在 `strategy_decisions` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。
|
||||||
- **不得编造**输入中不存在的销量、占比、价格数字;若底稿与摘要中有数字,须保持一致;表述集中度时用「第一大……份额」「前三家合计」等中文,**不要用** CR1、CR3 等缩写;
|
- **表单中为空或占位(如 *待填*、*骨架占位*)的项**:由你结合 `structured_brief`、`report_strategy_excerpt`(若非空)与数据摘录**补全为可执行表述**;补全须与数据方向一致,**不得**编造输入中不存在的销量、GMV、未给出的价格或占比。
|
||||||
- 若 `structured_brief` 含 `matrix_overview_for_llm` 或矩阵相关字段,策略中应**呼应**细分类目分组与竞品矩阵结论,不得无故删光矩阵相关建议;
|
- **成稿阶段禁止**:再写一轮「请业务决策」「待确认后再定」「假设:待验证」等**二次决策话术**;不确定性用一句带过即可(如「需下周用原评论抽样核对」),勿重复堆砌。
|
||||||
- 可调整段落衔接、标题层级、列表与表格呈现,使更易读;可补充「建议」「待业务确认」类表述,但不虚构竞品名称或数据;
|
|
||||||
- **仅输出** Markdown 正文(不要 ``` 围栏包裹全文);
|
**与报告第九章对齐(当 `report_strategy_excerpt` 非空时)**:战略方向与主要判断须与该节选**一致**,不得明显矛盾;若 `business_notes` 或表单与节选冲突,正文中简短点明「与报告第九章归纳差异见业务备注」,且**不得**把节选与 `structured_brief` 均未出现的数字当作事实。
|
||||||
- **输出完整性**:须收束各小节与全文,勿在句子或列表中途戛然而止。"""
|
|
||||||
|
**当 `report_strategy_excerpt` 为空**:仅依据底稿与摘要润色,**不得编造**报告第九章结论。
|
||||||
|
|
||||||
|
**数据**:不得编造销量、占比、价格数字;底稿与摘要中的数字须保持一致;集中度用「第一大……份额」「前三家合计」等中文,**不要用** CR1、CR3。
|
||||||
|
|
||||||
|
**矩阵**:若 `structured_brief` 含矩阵相关字段,须**呼应**细分类目与竞品矩阵结论,不得无故删光。
|
||||||
|
|
||||||
|
**输出**:仅 Markdown 正文(不要 ``` 围栏);须收束各小节与全文,勿中途截断。"""
|
||||||
|
|
||||||
STRATEGY_USER_PREFIX = """请基于以下 JSON 输出最终策略稿(Markdown)。\n\n"""
|
STRATEGY_USER_PREFIX = """请基于以下 JSON 输出最终策略稿(Markdown)。\n\n"""
|
||||||
|
|
||||||
@ -93,8 +100,8 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
}
|
}
|
||||||
if report_uses_chapter8_text_mining_probe(report_config):
|
if report_uses_chapter8_text_mining_probe(report_config):
|
||||||
payload["structured_brief_omission_note"] = (
|
payload["structured_brief_omission_note"] = (
|
||||||
"已启用第八章文本挖掘:本 JSON 中的 structured_brief 已省略关注词/场景子串计数字段,"
|
"已启用第八章文本挖掘:structured_brief 已省略关注词/场景子串计数字段,避免与报告正文口径冲突。"
|
||||||
"避免与当前报告正文口径冲突;请依规则底稿第五节说明与报告 §8 自拟假设,勿逐条编造子串命中列表。"
|
"请依报告 §8 与摘要其他字段,将底稿第五节写成具体执行要点(需求焦点、场景、传播),勿逐条编造子串命中列表。"
|
||||||
)
|
)
|
||||||
raw = json.dumps(payload, ensure_ascii=False)
|
raw = json.dumps(payload, ensure_ascii=False)
|
||||||
if len(raw) > 500_000:
|
if len(raw) > 500_000:
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
市场策略 Markdown 草稿:侧重**策略制定框架**(目标、战场、定位、支柱、行动),
|
市场策略 Markdown 草稿:**规则骨架**(占位 + 少量数据摘录),供业务与大模型成稿对齐。
|
||||||
基于同任务结构化摘要与可选业务备注规则生成;附录为关键数据速览。
|
|
||||||
|
|
||||||
后续可接 LLM 润色;当前无模型调用,便于验收与追溯。
|
- 决策在「策略生成」表单完成;未填项由大模型结合摘要与报告节选补全。
|
||||||
|
- 骨架刻意短、可执行;避免与成稿重复的「假设 / 待验证」套话。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ def build_strategy_draft_markdown(
|
|||||||
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,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成可下载的 Markdown:策略框架为主,附录为数据速览。"""
|
"""生成可下载的 Markdown:策略骨架为主,附录为数据速览。"""
|
||||||
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()
|
||||||
@ -129,7 +129,8 @@ def build_strategy_draft_markdown(
|
|||||||
lines: list[str] = [
|
lines: list[str] = [
|
||||||
f"# 市场策略制定草稿 · 「{kw}」",
|
f"# 市场策略制定草稿 · 「{kw}」",
|
||||||
"",
|
"",
|
||||||
"> 本稿用于**辅助制定市场策略**;由规则根据本批次结构化摘要与业务备注生成,**非大模型自由发挥**,定稿前请业务修订。",
|
"> **骨架说明**:本页为**规则骨架**(占位与少量摘录)。勾选大模型生成时,在骨架与数据上写**短、可执行**成稿。"
|
||||||
|
"**决策在策略生成表单完成**;未填项由模型结合本任务摘要与报告节选补全,**成稿不再写「请再选 / 请决策」式套话**。",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
if generated_at_iso:
|
if generated_at_iso:
|
||||||
@ -140,7 +141,7 @@ def build_strategy_draft_markdown(
|
|||||||
[
|
[
|
||||||
"---",
|
"---",
|
||||||
"",
|
"",
|
||||||
"## 一、战略背景与目标(请业务补全)",
|
"## 一、目标与边界",
|
||||||
"",
|
"",
|
||||||
_goal_bullet("本品角色", str(d.get("product_role") or ""), "新品 / 追赶 / 防守 / 拓品类 …"),
|
_goal_bullet("本品角色", str(d.get("product_role") or ""), "新品 / 追赶 / 防守 / 拓品类 …"),
|
||||||
_goal_bullet("时间范围", str(d.get("time_horizon") or ""), "如:本季度 / 未来 12 周"),
|
_goal_bullet("时间范围", str(d.get("time_horizon") or ""), "如:本季度 / 未来 12 周"),
|
||||||
@ -149,7 +150,22 @@ def build_strategy_draft_markdown(
|
|||||||
str(d.get("success_criteria") or ""),
|
str(d.get("success_criteria") or ""),
|
||||||
"如:搜索位次、转化率、声量、复购 …",
|
"如:搜索位次、转化率、声量、复购 …",
|
||||||
),
|
),
|
||||||
_goal_bullet("非目标(明确不做什么)", str(d.get("non_goals") or ""), "可选"),
|
_goal_bullet("非目标", str(d.get("non_goals") or ""), "明确不做什么(可选)"),
|
||||||
|
_goal_bullet(
|
||||||
|
"目标客群",
|
||||||
|
str(d.get("audience_segment") or ""),
|
||||||
|
"一句话:为谁、什么场景(可选)",
|
||||||
|
),
|
||||||
|
_goal_bullet(
|
||||||
|
"主要对标",
|
||||||
|
str(d.get("competitor_reference") or ""),
|
||||||
|
"品牌或价位带参照(可选)",
|
||||||
|
),
|
||||||
|
_goal_bullet(
|
||||||
|
"资源与预算备注",
|
||||||
|
str(d.get("resource_notes") or ""),
|
||||||
|
"人力、投放、产能等量级(可选)",
|
||||||
|
),
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -159,7 +175,7 @@ def build_strategy_draft_markdown(
|
|||||||
comm_n = scope.get("comment_flat_rows")
|
comm_n = scope.get("comment_flat_rows")
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 二、战场界定(监测语境)",
|
"## 二、战场与样本",
|
||||||
"",
|
"",
|
||||||
f"- **监测关键词 / 货架语境**:{kw}",
|
f"- **监测关键词 / 货架语境**:{kw}",
|
||||||
f"- **批次**:{_esc(brief.get('batch_label')) or '—'}",
|
f"- **批次**:{_esc(brief.get('batch_label')) or '—'}",
|
||||||
@ -167,22 +183,21 @@ def build_strategy_draft_markdown(
|
|||||||
)
|
)
|
||||||
if merged_n is not None or comm_n is not None:
|
if merged_n is not None or comm_n is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
f"- **深入样本规模**:深入 SKU ≈ {_num(merged_n)};评价扁平条数 ≈ {_num(comm_n)}。"
|
f"- **深入样本**:深入 SKU ≈ {_num(merged_n)};评价扁平条数 ≈ {_num(comm_n)}。"
|
||||||
"*策略含义:样本越大,以下「假设」越需抽样复核原评论。*"
|
|
||||||
)
|
)
|
||||||
bf = _esc(d.get("battlefield_one_line") or "").strip()
|
bf = _esc(d.get("battlefield_one_line") or "").strip()
|
||||||
if bf:
|
if bf:
|
||||||
lines.append(f"- **一句话战场**:{bf}")
|
lines.append(f"- **一句话战场**:{bf}")
|
||||||
else:
|
else:
|
||||||
lines.append(
|
lines.append(
|
||||||
"- **一句话战场**:*(请用业务语言写:我们在哪个需求场景、与谁抢同一批用户?)*"
|
"- **一句话战场**:*(在哪个需求场景、与谁抢同一批用户?)*"
|
||||||
)
|
)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
conc = brief.get("concentration") or {}
|
conc = brief.get("concentration") or {}
|
||||||
shops = conc.get("shops_from_list") or {}
|
shops = conc.get("shops_from_list") or {}
|
||||||
dbrand = conc.get("detail_brand_among_merged") or {}
|
dbrand = conc.get("detail_brand_among_merged") or {}
|
||||||
lines.extend(["## 三、竞争格局 → 策略含义", ""])
|
lines.extend(["## 三、竞争结构(摘录)", ""])
|
||||||
n_shop = _cr_narrative(
|
n_shop = _cr_narrative(
|
||||||
"列表侧店铺集中度",
|
"列表侧店铺集中度",
|
||||||
concentration_first_share(shops),
|
concentration_first_share(shops),
|
||||||
@ -200,23 +215,20 @@ def build_strategy_draft_markdown(
|
|||||||
if n_brand:
|
if n_brand:
|
||||||
lines.append(n_brand)
|
lines.append(n_brand)
|
||||||
if not n_shop and not n_brand:
|
if not n_shop and not n_brand:
|
||||||
lines.append("*本摘要未含集中度指标,请结合本批次竞争结构数据补全后再写判断。*")
|
lines.append("*本摘要未含集中度指标,请结合本批次竞争结构数据补全。*")
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"**可下判断的提问(自测)**",
|
"- **自测**:头部已占稳心智时,侧翼还是正面替代?分散时是否用细分场景或内容教育切入?",
|
||||||
"",
|
|
||||||
"- 若头部已占稳心智,本品是**侧翼**还是**正面替代**?",
|
|
||||||
"- 店铺/品牌分散时,是否适合用**细分场景**或**内容教育**切入?",
|
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
stance = _esc(d.get("competitive_stance") or "").strip()
|
stance = _esc(d.get("competitive_stance") or "").strip()
|
||||||
stance_line = {
|
stance_line = {
|
||||||
"flank": "- **本品倾向**:倾向**侧翼切入**,避免与头部正面硬碰。",
|
"flank": "- **本品倾向**:侧翼切入,避免与头部正面硬碰。",
|
||||||
"head_on": "- **本品倾向**:倾向**正面替代**,对标头部主战场。",
|
"head_on": "- **本品倾向**:正面替代,对标头部主战场。",
|
||||||
"both": "- **本品倾向**:计划**分层推进**(部分场景侧翼、部分场景正面)。",
|
"both": "- **本品倾向**:分层推进(部分场景侧翼、部分场景正面)。",
|
||||||
"undecided": "- **本品倾向**:**尚未拍板**,需在会议中对齐后再定主战场叙事。",
|
"undecided": "- **本品倾向**:*(表单未选;成稿时据数据写清倾向)*",
|
||||||
}.get(stance)
|
}.get(stance)
|
||||||
if stance_line:
|
if stance_line:
|
||||||
lines.append(stance_line)
|
lines.append(stance_line)
|
||||||
@ -224,16 +236,15 @@ def build_strategy_draft_markdown(
|
|||||||
|
|
||||||
mix = brief.get("category_mix_top") or []
|
mix = brief.get("category_mix_top") or []
|
||||||
if mix:
|
if mix:
|
||||||
lines.append("### 类目结构提示(Top)")
|
lines.append("### 类目结构(Top)")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("*以下仅作「货架长什么样」的速记。*")
|
|
||||||
for item in mix[:6]:
|
for item in mix[:6]:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
lines.append(f"- {_esc(item.get('label'))}:{_num(item.get('count'))}")
|
lines.append(f"- {_esc(item.get('label'))}:{_num(item.get('count'))}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
pst = brief.get("price_stats") or {}
|
pst = brief.get("price_stats") or {}
|
||||||
lines.extend(["## 四、价格带与定位选项(启发式)", ""])
|
lines.extend(["## 四、价格带与定位(业务已选)", ""])
|
||||||
if pst.get("n"):
|
if pst.get("n"):
|
||||||
src = _esc(brief.get("price_stats_source")) or "—"
|
src = _esc(brief.get("price_stats_source")) or "—"
|
||||||
lines.extend(
|
lines.extend(
|
||||||
@ -241,46 +252,46 @@ def build_strategy_draft_markdown(
|
|||||||
f"- **价格来源**:{src},有效价样本 n = {_num(pst.get('n'))}。",
|
f"- **价格来源**:{src},有效价样本 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'))}。",
|
||||||
"",
|
"",
|
||||||
"**定位选项(请勾一条或改写,并写明理由)**",
|
"**主定位(与表单一致)**",
|
||||||
"",
|
"",
|
||||||
f"- {_pos_mark(pos, 'top')} **贴顶**:对标中高位或头部价位带,强调品质/成分/背书。",
|
f"- {_pos_mark(pos, 'top')} **贴顶**:中高位或头部价位带,强调品质/成分/背书。",
|
||||||
f"- {_pos_mark(pos, 'mid')} **卡腰**:围绕中位数一带,强调性价比与场景匹配。",
|
f"- {_pos_mark(pos, 'mid')} **卡腰**:围绕中位数一带,强调性价比与场景匹配。",
|
||||||
f"- {_pos_mark(pos, 'entry')} **下探**:贴近区间下限,强调入门与拉新(注意毛利与品牌调性)。",
|
f"- {_pos_mark(pos, 'entry')} **下探**:贴近区间下限,强调入门与拉新(注意毛利与调性)。",
|
||||||
f"- {_pos_mark(pos, 'different')} **另起带**:刻意避开主价格带,用规格/组合/服务差异化。",
|
f"- {_pos_mark(pos, 'different')} **另起带**:避开主价格带,用规格/组合/服务差异化。",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
lines.append("*摘要中无价带统计,请结合本批次价格相关数据补全后再填上表。*")
|
lines.append("*摘要中无价带统计,请结合本批次价格数据补全。*")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"**定位选项(请勾一条或改写,并写明理由)**",
|
"**主定位(与表单一致)**",
|
||||||
"",
|
"",
|
||||||
f"- {_pos_mark(pos, 'top')} **贴顶**:对标中高位或头部价位带,强调品质/成分/背书。",
|
f"- {_pos_mark(pos, 'top')} **贴顶**:中高位或头部价位带,强调品质/成分/背书。",
|
||||||
f"- {_pos_mark(pos, 'mid')} **卡腰**:围绕中位数一带,强调性价比与场景匹配。",
|
f"- {_pos_mark(pos, 'mid')} **卡腰**:围绕中位数一带,强调性价比与场景匹配。",
|
||||||
f"- {_pos_mark(pos, 'entry')} **下探**:贴近区间下限,强调入门与拉新(注意毛利与品牌调性)。",
|
f"- {_pos_mark(pos, 'entry')} **下探**:贴近区间下限,强调入门与拉新(注意毛利与调性)。",
|
||||||
f"- {_pos_mark(pos, 'different')} **另起带**:刻意避开主价格带,用规格/组合/服务差异化。",
|
f"- {_pos_mark(pos, 'different')} **另起带**:避开主价格带,用规格/组合/服务差异化。",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
ckw = brief.get("comment_focus_keywords") or []
|
ckw = brief.get("comment_focus_keywords") or []
|
||||||
usc = brief.get("usage_scenarios") or []
|
usc = brief.get("usage_scenarios") or []
|
||||||
lines.extend(["## 五、用户需求与场景 — 可写成策略的假设", ""])
|
lines.extend(["## 五、用户与评论侧", ""])
|
||||||
if use_ch8_probe:
|
if use_ch8_probe:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"*当前任务报告以**第八章评论侧文本挖掘**为主线(替代原关注词/场景条形图为主呈现)时,**请勿**再逐条照搬「子串命中次数」式假设列表——结构化摘要里仍可能含 `comment_focus_keywords` / `usage_scenarios` 等字段,**与当前报告正文展示口径可能不一致**。*",
|
"*当前任务报告以**第八章评论侧文本挖掘**为主呈现时,此处**不**逐条罗列子串命中次数(与报告 §8 口径一致)。*",
|
||||||
"",
|
"",
|
||||||
"*请结合报告中 **§8** 评论与情感归纳、文本挖掘小节,以及评价原文抽样,自拟若干条「待验证假设」(需可复核、可证伪)。*",
|
"- **需求焦点**:*(骨架占位;成稿写 1~2 句可执行结论)*",
|
||||||
|
"- **场景侧重**:*(骨架占位)*",
|
||||||
|
"- **传播切入点**:*(骨架占位)*",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
lines.append(
|
lines.append("*下列为关注词/场景**统计摘录**(口径同本批次摘要);成稿时写成具体动作,勿重复统计句。*")
|
||||||
"*下列由关注词/场景**计数**转化而来,是「待验证假设」而非结论;请结合评价原文抽样修订。*"
|
|
||||||
)
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if ckw:
|
if ckw:
|
||||||
for item in ckw[:8]:
|
for item in ckw[:8]:
|
||||||
@ -288,8 +299,7 @@ def build_strategy_draft_markdown(
|
|||||||
w = _esc(item.get("word"))
|
w = _esc(item.get("word"))
|
||||||
c = _num(item.get("count"))
|
c = _num(item.get("count"))
|
||||||
lines.append(
|
lines.append(
|
||||||
f"- **假设**:用户决策中「{w}」被频繁提及(约 {c} 次统计命中)—— "
|
f"- 「{w}」:子串统计命中约 **{c}** 次(口径同报告关注词)。"
|
||||||
f"*可追问:本品故事是否正面回应?传播关键词是否覆盖?*"
|
|
||||||
)
|
)
|
||||||
if usc:
|
if usc:
|
||||||
for item in usc[:6]:
|
for item in usc[:6]:
|
||||||
@ -298,19 +308,18 @@ def build_strategy_draft_markdown(
|
|||||||
cn = _num(item.get("count"))
|
cn = _num(item.get("count"))
|
||||||
sh = _pct(item.get("share_of_text_units"))
|
sh = _pct(item.get("share_of_text_units"))
|
||||||
lines.append(
|
lines.append(
|
||||||
f"- **场景命题**:「{sc}」在预设场景中约 {cn} 条、约占 {sh} 文本单元—— "
|
f"- 场景「{sc}」:约 **{cn}** 条,约占 **{sh}** 文本单元(预设场景分组)。"
|
||||||
f"*可追问:主图/详情/客服话术是否对齐该场景?*"
|
|
||||||
)
|
)
|
||||||
if not ckw and not usc:
|
if not ckw and not usc:
|
||||||
lines.append("*摘要中无关注词/场景组结果,请补全评论侧分析后再写本节。*")
|
lines.append("*摘要中无关注词/场景组,请结合评论侧分析补全本节。*")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
hints = brief.get("strategy_hints") or []
|
hints = brief.get("strategy_hints") or []
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 六、机会方向与策略支柱(草案)",
|
"## 六、机会与策略支柱",
|
||||||
"",
|
"",
|
||||||
"### 规则引擎提示(来自摘要 `strategy_hints`)",
|
"### 摘要提示(`strategy_hints`)",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -318,7 +327,7 @@ def build_strategy_draft_markdown(
|
|||||||
for h in hints:
|
for h in hints:
|
||||||
lines.append(f"- {_esc(h)}")
|
lines.append(f"- {_esc(h)}")
|
||||||
else:
|
else:
|
||||||
lines.append("*(当前无自动线索,请结合本批次结论手写 3~5 条机会)*")
|
lines.append("*(当前无自动线索)*")
|
||||||
pp = str(d.get("pillar_product") or "")
|
pp = str(d.get("pillar_product") or "")
|
||||||
pr = str(d.get("pillar_price") or "")
|
pr = str(d.get("pillar_price") or "")
|
||||||
pch = str(d.get("pillar_channel") or "")
|
pch = str(d.get("pillar_channel") or "")
|
||||||
@ -326,10 +335,10 @@ def build_strategy_draft_markdown(
|
|||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"### 策略支柱 — 请业务逐项填空",
|
"### 策略支柱(表单已填优先)",
|
||||||
"",
|
"",
|
||||||
"| 支柱 | 本品打算怎么做 | 与头部差异 | 证据 / 出处 |",
|
"| 支柱 | 本品动作 | 与头部差异 | 证据 / 出处 |",
|
||||||
"|------|----------------|------------|-------------|",
|
"|------|----------|------------|-------------|",
|
||||||
f"| 产品 | {_pillar_cell(pp)} | *待填* | *§* |",
|
f"| 产品 | {_pillar_cell(pp)} | *待填* | *§* |",
|
||||||
f"| 价格 | {_pillar_cell(pr)} | *待填* | *§* |",
|
f"| 价格 | {_pillar_cell(pr)} | *待填* | *§* |",
|
||||||
f"| 渠道/触点 | {_pillar_cell(pch)} | *待填* | *§* |",
|
f"| 渠道/触点 | {_pillar_cell(pch)} | *待填* | *§* |",
|
||||||
@ -342,17 +351,17 @@ def build_strategy_draft_markdown(
|
|||||||
rp = bool(d.get("ack_risk_price"))
|
rp = bool(d.get("ack_risk_price"))
|
||||||
rc = bool(d.get("ack_risk_concentration"))
|
rc = bool(d.get("ack_risk_concentration"))
|
||||||
rk_kw = (
|
rk_kw = (
|
||||||
"评论侧量化/关键词归纳是否**以偏概全**?(需原评论抽样)"
|
"评论侧归纳是否以偏概全?(需原评论抽样)"
|
||||||
if use_ch8_probe
|
if use_ch8_probe
|
||||||
else "关注词/场景是否**以偏概全**?(需原评论抽样)"
|
else "关注词/场景统计是否以偏概全?(需原评论抽样)"
|
||||||
)
|
)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 七、风险与待证伪",
|
"## 七、风险核对项",
|
||||||
"",
|
"",
|
||||||
_risk_line(rk, rk_kw),
|
_risk_line(rk, rk_kw),
|
||||||
_risk_line(rp, "价格带是否含大促/异常挂价?(需核对清洗规则)"),
|
_risk_line(rp, "价格带是否含大促/异常挂价?(需核对清洗规则)"),
|
||||||
_risk_line(rc, "列表集中度与深入样本品牌是否**矛盾**?(需解释渠道差异)"),
|
_risk_line(rc, "列表集中度与深入样本品牌是否不一致?(需解释渠道差异)"),
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -360,21 +369,19 @@ def build_strategy_draft_markdown(
|
|||||||
notes = _esc(business_notes)
|
notes = _esc(business_notes)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 八、业务约束与内部判断",
|
"## 八、业务约束与备注",
|
||||||
"",
|
"",
|
||||||
(notes if notes else "*(未填写。建议补充:渠道红线、价位策略、竞品对标名单、预算量级等。)*"),
|
(notes if notes else "*(未填写业务备注。)*"),
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 九、建议下一步(策略向)",
|
"## 九、下一步(可执行)",
|
||||||
"",
|
"",
|
||||||
"- [ ] 开会对齐:**§一** 目标与 **§八** 约束,确认 1~2 条主策略命题。",
|
"- [ ] 对齐 **§一** 目标与 **§八** 约束,锁定 1~2 条主命题。",
|
||||||
"- [ ] 为 **§六** 策略支柱表格每一行各找 **1 条数据证据**(注明出处)。",
|
"- [ ] 为 **§六** 支柱各补 **1 条数据证据** + **12 周内可交付动作**(负责人 + 时间)。",
|
||||||
"- [ ] 产出 **12 周节奏表**(里程碑 + 负责人),与本品排期挂钩。",
|
|
||||||
"- [ ] 定义 **3 个可观测指标**(周或双周复盘)。",
|
|
||||||
"",
|
"",
|
||||||
"---",
|
"---",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@ -438,6 +438,15 @@ class StrategyDraftRequestSerializer(serializers.Serializer):
|
|||||||
pillar_comm = serializers.CharField(
|
pillar_comm = serializers.CharField(
|
||||||
required=False, allow_blank=True, default="", max_length=800, trim_whitespace=False
|
required=False, allow_blank=True, default="", max_length=800, trim_whitespace=False
|
||||||
)
|
)
|
||||||
|
audience_segment = serializers.CharField(
|
||||||
|
required=False, allow_blank=True, default="", max_length=500, trim_whitespace=False
|
||||||
|
)
|
||||||
|
competitor_reference = serializers.CharField(
|
||||||
|
required=False, allow_blank=True, default="", max_length=800, trim_whitespace=False
|
||||||
|
)
|
||||||
|
resource_notes = serializers.CharField(
|
||||||
|
required=False, allow_blank=True, default="", max_length=1000, trim_whitespace=False
|
||||||
|
)
|
||||||
ack_risk_keywords = serializers.BooleanField(required=False, default=False)
|
ack_risk_keywords = serializers.BooleanField(required=False, default=False)
|
||||||
ack_risk_price = serializers.BooleanField(required=False, default=False)
|
ack_risk_price = serializers.BooleanField(required=False, default=False)
|
||||||
ack_risk_concentration = serializers.BooleanField(required=False, default=False)
|
ack_risk_concentration = serializers.BooleanField(required=False, default=False)
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
self.assertIn("任务 ID**:99", md)
|
self.assertIn("任务 ID**:99", md)
|
||||||
self.assertIn("假设A", md)
|
self.assertIn("假设A", md)
|
||||||
self.assertIn("重点:华东", md)
|
self.assertIn("重点:华东", md)
|
||||||
self.assertIn("战略背景与目标", md)
|
self.assertIn("目标与边界", md)
|
||||||
self.assertIn("市场策略制定草稿", md)
|
self.assertIn("市场策略制定草稿", md)
|
||||||
|
|
||||||
def test_strategy_decisions_merge(self) -> None:
|
def test_strategy_decisions_merge(self) -> None:
|
||||||
@ -52,9 +52,9 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
self.assertIn("- [ ] **贴顶**", md)
|
self.assertIn("- [ ] **贴顶**", md)
|
||||||
self.assertIn("侧翼切入", md)
|
self.assertIn("侧翼切入", md)
|
||||||
self.assertIn("| 产品 | 做低糖配方 |", md)
|
self.assertIn("| 产品 | 做低糖配方 |", md)
|
||||||
self.assertIn("- [x] 关注词/场景是否**以偏概全**", md)
|
self.assertIn("- [x] 关注词/场景统计是否以偏概全", md)
|
||||||
self.assertIn("- [ ] 价格带是否含大促", md)
|
self.assertIn("- [ ] 价格带是否含大促", md)
|
||||||
self.assertIn("- [x] 列表集中度与深入样本品牌是否**矛盾**", md)
|
self.assertIn("- [x] 列表集中度与深入样本品牌是否不一致", md)
|
||||||
|
|
||||||
def test_chapter8_probe_omits_focus_scenario_count_bullets(self) -> None:
|
def test_chapter8_probe_omits_focus_scenario_count_bullets(self) -> None:
|
||||||
brief = {
|
brief = {
|
||||||
@ -76,8 +76,8 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
report_config={"chapter8_text_mining_probe": True},
|
report_config={"chapter8_text_mining_probe": True},
|
||||||
)
|
)
|
||||||
self.assertIn("文本挖掘", md)
|
self.assertIn("文本挖掘", md)
|
||||||
self.assertNotIn("假设**:用户决策中「口感」", md)
|
self.assertNotIn("子串统计命中约 **501**", md)
|
||||||
self.assertNotIn("场景命题**:「控糖", md)
|
self.assertNotIn("场景「控糖", md)
|
||||||
|
|
||||||
def test_legacy_report_shows_focus_scenario_bullets(self) -> None:
|
def test_legacy_report_shows_focus_scenario_bullets(self) -> None:
|
||||||
brief = {
|
brief = {
|
||||||
@ -98,5 +98,5 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
brief=brief,
|
brief=brief,
|
||||||
report_config={"chapter8_text_mining_probe": False},
|
report_config={"chapter8_text_mining_probe": False},
|
||||||
)
|
)
|
||||||
self.assertIn("假设**:用户决策中「口感」", md)
|
self.assertIn("「口感」:子串统计命中约 **501** 次", md)
|
||||||
self.assertIn("场景命题**:「控糖/血糖相关」", md)
|
self.assertIn("场景「控糖/血糖相关」:约 **305** 条", md)
|
||||||
|
|||||||
@ -133,6 +133,9 @@ class JobStrategyDraftView(APIView):
|
|||||||
"pillar_price": vd.get("pillar_price") or "",
|
"pillar_price": vd.get("pillar_price") or "",
|
||||||
"pillar_channel": vd.get("pillar_channel") or "",
|
"pillar_channel": vd.get("pillar_channel") or "",
|
||||||
"pillar_comm": vd.get("pillar_comm") or "",
|
"pillar_comm": vd.get("pillar_comm") or "",
|
||||||
|
"audience_segment": vd.get("audience_segment") or "",
|
||||||
|
"competitor_reference": vd.get("competitor_reference") or "",
|
||||||
|
"resource_notes": vd.get("resource_notes") or "",
|
||||||
"ack_risk_keywords": bool(vd.get("ack_risk_keywords")),
|
"ack_risk_keywords": bool(vd.get("ack_risk_keywords")),
|
||||||
"ack_risk_price": bool(vd.get("ack_risk_price")),
|
"ack_risk_price": bool(vd.get("ack_risk_price")),
|
||||||
"ack_risk_concentration": bool(vd.get("ack_risk_concentration")),
|
"ack_risk_concentration": bool(vd.get("ack_risk_concentration")),
|
||||||
|
|||||||
@ -549,6 +549,18 @@ components:
|
|||||||
pillar_comm:
|
pillar_comm:
|
||||||
type: string
|
type: string
|
||||||
maxLength: 800
|
maxLength: 800
|
||||||
|
audience_segment:
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
description: 目标客群一句话(可选)
|
||||||
|
competitor_reference:
|
||||||
|
type: string
|
||||||
|
maxLength: 800
|
||||||
|
description: 主要对标品牌或价位带(可选)
|
||||||
|
resource_notes:
|
||||||
|
type: string
|
||||||
|
maxLength: 1000
|
||||||
|
description: 资源与预算量级备注(可选)
|
||||||
ack_risk_keywords:
|
ack_risk_keywords:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|||||||
@ -48,6 +48,9 @@ const decisions = reactive({
|
|||||||
pillar_price: '',
|
pillar_price: '',
|
||||||
pillar_channel: '',
|
pillar_channel: '',
|
||||||
pillar_comm: '',
|
pillar_comm: '',
|
||||||
|
audience_segment: '',
|
||||||
|
competitor_reference: '',
|
||||||
|
resource_notes: '',
|
||||||
ack_risk_keywords: false,
|
ack_risk_keywords: false,
|
||||||
ack_risk_price: false,
|
ack_risk_price: false,
|
||||||
ack_risk_concentration: false,
|
ack_risk_concentration: false,
|
||||||
@ -89,6 +92,9 @@ function buildPayload() {
|
|||||||
pillar_price: decisions.pillar_price,
|
pillar_price: decisions.pillar_price,
|
||||||
pillar_channel: decisions.pillar_channel,
|
pillar_channel: decisions.pillar_channel,
|
||||||
pillar_comm: decisions.pillar_comm,
|
pillar_comm: decisions.pillar_comm,
|
||||||
|
audience_segment: decisions.audience_segment,
|
||||||
|
competitor_reference: decisions.competitor_reference,
|
||||||
|
resource_notes: decisions.resource_notes,
|
||||||
ack_risk_keywords: decisions.ack_risk_keywords,
|
ack_risk_keywords: decisions.ack_risk_keywords,
|
||||||
ack_risk_price: decisions.ack_risk_price,
|
ack_risk_price: decisions.ack_risk_price,
|
||||||
ack_risk_concentration: decisions.ack_risk_concentration,
|
ack_risk_concentration: decisions.ack_risk_concentration,
|
||||||
@ -176,7 +182,7 @@ watch(
|
|||||||
<p class="hint-top">
|
<p class="hint-top">
|
||||||
选择<strong>已成功</strong>任务,在下方填空与勾选。<strong>默认</strong>使用大模型在规则底稿与同任务数据摘要基础上成稿(需服务端已配置网关)。若需更快、不调用智能服务,可勾选「本次仅生成规则稿」。策略配置与「分析报告生成」页的<strong>报告配置</strong>相互独立。策略稿与宿主报告第九章的归纳<strong>默认对齐</strong>。提交后跳转到
|
选择<strong>已成功</strong>任务,在下方填空与勾选。<strong>默认</strong>使用大模型在规则底稿与同任务数据摘要基础上成稿(需服务端已配置网关)。若需更快、不调用智能服务,可勾选「本次仅生成规则稿」。策略配置与「分析报告生成」页的<strong>报告配置</strong>相互独立。策略稿与宿主报告第九章的归纳<strong>默认对齐</strong>。提交后跳转到
|
||||||
<RouterLink to="/jd/strategy-view">策略稿预览</RouterLink>
|
<RouterLink to="/jd/strategy-view">策略稿预览</RouterLink>
|
||||||
。未填项在文稿中仍保留占位提示。
|
。<strong>决策在本页完成</strong>:已填项会写入底稿并由大模型落实为执行句;未填项由大模型结合数据补全。成稿不再重复「请再选」式表述。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@ -226,6 +232,30 @@ watch(
|
|||||||
<span>非目标</span>
|
<span>非目标</span>
|
||||||
<textarea v-model="decisions.non_goals" rows="2" placeholder="明确不做什么(可选)" />
|
<textarea v-model="decisions.non_goals" rows="2" placeholder="明确不做什么(可选)" />
|
||||||
</label>
|
</label>
|
||||||
|
<label class="fld fld-block">
|
||||||
|
<span>目标客群</span>
|
||||||
|
<input
|
||||||
|
v-model="decisions.audience_segment"
|
||||||
|
type="text"
|
||||||
|
placeholder="一句话:为谁、什么场景(可选)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="fld fld-block">
|
||||||
|
<span>主要对标</span>
|
||||||
|
<input
|
||||||
|
v-model="decisions.competitor_reference"
|
||||||
|
type="text"
|
||||||
|
placeholder="品牌或价位带参照(可选)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="fld fld-block">
|
||||||
|
<span>资源与预算备注</span>
|
||||||
|
<textarea
|
||||||
|
v-model="decisions.resource_notes"
|
||||||
|
rows="2"
|
||||||
|
placeholder="人力、投放、产能等量级(可选)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user