mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
fix(strategy): 文本挖掘开启时策略稿不再依赖关注词/场景计数
报告配置 chapter8_text_mining_probe 为 True(默认)时,规则底稿第五节改为说明口径并去掉子串命中列表;LLM 请求从 structured_brief 剔除相关字段并附 omission_note。关闭探针时保留旧版第五节。任务 API 传入 report_config。 Made-with: Cursor
This commit is contained in:
parent
f06fd12f1f
commit
932c3d6aeb
@ -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)
|
||||
|
||||
@ -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, "列表集中度与深入样本品牌是否**矛盾**?(需解释渠道差异)"),
|
||||
"",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user