fix(strategy): 文本挖掘开启时策略稿不再依赖关注词/场景计数

报告配置 chapter8_text_mining_probe 为 True(默认)时,规则底稿第五节改为说明口径并去掉子串命中列表;LLM 请求从 structured_brief 剔除相关字段并附 omission_note。关闭探针时保留旧版第五节。任务 API 传入 report_config。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-17 16:56:08 +08:00
parent f06fd12f1f
commit 932c3d6aeb
4 changed files with 125 additions and 27 deletions

View File

@ -6,7 +6,10 @@ import os
from typing import Any
from ..reporting.brief_compact import compact_brief_for_llm
from ..reporting.strategy_draft import build_strategy_draft_markdown
from ..reporting.strategy_draft import (
build_strategy_draft_markdown,
report_uses_chapter8_text_mining_probe,
)
from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window_size
STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要**与业务侧填写的**决策字段**,把「规则底稿」润色为可读的策略 Markdown。
@ -33,6 +36,7 @@ def generate_strategy_draft_markdown_llm(
generated_at_iso: str,
strategy_decisions: dict[str, Any],
report_strategy_excerpt: str | None = None,
report_config: dict[str, Any] | None = None,
) -> str:
"""
``report_strategy_excerpt``与同任务宿主报告第九章策略与机会正文对齐的节选
@ -45,6 +49,7 @@ def generate_strategy_draft_markdown_llm(
business_notes=business_notes,
generated_at_iso=generated_at_iso,
strategy_decisions=strategy_decisions,
report_config=report_config,
)
excerpt_raw = (report_strategy_excerpt or "").strip()
sys_prompt = STRATEGY_SYSTEM
@ -58,6 +63,15 @@ def generate_strategy_draft_markdown_llm(
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)
for k in (
"comment_focus_keywords",
"usage_scenarios",
"usage_scenarios_denominator",
"usage_scenarios_by_matrix_group",
):
compact.pop(k, None)
ex = (
_truncate_strategy_narrative(excerpt_raw, excerpt_max)
if excerpt_raw
@ -77,6 +91,11 @@ def generate_strategy_draft_markdown_llm(
"rules_draft_markdown": rd,
"report_strategy_excerpt": ex,
}
if report_uses_chapter8_text_mining_probe(report_config):
payload["structured_brief_omission_note"] = (
"已启用第八章文本挖掘:本 JSON 中的 structured_brief 已省略关注词/场景子串计数字段,"
"避免与当前报告正文口径冲突;请依规则底稿第五节说明与报告 §8 自拟假设,勿逐条编造子串命中列表。"
)
raw = json.dumps(payload, ensure_ascii=False)
if len(raw) > 500_000:
payload["rules_draft_markdown"] = _truncate_rules_draft_md(rd, 200_000)

View File

@ -98,6 +98,19 @@ def _risk_line(checked: bool, text: str) -> str:
return f"- {mark} {text}"
def report_uses_chapter8_text_mining_probe(report_config: dict[str, Any] | None) -> bool:
"""
与任务 ``report_config`` ``chapter8_text_mining_probe`` 一致未显式设置时默认 ``True``
``jd.runner.get_default_report_config`` 一致
``True`` 策略稿第五节不再逐条列举关注词/场景子串命中以免与当前报告正文口径冲突
"""
if not isinstance(report_config, dict):
return True
if "chapter8_text_mining_probe" in report_config:
return bool(report_config.get("chapter8_text_mining_probe"))
return True
def build_strategy_draft_markdown(
*,
job_id: int,
@ -106,8 +119,10 @@ def build_strategy_draft_markdown(
business_notes: str = "",
generated_at_iso: str = "",
strategy_decisions: dict[str, Any] | None = None,
report_config: dict[str, Any] | None = None,
) -> str:
"""生成可下载的 Markdown策略框架为主附录为数据速览。"""
use_ch8_probe = report_uses_chapter8_text_mining_probe(report_config)
d = strategy_decisions or {}
pos = _esc(d.get("positioning_choice") or "").strip()
kw = _esc(brief.get("keyword")) or _esc(keyword) or ""
@ -253,31 +268,41 @@ def build_strategy_draft_markdown(
ckw = brief.get("comment_focus_keywords") or []
usc = brief.get("usage_scenarios") or []
lines.extend(["## 五、用户需求与场景 — 可写成策略的假设", ""])
lines.append(
"*下列由关注词/场景**计数**转化而来,是「待验证假设」而非结论;请结合评价原文抽样修订。*"
)
lines.append("")
if ckw:
for item in ckw[:8]:
if isinstance(item, dict):
w = _esc(item.get("word"))
c = _num(item.get("count"))
lines.append(
f"- **假设**:用户决策中「{w}」被频繁提及(约 {c} 次统计命中)—— "
f"*可追问:本品故事是否正面回应?传播关键词是否覆盖?*"
)
if usc:
for item in usc[:6]:
if isinstance(item, dict):
sc = _esc(item.get("scenario"))
cn = _num(item.get("count"))
sh = _pct(item.get("share_of_text_units"))
lines.append(
f"- **场景命题**:「{sc}」在预设场景中约 {cn} 条、约占 {sh} 文本单元—— "
f"*可追问:主图/详情/客服话术是否对齐该场景?*"
)
if not ckw and not usc:
lines.append("*摘要中无关注词/场景组结果,请补全评论侧分析后再写本节。*")
if use_ch8_probe:
lines.extend(
[
"*当前任务报告以**第八章评论侧文本挖掘**为主线(替代原关注词/场景条形图为主呈现)时,**请勿**再逐条照搬「子串命中次数」式假设列表——结构化摘要里仍可能含 `comment_focus_keywords` / `usage_scenarios` 等字段,**与当前报告正文展示口径可能不一致**。*",
"",
"*请结合报告中 **§8** 评论与情感归纳、文本挖掘小节,以及评价原文抽样,自拟若干条「待验证假设」(需可复核、可证伪)。*",
"",
]
)
else:
lines.append(
"*下列由关注词/场景**计数**转化而来,是「待验证假设」而非结论;请结合评价原文抽样修订。*"
)
lines.append("")
if ckw:
for item in ckw[:8]:
if isinstance(item, dict):
w = _esc(item.get("word"))
c = _num(item.get("count"))
lines.append(
f"- **假设**:用户决策中「{w}」被频繁提及(约 {c} 次统计命中)—— "
f"*可追问:本品故事是否正面回应?传播关键词是否覆盖?*"
)
if usc:
for item in usc[:6]:
if isinstance(item, dict):
sc = _esc(item.get("scenario"))
cn = _num(item.get("count"))
sh = _pct(item.get("share_of_text_units"))
lines.append(
f"- **场景命题**:「{sc}」在预设场景中约 {cn} 条、约占 {sh} 文本单元—— "
f"*可追问:主图/详情/客服话术是否对齐该场景?*"
)
if not ckw and not usc:
lines.append("*摘要中无关注词/场景组结果,请补全评论侧分析后再写本节。*")
lines.append("")
hints = brief.get("strategy_hints") or []
@ -316,11 +341,16 @@ def build_strategy_draft_markdown(
rk = bool(d.get("ack_risk_keywords"))
rp = bool(d.get("ack_risk_price"))
rc = bool(d.get("ack_risk_concentration"))
rk_kw = (
"评论侧量化/关键词归纳是否**以偏概全**?(需原评论抽样)"
if use_ch8_probe
else "关注词/场景是否**以偏概全**?(需原评论抽样)"
)
lines.extend(
[
"## 七、风险与待证伪",
"",
_risk_line(rk, "关注词/场景是否**以偏概全**?(需原评论抽样)"),
_risk_line(rk, rk_kw),
_risk_line(rp, "价格带是否含大促/异常挂价?(需核对清洗规则)"),
_risk_line(rc, "列表集中度与深入样本品牌是否**矛盾**?(需解释渠道差异)"),
"",

View File

@ -45,6 +45,7 @@ class StrategyDraftTests(SimpleTestCase):
keyword="K",
brief=brief,
strategy_decisions=decisions,
report_config={"chapter8_text_mining_probe": False},
)
self.assertIn("**本品角色**:追赶型", md)
self.assertIn("- [x] **卡腰**", md)
@ -54,3 +55,48 @@ class StrategyDraftTests(SimpleTestCase):
self.assertIn("- [x] 关注词/场景是否**以偏概全**", md)
self.assertIn("- [ ] 价格带是否含大促", md)
self.assertIn("- [x] 列表集中度与深入样本品牌是否**矛盾**", md)
def test_chapter8_probe_omits_focus_scenario_count_bullets(self) -> None:
brief = {
"schema_version": 1,
"keyword": "低GI",
"comment_focus_keywords": [{"word": "口感", "count": 501}],
"usage_scenarios": [
{
"scenario": "控糖/血糖相关",
"count": 305,
"share_of_text_units": 0.272,
}
],
}
md = build_strategy_draft_markdown(
job_id=1,
keyword="低GI",
brief=brief,
report_config={"chapter8_text_mining_probe": True},
)
self.assertIn("文本挖掘", md)
self.assertNotIn("假设**:用户决策中「口感」", md)
self.assertNotIn("场景命题**:「控糖", md)
def test_legacy_report_shows_focus_scenario_bullets(self) -> None:
brief = {
"schema_version": 1,
"keyword": "低GI",
"comment_focus_keywords": [{"word": "口感", "count": 501}],
"usage_scenarios": [
{
"scenario": "控糖/血糖相关",
"count": 305,
"share_of_text_units": 0.272,
}
],
}
md = build_strategy_draft_markdown(
job_id=1,
keyword="低GI",
brief=brief,
report_config={"chapter8_text_mining_probe": False},
)
self.assertIn("假设**:用户决策中「口感」", md)
self.assertIn("场景命题**:「控糖/血糖相关」", md)

View File

@ -158,6 +158,7 @@ class JobStrategyDraftView(APIView):
report_excerpt, excerpt_src = load_report_strategy_excerpt(job.run_dir)
except OSError:
report_excerpt, excerpt_src = "", "none"
rc_job = job.report_config if isinstance(job.report_config, dict) else None
try:
if generator == "llm":
md = generate_strategy_draft_markdown_llm(
@ -168,6 +169,7 @@ class JobStrategyDraftView(APIView):
generated_at_iso=gen_at,
strategy_decisions=strategy_decisions,
report_strategy_excerpt=report_excerpt,
report_config=rc_job,
)
src = "llm_text_ai_crawler_v1"
else:
@ -178,6 +180,7 @@ class JobStrategyDraftView(APIView):
business_notes=notes,
generated_at_iso=gen_at,
strategy_decisions=strategy_decisions,
report_config=rc_job,
)
src = "structured_summary_rules_v1"
except ValueError as e: