diff --git a/backend/pipeline/demos/dump_strategy_llm_input_md.py b/backend/pipeline/demos/dump_strategy_llm_input_md.py index 05f90ef..fe76270 100644 --- a/backend/pipeline/demos/dump_strategy_llm_input_md.py +++ b/backend/pipeline/demos/dump_strategy_llm_input_md.py @@ -1,11 +1,25 @@ """ -导出与「策略大模型润色」一次调用一致的完整输入,生成 Markdown(供对照)。 +导出「独立策略稿 · 大模型润色」一次调用与生产一致的完整入参(不请求网关)。 -用法(在项目 backend 目录):: +与 ``generate_strategy_draft_markdown_llm`` / ``resolve_strategy_draft_llm_input_snapshot`` +使用相同的截断阶梯与 ``payload`` 字段(含 ``strategy_decisions_substantive``)。 +用法(在 backend 目录):: + + # 按数据库任务(默认取最近成功任务;可指定 job-id) python -m pipeline.demos.dump_strategy_llm_input_md [--job-id 12] [--matrix-index 0] -若未指定 --matrix-index,则默认收窄到第一个矩阵分组(与「选第一个细类」等效)。 + # 仅磁盘 run_dir(无需 PipelineJob;job_id 写 0 进 JSON,仅影响底稿抬头占位) + python -m pipeline.demos.dump_strategy_llm_input_md --run-dir \"D:/.../pipeline_runs/某批次\" + + # 与线上一致:从文件载入当时提交的 strategy_decisions + python -m pipeline.demos.dump_strategy_llm_input_md --run-dir \"...\" --decisions-json decisions.json + + # 指定输出 + python -m pipeline.demos.dump_strategy_llm_input_md --run-dir \"...\" -o path/to/snap.md + + # 只打印摘要、不写文件 + python -m pipeline.demos.dump_strategy_llm_input_md --run-dir \"...\" --no-md """ from __future__ import annotations @@ -14,12 +28,12 @@ import json import os import sys from pathlib import Path +from typing import Any -# Django -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "market_assistant.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") -def _strategy_decisions_empty() -> dict: +def _strategy_decisions_empty() -> dict[str, Any]: return { "product_role": "", "time_horizon": "", @@ -43,6 +57,13 @@ def _strategy_decisions_empty() -> dict: } +def _merge_decisions(base: dict[str, Any], overlay: dict[str, Any] | None) -> dict[str, Any]: + out = dict(base) + if isinstance(overlay, dict): + out.update(overlay) + return out + + def main() -> int: import django @@ -53,12 +74,11 @@ def main() -> int: from pipeline.jd.runner import build_competitor_brief_for_job from pipeline.llm.generate_strategy import ( STRATEGY_SYSTEM, - STRATEGY_USER_PREFIX, - _omit_ch8_probe_wordchart_fields, - _truncate_strategy_narrative, + resolve_strategy_draft_llm_input_snapshot, + _min_strategy_completion_tokens, ) + from pipeline.llm.llm_client import estimate_chat_input_tokens from pipeline.models import JobStatus, PipelineJob - from pipeline.reporting.brief_compact import compact_brief_for_llm from pipeline.reporting.brief_strategy_scope import ( filter_brief_for_strategy_matrix_group, list_matrix_groups_for_api, @@ -67,13 +87,16 @@ def main() -> int: load_report_matrix_group_evidence_markdown, ) from pipeline.reporting.report_strategy_excerpt import load_report_strategy_excerpt - from pipeline.reporting.strategy_draft import ( - build_strategy_draft_markdown, - report_uses_chapter8_text_mining_probe, - ) - p = argparse.ArgumentParser() - p.add_argument("--job-id", type=int, default=None) + p = argparse.ArgumentParser(description=__doc__) + src = p.add_mutually_exclusive_group() + src.add_argument("--job-id", type=int, default=None, help="PipelineJob 主键") + src.add_argument( + "--run-dir", + type=Path, + default=None, + help="运行目录(与 job.run_dir 相同结构;与 --job-id 二选一)", + ) p.add_argument( "--matrix-index", type=int, @@ -85,40 +108,89 @@ def main() -> int: action="store_true", help="与 --matrix-index -1 相同:不收窄 brief、不抽细类报告节选", ) + p.add_argument( + "--decisions-json", + type=Path, + default=None, + help="覆盖 strategy_decisions 的 JSON 对象文件(与线上一致时传入)", + ) + p.add_argument( + "--business-notes", + type=str, + default="", + help="与接口 business_notes 一致的业务备注", + ) + p.add_argument( + "--snapshot-job-id", + type=int, + default=None, + help="写入 payload.job_id(仅 --run-dir 时有效;默认 0)", + ) p.add_argument( "-o", "--output", - type=str, + type=Path, default=None, - help="输出 .md 路径(默认:docs/planning/策略生成-LLM全量输入快照.md)", + help="输出 .md 路径(默认:run_dir/strategy_draft_llm_input_snapshot.md 或 docs/planning/…)", + ) + p.add_argument( + "--no-md", + action="store_true", + help="不写入 Markdown,仅打印控制台摘要", ) args = p.parse_args() backend_dir = Path(__file__).resolve().parents[2] repo_root = backend_dir.parent - default_out = ( + default_docs_out = ( repo_root / "docs" / "planning" / "策略生成-LLM全量输入快照.md" ) - out_path = Path(args.output) if args.output else default_out - job_id = args.job_id - if job_id: - job = PipelineJob.objects.filter(pk=job_id).first() + run_dir_s: str + kw: str + rc: dict[str, Any] | None + job_id: int + + if args.run_dir is not None: + run_dir_p = args.run_dir.expanduser().resolve() + if not run_dir_p.is_dir(): + print(f"run_dir 不存在: {run_dir_p}", file=sys.stderr) + return 1 + rc_path = run_dir_p / "effective_report_config.json" + meta_path = run_dir_p / "run_meta.json" + if not rc_path.is_file() or not meta_path.is_file(): + print("缺少 effective_report_config.json 或 run_meta.json", file=sys.stderr) + return 1 + rc = json.loads(rc_path.read_text(encoding="utf-8")) + meta = json.loads(meta_path.read_text(encoding="utf-8")) + kw = (meta.get("keyword") or "").strip() + if not kw: + print("run_meta 无 keyword", file=sys.stderr) + return 1 + run_dir_s = str(run_dir_p) + job_id = int(args.snapshot_job_id) if args.snapshot_job_id is not None else 0 else: - job = ( - PipelineJob.objects.filter(status=JobStatus.SUCCESS) - .exclude(run_dir="") - .order_by("-id") - .first() - ) - if not job: - print("无可用成功任务(需 run_dir 非空)", file=sys.stderr) - return 1 + jid = args.job_id + if jid: + job = PipelineJob.objects.filter(pk=jid).first() + else: + job = ( + PipelineJob.objects.filter(status=JobStatus.SUCCESS) + .exclude(run_dir="") + .order_by("-id") + .first() + ) + if not job: + print("无可用任务:请指定 --job-id 或 --run-dir", file=sys.stderr) + return 1 + run_dir_s = job.run_dir + kw = job.keyword + rc = job.report_config if isinstance(job.report_config, dict) else None + job_id = job.id - rc = job.report_config if isinstance(job.report_config, dict) else None brief = build_competitor_brief_for_job( - job.run_dir, - job.keyword, + run_dir_s, + kw, report_config=rc, ) matrix_groups = list_matrix_groups_for_api(brief) @@ -140,91 +212,106 @@ def main() -> int: gen_at = timezone.now().isoformat() sd = _strategy_decisions_empty() - rules_md = build_strategy_draft_markdown( - job_id=job.id, - keyword=job.keyword, - brief=brief, - business_notes="", - generated_at_iso=gen_at, - strategy_decisions=sd, - report_config=rc, - ) + if args.decisions_json is not None: + dp = args.decisions_json.expanduser().resolve() + if not dp.is_file(): + print(f"decisions-json 不存在: {dp}", file=sys.stderr) + return 1 + try: + loaded = json.loads(dp.read_text(encoding="utf-8")) + except json.JSONDecodeError as e: + print(f"decisions-json 非合法 JSON: {e}", file=sys.stderr) + return 1 + if not isinstance(loaded, dict): + print("decisions-json 根须为 JSON 对象", file=sys.stderr) + return 1 + sd = _merge_decisions(sd, loaded) - excerpt_raw, excerpt_src = load_report_strategy_excerpt(job.run_dir) + excerpt_raw, excerpt_src = load_report_strategy_excerpt(run_dir_s) excerpt_raw = (excerpt_raw or "").strip() evidence_md = "" evidence_src = "none" if scoped_label: evidence_md, evidence_src = load_report_matrix_group_evidence_markdown( - job.run_dir, + run_dir_s, scoped_label, ) - compact_max = 80_000 - excerpt_max = 24_000 - compact = compact_brief_for_llm(brief, max_chars=compact_max) - if report_uses_chapter8_text_mining_probe(rc): - compact = dict(compact) - _omit_ch8_probe_wordchart_fields(compact) - ex = ( - _truncate_strategy_narrative(excerpt_raw, excerpt_max) - if excerpt_raw - else "" - ) - ev_max = min(24_000, max(3_000, excerpt_max + excerpt_max // 2)) - gm = ( - _truncate_strategy_narrative(evidence_md.strip(), ev_max) - if evidence_md - else "" + payload, user_body, tier_note = resolve_strategy_draft_llm_input_snapshot( + job_id=job_id, + keyword=kw, + brief=brief, + business_notes=(args.business_notes or "").strip(), + generated_at_iso=gen_at, + strategy_decisions=sd, + report_strategy_excerpt=excerpt_raw or None, + report_matrix_group_evidence_md=evidence_md.strip() or None, + report_config=rc, ) - payload: dict = { - "job_id": job.id, - "keyword": job.keyword, - "generated_at_iso": gen_at, - "strategy_decisions": sd, - "business_notes": "", - "structured_brief": compact, - "rules_draft_markdown": rules_md, - "report_strategy_excerpt": ex, - "report_matrix_group_evidence_md": gm, - "chapter8_text_mining_probe": bool( - report_uses_chapter8_text_mining_probe(rc) - ), - } - if report_uses_chapter8_text_mining_probe(rc): - payload["structured_brief_omission_note"] = ( - "已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top、" - "`strategy_hints` 等;全量 brief 已**不再**包含 ``comment_sentiment_lexicon``。**不得**再以星级子集预设口语短语计数或预设场景占比作为论据。" - "用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。" - ) - - user_body = STRATEGY_USER_PREFIX + json.dumps(payload, ensure_ascii=False) + min_comp = _min_strategy_completion_tokens() + est_in = estimate_chat_input_tokens(STRATEGY_SYSTEM, user_body) full_chars = len(STRATEGY_SYSTEM) + len(user_body) + rd = payload.get("rules_draft_markdown") + rd_len = len(rd) if isinstance(rd, str) else 0 + sb = payload.get("structured_brief") + sb_json_len = len(json.dumps(sb, ensure_ascii=False)) if sb else 0 + + print("run_dir:", run_dir_s) + print("job_id (payload):", job_id) + print("keyword:", kw) + print("tier:", tier_note) + print("MA_STRATEGY_MIN_COMPLETION_TOKENS:", min_comp) + print("strategy_decisions_substantive:", payload.get("strategy_decisions_substantive")) + print("matrix scope:", f"{matrix_index} → 「{scoped_label}」" if scoped_label else "未收窄") + print("report_strategy_excerpt:", excerpt_src, "raw chars:", len(excerpt_raw)) + print("report_matrix_group_evidence:", evidence_src, "chars in payload:", len(payload.get("report_matrix_group_evidence_md") or "")) + print("STRATEGY_SYSTEM chars:", len(STRATEGY_SYSTEM)) + print("user chars:", len(user_body)) + print("total chars:", full_chars) + print("estimate_chat_input_tokens:", est_in) + print("structured_brief JSON len:", sb_json_len) + print("rules_draft_markdown len (in payload):", rd_len) + + if args.no_md: + return 0 + + if args.output is not None: + out_path = args.output.expanduser().resolve() + elif args.run_dir is not None: + out_path = args.run_dir.expanduser().resolve() / "strategy_draft_llm_input_snapshot.md" + else: + out_path = default_docs_out out_path.parent.mkdir(parents=True, exist_ok=True) - lines: list[str] = [ - "# 策略生成 · 大模型一次调用的「全量输入」快照", + "# 独立策略稿 · 大模型一次调用的「全量输入」快照", "", - "> **生成方式**:本机 `pipeline.demos.dump_strategy_llm_input_md` 按与 " - "`generate_strategy_draft_markdown_llm` 相同的 payload 组装逻辑导出。", - "> **与线上一致性**:与真实接口相比,表单字段此处均为空默认;" - "你只要把当时提交的 `strategy_decisions` / `business_notes` 代入即与线上等价。", + "> **生成方式**:`pipeline.demos.dump_strategy_llm_input_md` 调用 " + "`resolve_strategy_draft_llm_input_snapshot`,与 ``generate_strategy_draft_markdown_llm`` " + "首档通过的 ``payload`` / ``user`` 一致(不请求网关)。", + "> **与线上一致**:将当时 POST 的 `strategy_decisions`、`business_notes`、`strategy_matrix_group_index` " + "与本脚本参数对齐即可复现。", "", "## 快照元数据", "", - f"- **任务 ID**:{job.id}", - f"- **关键词**:{job.keyword}", - f"- **run_dir**:`{job.run_dir}`", + f"- **任务 ID(payload.job_id)**:{job_id}", + f"- **关键词**:{kw}", + f"- **run_dir**:`{run_dir_s}`", f"- **矩阵分组**:{matrix_index if matrix_index is not None and matrix_index >= 0 else '未收窄(全部分类)'}{f' → 「{scoped_label}」' if scoped_label else ''}", f"- **本任务可选细类(节选)**:{group_names[:20]}{'…' if len(group_names) > 20 else ''}", - f"- **第九章节选来源**:{excerpt_src},约 {len(ex)} 字符", - f"- **细类报告节选来源**:{evidence_src},约 {len(gm)} 字符", + f"- **选用档位**:{tier_note}", + f"- **strategy_decisions_substantive**:{payload.get('strategy_decisions_substantive')!r}", + f"- **第九章节选来源**:{excerpt_src}", + f"- **细类报告节选来源**:{evidence_src}", + f"- **MA_STRATEGY_MIN_COMPLETION_TOKENS**:{min_comp}", f"- **System 字符数**:{len(STRATEGY_SYSTEM)}", f"- **User 消息字符数**:{len(user_body)}", f"- **合计约**:{full_chars} 字符", + f"- **estimate_chat_input_tokens(项目内启发式)**:{est_in}", + f"- **structured_brief 序列化长度**:{sb_json_len}", + f"- **rules_draft_markdown(payload 内)字符数**:{rd_len}", "", "---", "", diff --git a/backend/pipeline/llm/generate.py b/backend/pipeline/llm/generate.py index 046a7ba..8a08829 100644 --- a/backend/pipeline/llm/generate.py +++ b/backend/pipeline/llm/generate.py @@ -45,6 +45,7 @@ from .generate_strategy import ( STRATEGY_USER_PREFIX, generate_strategy_draft_markdown_llm, generate_strategy_opportunities_llm, + resolve_strategy_draft_llm_input_snapshot, strategy_decisions_substantive, ) from .llm_client import call_llm as _call_llm @@ -81,6 +82,7 @@ __all__ = [ "generate_section_bridges_llm", "generate_strategy_draft_markdown_llm", "generate_strategy_opportunities_llm", + "resolve_strategy_draft_llm_input_snapshot", "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 de2d339..30829eb 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -200,6 +200,205 @@ STRATEGY_USER_PREFIX = ( ) +def _build_strategy_draft_llm_payload_and_user( + *, + job_id: int, + keyword: str, + generated_at_iso: str, + strategy_decisions: dict[str, Any], + business_notes: str, + brief: dict[str, Any], + report_config: dict[str, Any] | None, + rules_md: str, + excerpt_raw: str, + group_evidence_raw: str, + compact_max: int, + excerpt_max: int, + rules_max: int | None, +) -> tuple[dict[str, Any], str]: + compact = compact_brief_for_llm(brief, max_chars=compact_max) + if report_uses_chapter8_text_mining_probe(report_config): + compact = dict(compact) + _omit_ch8_probe_wordchart_fields(compact) + ex = ( + _truncate_strategy_narrative(excerpt_raw, excerpt_max) if excerpt_raw else "" + ) + ev_max = min(24_000, max(3_000, excerpt_max + excerpt_max // 2)) + gm = ( + _truncate_strategy_narrative(group_evidence_raw, ev_max) + if group_evidence_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, + "strategy_decisions_substantive": strategy_decisions_substantive( + strategy_decisions + ), + "business_notes": business_notes, + "structured_brief": compact, + "rules_draft_markdown": rd, + "report_strategy_excerpt": ex, + "report_matrix_group_evidence_md": gm, + "chapter8_text_mining_probe": bool( + report_uses_chapter8_text_mining_probe(report_config) + ), + } + if report_uses_chapter8_text_mining_probe(report_config): + payload["structured_brief_omission_note"] = ( + "已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top、" + "``strategy_hints`` 等;报告已**不再**输出 ``comment_sentiment_lexicon``(星级子集预设口语短语)及同口径图。**不得**再以这类子串计数、短语条形图或预设场景占比作为论据。" + "用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及 brief 已给字段一致;若 `report_strategy_excerpt` 非空则勿与其明显矛盾。**默认**节选为空,勿编造「报告策略长文已写明的」具体活动规则。" + ) + raw = json.dumps(payload, ensure_ascii=False) + if len(raw) > 500_000: + payload["rules_draft_markdown"] = _truncate_rules_draft_md(rd, 200_000) + raw = json.dumps(payload, ensure_ascii=False) + return payload, STRATEGY_USER_PREFIX + raw + + +def resolve_strategy_draft_llm_input_snapshot( + *, + job_id: int, + keyword: str, + brief: dict[str, Any], + business_notes: str, + generated_at_iso: str, + strategy_decisions: dict[str, Any], + report_strategy_excerpt: str | None = None, + report_matrix_group_evidence_md: str | None = None, + report_config: dict[str, Any] | None = None, +) -> tuple[dict[str, Any], str, str]: + """ + 复现 ``generate_strategy_draft_markdown_llm`` 在**首档通过** ``_strategy_prompt_ok_for_call`` 时的 + ``payload`` 与完整 ``user`` 字符串(不请求网关)。 + + 返回 ``(payload, user, tier_note)``;若所有档位均未通过,与生产一致仍返回最后一档兜底组装的 + ``(payload, user, tier_note)``(``tier_note`` 标明可能仍会由网关报错)。 + """ + rules_md = build_strategy_draft_markdown( + job_id=job_id, + keyword=keyword, + brief=brief, + business_notes=business_notes, + 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() + sys_prompt = STRATEGY_SYSTEM + min_comp = _min_strategy_completion_tokens() + min_comp_relaxed = max(256, min_comp // 2) + + 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), + ): + payload, user = _build_strategy_draft_llm_payload_and_user( + job_id=job_id, + keyword=keyword, + generated_at_iso=generated_at_iso, + strategy_decisions=strategy_decisions, + business_notes=business_notes, + brief=brief, + report_config=report_config, + rules_md=rules_md, + excerpt_raw=excerpt_raw, + group_evidence_raw=group_evidence_raw, + 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 + ): + rules_note = ( + "未截断" + if cap_rules is None + else f"rules_draft_markdown 截断上限 {cap_rules} 字" + ) + note = ( + "首档(标准 completion 阈值):" + f"structured_brief max_chars={cap_brief}," + f"report_strategy_excerpt / 节选侧 excerpt_max={cap_excerpt}," + f"{rules_note}。" + ) + return payload, user, note + + 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), + ): + payload, user = _build_strategy_draft_llm_payload_and_user( + job_id=job_id, + keyword=keyword, + generated_at_iso=generated_at_iso, + strategy_decisions=strategy_decisions, + business_notes=business_notes, + brief=brief, + report_config=report_config, + rules_md=rules_md, + excerpt_raw=excerpt_raw, + group_evidence_raw=group_evidence_raw, + 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 + ): + note = ( + "首档(relaxed completion 阈值):" + f"structured_brief max_chars={cap_brief}," + f"excerpt_max={cap_excerpt}," + f"rules_draft 截断上限 {cap_rules}。" + ) + return payload, user, note + + payload, user = _build_strategy_draft_llm_payload_and_user( + job_id=job_id, + keyword=keyword, + generated_at_iso=generated_at_iso, + strategy_decisions=strategy_decisions, + business_notes=business_notes, + brief=brief, + report_config=report_config, + rules_md=rules_md, + excerpt_raw=excerpt_raw, + group_evidence_raw=group_evidence_raw, + compact_max=6_000, + excerpt_max=1_000, + rules_max=28_000, + ) + note = ( + "所有标准/relaxed 档位均未通过 ``_strategy_prompt_ok_for_call``," + "与生产一致使用兜底档:structured_brief max_chars=6000,excerpt_max=1000," + "rules_draft 截断上限 28000(网关仍可能报错)。" + ) + return payload, user, note + + def generate_strategy_draft_markdown_llm( *, job_id: int, @@ -218,119 +417,18 @@ def generate_strategy_draft_markdown_llm( ``report_matrix_group_evidence_md``:按所选矩阵细类从 ``competitor_analysis.md`` 抽取的第五~第八章大模型小节摘录(见 ``reporting.report_matrix_group_evidence.load_report_matrix_group_evidence_markdown``);用于与收窄后的 ``structured_brief`` 一并支撑策略叙事。 """ - rules_md = build_strategy_draft_markdown( + _payload, user, _tier = resolve_strategy_draft_llm_input_snapshot( job_id=job_id, keyword=keyword, brief=brief, business_notes=business_notes, generated_at_iso=generated_at_iso, strategy_decisions=strategy_decisions, + report_strategy_excerpt=report_strategy_excerpt, + report_matrix_group_evidence_md=report_matrix_group_evidence_md, 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() - 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) - if report_uses_chapter8_text_mining_probe(report_config): - compact = dict(compact) - _omit_ch8_probe_wordchart_fields(compact) - ex = ( - _truncate_strategy_narrative(excerpt_raw, excerpt_max) - if excerpt_raw - else "" - ) - ev_max = min(24_000, max(3_000, excerpt_max + excerpt_max // 2)) - gm = ( - _truncate_strategy_narrative(group_evidence_raw, ev_max) - if group_evidence_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, - "strategy_decisions_substantive": strategy_decisions_substantive( - strategy_decisions - ), - "business_notes": business_notes, - "structured_brief": compact, - "rules_draft_markdown": rd, - "report_strategy_excerpt": ex, - "report_matrix_group_evidence_md": gm, - "chapter8_text_mining_probe": bool( - report_uses_chapter8_text_mining_probe(report_config) - ), - } - if report_uses_chapter8_text_mining_probe(report_config): - payload["structured_brief_omission_note"] = ( - "已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top、" - "``strategy_hints`` 等;报告已**不再**输出 ``comment_sentiment_lexicon``(星级子集预设口语短语)及同口径图。**不得**再以这类子串计数、短语条形图或预设场景占比作为论据。" - "用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及 brief 已给字段一致;若 `report_strategy_excerpt` 非空则勿与其明显矛盾。**默认**节选为空,勿编造「报告策略长文已写明的」具体活动规则。" - ) - raw = json.dumps(payload, ensure_ascii=False) - 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) + return call_llm(STRATEGY_SYSTEM, user) STRATEGY_OPPORTUNITIES_SYSTEM = (