diff --git a/backend/pipeline/demos/dump_strategy_llm_input_md.py b/backend/pipeline/demos/dump_strategy_llm_input_md.py index fe76270..1bc775a 100644 --- a/backend/pipeline/demos/dump_strategy_llm_input_md.py +++ b/backend/pipeline/demos/dump_strategy_llm_input_md.py @@ -36,6 +36,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") def _strategy_decisions_empty() -> dict[str, Any]: return { "product_role": "", + "stage_goal_type": "", "time_horizon": "", "success_criteria": "", "non_goals": "", diff --git a/backend/pipeline/llm/generate_strategy.py b/backend/pipeline/llm/generate_strategy.py index 30829eb..40fa1d3 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -15,6 +15,7 @@ from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window # 与策略生成表单 POST 字段一致:任一则视为业务已提供「实质决策」,否则由模型基于数据推断草案。 _STRATEGY_DECISION_SUBSTANTIVE_KEYS: tuple[str, ...] = ( "product_role", + "stage_goal_type", "battlefield_one_line", "audience_segment", "time_horizon", @@ -142,7 +143,7 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要** - 仍须遵守全局禁止编造:数字、品牌、店铺、用户原话、活动规则仅可来自输入依据;无依据处用假设语气。 **决策边界(硬性)**: -- **当 `strategy_decisions_substantive` 为 true 时**:业务已在 `strategy_decisions` 中填写的项(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。 +- **当 `strategy_decisions_substantive` 为 true 时**:业务已在 `strategy_decisions` 中填写的项(角色、**本阶段策略目标类型**(`stage_goal_type`)、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。**若 `stage_goal_type` 非空**,「策略范围与前提」表中「本阶段策略目标类型」列须**直接采用该表述**(可略作语序润色),**不得**改判为另一类阶段目标。 - **当 `strategy_decisions_substantive` 为 true** 而部分表单项仍为空或占位:结合监测摘要与节选**补全为可执行表述**,与数据方向一致。 - **当 `strategy_decisions_substantive` 为 false 时**:适用上文「业务决策未填写时的成稿义务」,**禁止**以「请先填表」类表述搪塞全篇。 - **成稿阶段避免**:反复「请业务决策」;不确定时在 §2.1 用「类目/细类」+「假设:」「待业务确认:」**写清**,**禁止**只写泛化一句。 diff --git a/backend/pipeline/reporting/strategy_draft.py b/backend/pipeline/reporting/strategy_draft.py index 15afc49..1fa3323 100644 --- a/backend/pipeline/reporting/strategy_draft.py +++ b/backend/pipeline/reporting/strategy_draft.py @@ -238,11 +238,13 @@ def build_strategy_draft_markdown( if not for_llm_input else "*(待确认)*" ) - _table_goal_type = ( - "*(成稿从规划文档「目标类型选项」择一或组合,并与下列成功标准一致)*" - if not for_llm_input - else "*(待确认)*" + sgt = _esc(d.get("stage_goal_type") or "").strip() + _goal_type_placeholder = ( + "待确认:可填如「A 心智占位 / B 转化爬坡 …」,见规划文档「目标类型选项」" + if for_llm_input + else "成稿从规划文档「目标类型选项」择一或组合,并与下列成功标准一致" ) + _table_goal_type = _scope_cell(sgt, _goal_type_placeholder) _summary_user_side = ( "- **用户侧**:*(一两句结论即可:讨论焦点与负向主题;**按细类分句**归纳,**勿**混成「全站用户」一句;**勿**展开与报告重复的细类统计、词频。)*" if not for_llm_input @@ -357,6 +359,11 @@ def build_strategy_draft_markdown( ] ), _goal_bullet("本品角色", str(d.get("product_role") or ""), "新品 / 追赶 / 防守 / 拓品类 …"), + _goal_bullet( + "本阶段策略目标类型", + str(d.get("stage_goal_type") or ""), + "见规划文档「目标类型选项」或自填(如心智占位、转化爬坡)", + ), _goal_bullet( "目标客群", str(d.get("audience_segment") or ""), diff --git a/backend/pipeline/serializers.py b/backend/pipeline/serializers.py index 0f18d6f..4c7e189 100644 --- a/backend/pipeline/serializers.py +++ b/backend/pipeline/serializers.py @@ -410,6 +410,13 @@ class StrategyDraftRequestSerializer(serializers.Serializer): product_role = serializers.CharField( required=False, allow_blank=True, default="", max_length=500, trim_whitespace=False ) + stage_goal_type = serializers.CharField( + required=False, + allow_blank=True, + default="", + max_length=1000, + trim_whitespace=False, + ) time_horizon = serializers.CharField( required=False, allow_blank=True, default="", max_length=200, trim_whitespace=False ) diff --git a/backend/pipeline/tests/test_strategy_draft.py b/backend/pipeline/tests/test_strategy_draft.py index ad379d7..a2d8723 100644 --- a/backend/pipeline/tests/test_strategy_draft.py +++ b/backend/pipeline/tests/test_strategy_draft.py @@ -20,6 +20,9 @@ class StrategyDraftTests(SimpleTestCase): self.assertTrue( strategy_decisions_substantive({"product_role": "新品"}) ) + self.assertTrue( + strategy_decisions_substantive({"stage_goal_type": "B 转化爬坡"}) + ) def test_for_llm_input_omits_dev_traces(self) -> None: brief = { @@ -78,6 +81,24 @@ class StrategyDraftTests(SimpleTestCase): self.assertIn("## 七、品牌四线", md) self.assertIn("市场策略制定草稿", md) + def test_stage_goal_type_in_scope_table(self) -> None: + brief = { + "schema_version": 1, + "keyword": "K", + "batch_label": "b1", + "scope": {"merged_sku_count": 2}, + } + md = build_strategy_draft_markdown( + job_id=1, + keyword="K", + brief=brief, + strategy_decisions={"stage_goal_type": "A 心智占位"}, + generated_at_iso="2026-04-09T12:00:00", + for_llm_input=True, + ) + self.assertIn("| **本阶段策略目标类型** | A 心智占位 |", md) + self.assertIn("**本阶段策略目标类型**:A 心智占位", md) + def test_strategy_decisions_merge(self) -> None: brief = {"schema_version": 1, "keyword": "K", "batch_label": "b"} decisions = { diff --git a/backend/pipeline/views/job_report_views.py b/backend/pipeline/views/job_report_views.py index 8dd19f1..5463daf 100644 --- a/backend/pipeline/views/job_report_views.py +++ b/backend/pipeline/views/job_report_views.py @@ -141,6 +141,7 @@ class JobStrategyDraftView(APIView): notes = (vd.get("business_notes") or "").strip() strategy_decisions = { "product_role": vd.get("product_role") or "", + "stage_goal_type": vd.get("stage_goal_type") or "", "time_horizon": vd.get("time_horizon") or "", "success_criteria": vd.get("success_criteria") or "", "non_goals": vd.get("non_goals") or "", diff --git a/frontend/src/views/jd/JdStrategyBuildView.vue b/frontend/src/views/jd/JdStrategyBuildView.vue index 7936eff..414d7c8 100644 --- a/frontend/src/views/jd/JdStrategyBuildView.vue +++ b/frontend/src/views/jd/JdStrategyBuildView.vue @@ -49,6 +49,7 @@ const briefMatrixErr = ref('') const decisions = reactive({ product_role: '', + stage_goal_type: '', time_horizon: '', success_criteria: '', non_goals: '', @@ -96,6 +97,7 @@ function buildPayload() { generator, business_notes: businessNotes.value, product_role: decisions.product_role, + stage_goal_type: decisions.stage_goal_type, time_horizon: decisions.time_horizon, success_criteria: decisions.success_criteria, non_goals: decisions.non_goals, @@ -315,7 +317,7 @@ watch(
策略范围与前提

- 界定本次策略的任务边界与阶段目标。监测词、批次由任务自动带出;主推类目以顶部「矩阵细类」为准。角色、战场、客群会进入策略稿开篇,并与下一栏「主要对标」衔接。 + 界定本次策略的任务边界与阶段目标。监测词、批次由任务自动带出;主推类目以顶部「矩阵细类」为准。角色、本阶段策略目标类型、战场、客群会进入策略稿开篇;目标类型填好后大模型不得擅自改判。并与下一栏「主要对标」衔接。

+