diff --git a/backend/pipeline/llm/generate_strategy.py b/backend/pipeline/llm/generate_strategy.py index d35612a..709886b 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -18,7 +18,8 @@ STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要** - **不得编造**输入中不存在的销量、占比、价格数字;若底稿与摘要中有数字,须保持一致;表述集中度时用「第一大……份额」「前三家合计」等中文,**不要用** CR1、CR3 等缩写; - 若 `structured_brief` 含 `matrix_overview_for_llm` 或矩阵相关字段,策略中应**呼应**细分类目分组与竞品矩阵结论,不得无故删光矩阵相关建议; - 可调整段落衔接、标题层级、列表与表格呈现,使更易读;可补充「建议」「待业务确认」类表述,但不虚构竞品名称或数据; -- **仅输出** Markdown 正文(不要 ``` 围栏包裹全文)。""" +- **仅输出** Markdown 正文(不要 ``` 围栏包裹全文); +- **输出完整性**:须收束各小节与全文,勿在句子或列表中途戛然而止。""" STRATEGY_USER_PREFIX = """请基于以下 JSON 输出最终策略稿(Markdown)。\n\n""" @@ -45,24 +46,86 @@ def generate_strategy_draft_markdown_llm( generated_at_iso=generated_at_iso, strategy_decisions=strategy_decisions, ) - compact = compact_brief_for_llm(brief, max_chars=80_000) - excerpt = (report_strategy_excerpt or "").strip() - payload = { - "job_id": job_id, - "keyword": keyword, - "generated_at_iso": generated_at_iso, - "strategy_decisions": strategy_decisions, - "business_notes": business_notes, - "structured_brief": compact, - "rules_draft_markdown": rules_md, - "report_strategy_excerpt": excerpt, - } - raw = json.dumps(payload, ensure_ascii=False) - if len(raw) > 500_000: - payload["rules_draft_markdown"] = rules_md[:200_000] + "\n\n…(底稿过长已截断,请勿编造截断后内容)\n" + excerpt_raw = (report_strategy_excerpt or "").strip() + sys_prompt = STRATEGY_SYSTEM + min_comp = _min_strategy_completion_tokens() + min_comp_relaxed = max(256, min_comp // 2) + + def _payload_and_user( + *, + compact_max: int, + excerpt_max: int, + rules_max: int | None, + ) -> str: + compact = compact_brief_for_llm(brief, max_chars=compact_max) + ex = ( + _truncate_strategy_narrative(excerpt_raw, excerpt_max) + if excerpt_raw + else "" + ) + if rules_max is None: + rd = rules_md + else: + rd = _truncate_rules_draft_md(rules_md, rules_max) + payload: dict[str, Any] = { + "job_id": job_id, + "keyword": keyword, + "generated_at_iso": generated_at_iso, + "strategy_decisions": strategy_decisions, + "business_notes": business_notes, + "structured_brief": compact, + "rules_draft_markdown": rd, + "report_strategy_excerpt": ex, + } raw = json.dumps(payload, ensure_ascii=False) - user = STRATEGY_USER_PREFIX + raw - return call_llm(STRATEGY_SYSTEM, user) + if len(raw) > 500_000: + payload["rules_draft_markdown"] = _truncate_rules_draft_md(rd, 200_000) + raw = json.dumps(payload, ensure_ascii=False) + return STRATEGY_USER_PREFIX + raw + + for cap_brief, cap_excerpt, cap_rules in ( + (80_000, 24_000, None), + (64_000, 20_000, None), + (48_000, 17_000, None), + (36_000, 14_000, None), + (28_000, 11_000, None), + (22_000, 9_000, None), + (18_000, 7_000, None), + (14_000, 5_000, None), + (12_000, 4_000, 220_000), + (10_000, 3_500, 180_000), + (10_000, 3_000, 150_000), + (9_000, 2_500, 120_000), + (8_000, 2_000, 100_000), + (8_000, 2_000, 70_000), + ): + user = _payload_and_user( + compact_max=cap_brief, + excerpt_max=cap_excerpt, + rules_max=cap_rules, + ) + if _strategy_prompt_ok_for_call( + sys_prompt, user, min_completion_tokens=min_comp + ): + return call_llm(sys_prompt, user) + + for cap_brief, cap_excerpt, cap_rules in ( + (10_000, 2_000, 55_000), + (8_000, 1_500, 45_000), + (7_000, 1_200, 35_000), + ): + user = _payload_and_user( + compact_max=cap_brief, + excerpt_max=cap_excerpt, + rules_max=cap_rules, + ) + if _strategy_prompt_ok_for_call( + sys_prompt, user, min_completion_tokens=min_comp_relaxed + ): + return call_llm(sys_prompt, user) + + user = _payload_and_user(compact_max=6_000, excerpt_max=1_000, rules_max=28_000) + return call_llm(sys_prompt, user) STRATEGY_OPPORTUNITIES_SYSTEM = """你是 B 端市场与增长顾问。输入 JSON 含 ``keyword``、``competitor_brief``(与本任务规则报告同源的结构化摘要,可能经裁剪,并含 ``matrix_overview_for_llm``),以及可选 ``prior_chapter_llm_narratives``(本报告 **第五至第八章** 已生成的大模型归纳节选,与正文**同源**)。 @@ -97,6 +160,19 @@ STRATEGY_OPPORTUNITIES_USER_PREFIX = ( ) +def _truncate_rules_draft_md(text: str, max_chars: int) -> str: + """规则策略底稿过长时截断,避免 JSON 与 completion 预算挤占输出。""" + s = (text or "").strip() + if not s: + return "" + if len(s) <= max_chars: + return s + return ( + s[: max_chars - 80].rstrip() + + "\n\n…(规则底稿已截断,请勿编造截断后内容。)\n" + ) + + def _truncate_strategy_narrative(text: str, max_chars: int) -> str: s = (text or "").strip() if not s: