fix(strategy): 探针模式下从 structured_brief 移除按细类关注词/场景子串

补充 _omit_ch8_probe_wordchart_fields:删除 consumer_feedback 内 focus_keyword_hits 与 scenarios_top;与文本挖掘主口径一致。导出脚本与 omission 说明同步;更新全量输入快照。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-21 09:42:46 +08:00
parent 9cf48e4d7b
commit ce4cef3235
4 changed files with 1091 additions and 10 deletions

View File

@ -0,0 +1,269 @@
"""
导出与策略大模型润色一次调用一致的完整输入生成 Markdown供对照
用法在项目 backend 目录::
python -m pipeline.demos.dump_strategy_llm_input_md [--job-id 12] [--matrix-index 0]
若未指定 --matrix-index则默认收窄到第一个矩阵分组选第一个细类等效
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
# Django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "market_assistant.settings")
def _strategy_decisions_empty() -> dict:
return {
"product_role": "",
"time_horizon": "",
"success_criteria": "",
"non_goals": "",
"battlefield_one_line": "",
"positioning_choice": "",
"competitive_stance": "",
"pillar_product": "",
"pillar_price": "",
"pillar_channel": "",
"pillar_comm": "",
"audience_segment": "",
"competitor_reference": "",
"resource_notes": "",
"marketing_strategy": "",
"general_strategy": "",
"ack_risk_keywords": False,
"ack_risk_price": False,
"ack_risk_concentration": False,
}
def main() -> int:
import django
django.setup()
from django.utils import timezone
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,
)
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,
)
from pipeline.reporting.report_matrix_group_evidence import (
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,
filter_strategy_hints_for_ch8_probe,
report_uses_chapter8_text_mining_probe,
)
p = argparse.ArgumentParser()
p.add_argument("--job-id", type=int, default=None)
p.add_argument(
"--matrix-index",
type=int,
default=0,
help="矩阵分组下标;设为 -1 表示不收窄(全部分类)",
)
p.add_argument(
"--no-scope",
action="store_true",
help="与 --matrix-index -1 相同:不收窄 brief、不抽细类报告节选",
)
p.add_argument(
"-o",
"--output",
type=str,
default=None,
help="输出 .md 路径默认docs/planning/策略生成-LLM全量输入快照.md",
)
args = p.parse_args()
backend_dir = Path(__file__).resolve().parents[2]
repo_root = backend_dir.parent
default_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()
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
rc = job.report_config if isinstance(job.report_config, dict) else None
brief = build_competitor_brief_for_job(
job.run_dir,
job.keyword,
report_config=rc,
)
matrix_groups = list_matrix_groups_for_api(brief)
group_names = [g.get("group") for g in matrix_groups if isinstance(g, dict)]
scoped_label = ""
matrix_index: int | None = args.matrix_index
if args.no_scope:
matrix_index = -1
if matrix_index is not None and matrix_index >= 0:
mg = brief.get("matrix_by_group")
if isinstance(mg, list) and matrix_index < len(mg):
scoped_label = (mg[matrix_index].get("group") or "").strip()
brief = filter_brief_for_strategy_matrix_group(
brief, matrix_group_index=matrix_index
)
else:
print(f"matrix_index {matrix_index} 超出范围", file=sys.stderr)
return 1
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,
)
excerpt_raw, excerpt_src = load_report_strategy_excerpt(job.run_dir)
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,
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)
if isinstance(compact.get("strategy_hints"), list):
compact["strategy_hints"] = filter_strategy_hints_for_ch8_probe(
compact["strategy_hints"]
)
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: 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 句子」,避免与报告 §8 主口径冲突。**不得**再以这类子串计数或预设场景占比作为论据。"
"用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。"
)
user_body = STRATEGY_USER_PREFIX + json.dumps(payload, ensure_ascii=False)
full_chars = len(STRATEGY_SYSTEM) + len(user_body)
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` 代入即与线上等价。",
"",
"## 快照元数据",
"",
f"- **任务 ID**{job.id}",
f"- **关键词**{job.keyword}",
f"- **run_dir**`{job.run_dir}`",
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"- **System 字符数**{len(STRATEGY_SYSTEM)}",
f"- **User 消息字符数**{len(user_body)}",
f"- **合计约**{full_chars} 字符",
"",
"---",
"",
"## 1. System 提示词(完整 `STRATEGY_SYSTEM`",
"",
"```text",
STRATEGY_SYSTEM,
"```",
"",
"---",
"",
"## 2. User 消息(完整:`STRATEGY_USER_PREFIX` + JSON",
"",
"以下为网关 **user** 角色一次发送的完整字符串(前缀 + 单行 JSON",
"",
"```text",
user_body,
"```",
"",
"---",
"",
"## 3. 同上 JSON 的排版版(便于人眼查看 `structured_brief` 结构)",
"",
"说明:若与第 2 节有任何不一致,以第 2 节(真实入参)为准。",
"",
"```json",
json.dumps(payload, ensure_ascii=False, indent=2),
"```",
"",
]
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"Wrote {out_path} ({out_path.stat().st_size // 1024} KB)")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -13,6 +13,39 @@ 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
def _omit_ch8_probe_wordchart_fields(compact: dict[str, Any]) -> None:
"""
第八章文本挖掘探针为主时去掉与**预设关注词/场景条形图**同源的统计字段
避免与报告 §8 文本挖掘主口径两用数据
仅影响传入大模型的 ``structured_brief````brief`` 全量仍可由规则稿使用
"""
for k in (
"comment_focus_keywords",
"usage_scenarios",
"usage_scenarios_denominator",
"usage_scenarios_by_matrix_group",
):
compact.pop(k, None)
cfb = compact.get("consumer_feedback_by_matrix_group")
if not isinstance(cfb, list):
return
slim: list[Any] = []
for g in cfb:
if not isinstance(g, dict):
slim.append(g)
continue
slim.append(
{
k: v
for k, v in g.items()
if k not in ("focus_keyword_hits", "scenarios_top")
}
)
compact["consumer_feedback_by_matrix_group"] = slim
STRATEGY_DATA_RULES = """**全局禁止编造(适用于输出全文各节、各表、各段;独立策略稿与报告第九章策略归纳**共用**本段,硬性)** STRATEGY_DATA_RULES = """**全局禁止编造(适用于输出全文各节、各表、各段;独立策略稿与报告第九章策略归纳**共用**本段,硬性)**
- **事实与数字**销量GMV占比价带条数份额券面额满减/满折门槛到手价店铺/品牌计数与排名SKU 接口返回量等**仅可**来自**本次调用输入 JSON** 中已给出的字段策略稿为 `structured_brief``rules_draft_markdown` 内摘录`report_strategy_excerpt`可选 **`report_matrix_group_evidence_md`**与同任务报告第五第八章细类大模型小节同源`strategy_decisions``business_notes`第九章嵌入为 `competitor_brief`可选 `prior_chapter_llm_narratives`**禁止**凭空新增改口径或写成已监测证实而无字段支撑 - **事实与数字**销量GMV占比价带条数份额券面额满减/满折门槛到手价店铺/品牌计数与排名SKU 接口返回量等**仅可**来自**本次调用输入 JSON** 中已给出的字段策略稿为 `structured_brief``rules_draft_markdown` 内摘录`report_strategy_excerpt`可选 **`report_matrix_group_evidence_md`**与同任务报告第五第八章细类大模型小节同源`strategy_decisions``business_notes`第九章嵌入为 `competitor_brief`可选 `prior_chapter_llm_narratives`**禁止**凭空新增改口径或写成已监测证实而无字段支撑
- **主体与名称****禁止**引入上述输入中**未出现****具体**品牌名店铺名SKU 商品标题作为**事实陈述** `strategy_decisions`/备注/brief/节选已含则可写否则用头部/同类竞品等泛称或待业务指定对标 - **主体与名称****禁止**引入上述输入中**未出现****具体**品牌名店铺名SKU 商品标题作为**事实陈述** `strategy_decisions`/备注/brief/节选已含则可写否则用头部/同类竞品等泛称或待业务指定对标
@ -153,13 +186,7 @@ def generate_strategy_draft_markdown_llm(
compact = compact_brief_for_llm(brief, max_chars=compact_max) compact = compact_brief_for_llm(brief, max_chars=compact_max)
if report_uses_chapter8_text_mining_probe(report_config): if report_uses_chapter8_text_mining_probe(report_config):
compact = dict(compact) compact = dict(compact)
for k in ( _omit_ch8_probe_wordchart_fields(compact)
"comment_focus_keywords",
"usage_scenarios",
"usage_scenarios_denominator",
"usage_scenarios_by_matrix_group",
):
compact.pop(k, None)
if isinstance(compact.get("strategy_hints"), list): if isinstance(compact.get("strategy_hints"), list):
compact["strategy_hints"] = filter_strategy_hints_for_ch8_probe( compact["strategy_hints"] = filter_strategy_hints_for_ch8_probe(
compact["strategy_hints"] compact["strategy_hints"]
@ -195,9 +222,9 @@ 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"] = (
"已启用第八章文本挖掘探针为主structured_brief 已省略「关注词/场景子串计数」及「与条形图同源的 strategy_hints 句子」" "已启用第八章文本挖掘探针为主structured_brief 已省略顶层「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top"
"避免与报告 §8 主口径冲突。**不得**再以词频或预设场景占比作为论据。" "以及「与条形图同源的 strategy_hints 句子」,避免与报告 §8 主口径冲突。**不得**再以这类子串计数或预设场景占比作为论据。"
"用户与评论侧须依报告 §8 文本挖掘归纳**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。" "用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。"
) )
raw = json.dumps(payload, ensure_ascii=False) raw = json.dumps(payload, ensure_ascii=False)
if len(raw) > 500_000: if len(raw) > 500_000:

View File

@ -3,6 +3,7 @@ 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.reporting.strategy_draft import build_strategy_draft_markdown from pipeline.reporting.strategy_draft import build_strategy_draft_markdown
@ -163,3 +164,24 @@ class StrategyDraftTests(SimpleTestCase):
self.assertIn("样本内品牌较分散", md) self.assertIn("样本内品牌较分散", md)
self.assertIn("price_promotion_signals", md) self.assertIn("price_promotion_signals", md)
self.assertIn("price_promotion_signals", md) self.assertIn("price_promotion_signals", md)
def test_ch8_probe_omit_wordchart_nested_in_consumer_feedback(self) -> None:
compact = {
"comment_focus_keywords": [{"word": "x", "count": 1}],
"usage_scenarios": [],
"consumer_feedback_by_matrix_group": [
{
"group": "饼干",
"comment_rows": 10,
"focus_keyword_hits": [{"word": "口感", "count": 5}],
"scenarios_top": [{"scenario": "早餐", "count": 2}],
}
],
}
_omit_ch8_probe_wordchart_fields(compact)
self.assertNotIn("comment_focus_keywords", compact)
self.assertNotIn("focus_keyword_hits", compact["consumer_feedback_by_matrix_group"][0])
self.assertNotIn("scenarios_top", compact["consumer_feedback_by_matrix_group"][0])
self.assertEqual(
compact["consumer_feedback_by_matrix_group"][0].get("comment_rows"), 10
)

File diff suppressed because one or more lines are too long