mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-22 08:01:34 +08:00
feat(pipeline): 策略 LLM 并入宿主报告按细类的大模型归纳节选
从 competitor_analysis.md 抽取 #### 细类名 下正文;收窄矩阵分组时传入 report_matrix_group_evidence_md,并在系统提示中约定与 structured_brief 及第九章节选的分工。 Made-with: Cursor
This commit is contained in:
parent
6143ad6180
commit
36ad21de6e
@ -14,7 +14,7 @@ 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
|
||||||
|
|
||||||
STRATEGY_DATA_RULES = """**全局禁止编造(适用于输出全文各节、各表、各段;独立策略稿与报告第九章策略归纳**共用**本段,硬性)**:
|
STRATEGY_DATA_RULES = """**全局禁止编造(适用于输出全文各节、各表、各段;独立策略稿与报告第九章策略归纳**共用**本段,硬性)**:
|
||||||
- **事实与数字**:销量、GMV、占比、价带、条数、份额、券面额、满减/满折门槛、到手价、店铺/品牌计数与排名、SKU 数、接口返回量等,**仅可**来自**本次调用输入 JSON** 中已给出的字段(策略稿为 `structured_brief`、`rules_draft_markdown` 内摘录、`report_strategy_excerpt`、`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/节选已含则可写;否则用「头部/同类竞品」等泛称或「待业务指定对标」。
|
||||||
- **用户侧表述**:**禁止**虚构评价原文、访谈引语、带引号的「用户说…」;细则见下文「§2 针对痛点要怎么做」表**痛点简述**列。
|
- **用户侧表述**:**禁止**虚构评价原文、访谈引语、带引号的「用户说…」;细则见下文「§2 针对痛点要怎么做」表**痛点简述**列。
|
||||||
- **促销与活动**:**禁止**编造活动名、具体规则、补贴比例;细则见下文促销与第八章探针相关条款。
|
- **促销与活动**:**禁止**编造活动名、具体规则、补贴比例;细则见下文促销与第八章探针相关条款。
|
||||||
@ -44,7 +44,12 @@ STRATEGY_DATA_RULES = """**全局禁止编造(适用于输出全文各节、
|
|||||||
|
|
||||||
STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**与业务侧填写的**决策字段**,把「规则底稿」写成**短、可执行**的策略 Markdown **独立成稿**。
|
STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**与业务侧填写的**决策字段**,把「规则底稿」写成**短、可执行**的策略 Markdown **独立成稿**。
|
||||||
|
|
||||||
**输入**:`rules_draft_markdown`(规则骨架,**六主轴 + 品牌四线**结构,与 `docs/demo` 市场策略稿示例同构)、`structured_brief`、`strategy_decisions`、`business_notes`;可选 `report_strategy_excerpt`。
|
**输入**:`rules_draft_markdown`(规则骨架,**六主轴 + 品牌四线**结构,与 `docs/demo` 市场策略稿示例同构)、`structured_brief`、`strategy_decisions`、`business_notes`;可选 `report_strategy_excerpt`;可选 **`report_matrix_group_evidence_md`**(与所选细类对齐的宿主报告大模型归纳摘录)。
|
||||||
|
|
||||||
|
**与细类收窄配套(当 JSON 含 `report_matrix_group_evidence_md` 且非空时,硬性)**:
|
||||||
|
- **定性主题**(用户讨论焦点、卖点/配料叙事、负向体验类型、场景与关注词归纳方向等)须与该节选及 `structured_brief` **方向一致**,**禁止**另写一套与节选**明显矛盾**的品类判断。
|
||||||
|
- **数字、份额、价带、条数**仍以 **`structured_brief` 为准**;节选与 brief 数字冲突时**采纳 brief**,勿复述冲突数字句。
|
||||||
|
- **`report_strategy_excerpt`(第九章)** 为**全关键词任务**下的策略归纳,可能与「仅选某细类」并行存在:写**该细类**策略时以 `structured_brief` + `report_matrix_group_evidence_md` 为主;第九章仅作检索池整体方向参考,**不得**把全池结论套成该细类已证实事实。
|
||||||
|
|
||||||
{STRATEGY_DATA_RULES}
|
{STRATEGY_DATA_RULES}
|
||||||
|
|
||||||
@ -114,11 +119,15 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
generated_at_iso: str,
|
generated_at_iso: str,
|
||||||
strategy_decisions: dict[str, Any],
|
strategy_decisions: dict[str, Any],
|
||||||
report_strategy_excerpt: str | None = None,
|
report_strategy_excerpt: str | None = None,
|
||||||
|
report_matrix_group_evidence_md: str | None = None,
|
||||||
report_config: dict[str, Any] | None = None,
|
report_config: dict[str, Any] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
``report_strategy_excerpt``:与同任务宿主报告第九章「策略与机会」正文对齐的节选(见
|
``report_strategy_excerpt``:与同任务宿主报告第九章「策略与机会」正文对齐的节选(见
|
||||||
``reporting.report_strategy_excerpt.load_report_strategy_excerpt``);空字符串表示未生成或未重跑第九章大模型。
|
``reporting.report_strategy_excerpt.load_report_strategy_excerpt``);空字符串表示未生成或未重跑第九章大模型。
|
||||||
|
|
||||||
|
``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(
|
rules_md = build_strategy_draft_markdown(
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
@ -130,6 +139,7 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
report_config=report_config,
|
report_config=report_config,
|
||||||
)
|
)
|
||||||
excerpt_raw = (report_strategy_excerpt or "").strip()
|
excerpt_raw = (report_strategy_excerpt or "").strip()
|
||||||
|
group_evidence_raw = (report_matrix_group_evidence_md or "").strip()
|
||||||
sys_prompt = STRATEGY_SYSTEM
|
sys_prompt = STRATEGY_SYSTEM
|
||||||
min_comp = _min_strategy_completion_tokens()
|
min_comp = _min_strategy_completion_tokens()
|
||||||
min_comp_relaxed = max(256, min_comp // 2)
|
min_comp_relaxed = max(256, min_comp // 2)
|
||||||
@ -159,6 +169,12 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
if excerpt_raw
|
if excerpt_raw
|
||||||
else ""
|
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:
|
if rules_max is None:
|
||||||
rd = rules_md
|
rd = rules_md
|
||||||
else:
|
else:
|
||||||
@ -172,6 +188,7 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
"structured_brief": compact,
|
"structured_brief": compact,
|
||||||
"rules_draft_markdown": rd,
|
"rules_draft_markdown": rd,
|
||||||
"report_strategy_excerpt": ex,
|
"report_strategy_excerpt": ex,
|
||||||
|
"report_matrix_group_evidence_md": gm,
|
||||||
"chapter8_text_mining_probe": bool(
|
"chapter8_text_mining_probe": bool(
|
||||||
report_uses_chapter8_text_mining_probe(report_config)
|
report_uses_chapter8_text_mining_probe(report_config)
|
||||||
),
|
),
|
||||||
|
|||||||
95
backend/pipeline/reporting/report_matrix_group_evidence.py
Normal file
95
backend/pipeline/reporting/report_matrix_group_evidence.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
从宿主报告 ``competitor_analysis.md`` 中按细类名抽取 **Markdown 四级标题** ``#### {细类名}``
|
||||||
|
下的正文,用于策略生成时并入「与同细类对齐」的大模型归纳(第五~第八章各块)。
|
||||||
|
|
||||||
|
报告生成侧约定:矩阵/价盘/促销/评论/场景等 LLM 小节均以 ``#### `` + 与矩阵一致的细类名为小节标题
|
||||||
|
(见 ``generate_group_summaries`` 系统提示)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
def extract_level4_sections_by_group_title(md: str, group_title: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
返回全文内所有 ``#### {group_title}`` 小节正文(不含标题行),按出现顺序。
|
||||||
|
标题须与 ``group_title`` 去首尾空白后**完全一致**。
|
||||||
|
"""
|
||||||
|
title = (group_title or "").strip()
|
||||||
|
if not title or not (md or "").strip():
|
||||||
|
return []
|
||||||
|
|
||||||
|
lines = md.splitlines()
|
||||||
|
n = len(lines)
|
||||||
|
blocks: list[str] = []
|
||||||
|
i = 0
|
||||||
|
while i < n:
|
||||||
|
line = lines[i]
|
||||||
|
m = re.match(r"^####\s+(.+?)\s*$", line)
|
||||||
|
if m and m.group(1).strip() == title:
|
||||||
|
i += 1
|
||||||
|
chunk: list[str] = []
|
||||||
|
while i < n:
|
||||||
|
nxt = lines[i]
|
||||||
|
if re.match(r"^####\s", nxt):
|
||||||
|
break
|
||||||
|
if re.match(r"^###\s", nxt) or re.match(r"^##\s", nxt):
|
||||||
|
break
|
||||||
|
if re.match(r"^#\s", nxt) and not nxt.startswith("##"):
|
||||||
|
break
|
||||||
|
chunk.append(nxt)
|
||||||
|
i += 1
|
||||||
|
body = "\n".join(chunk).strip()
|
||||||
|
if body:
|
||||||
|
blocks.append(body)
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def load_report_matrix_group_evidence_markdown(
|
||||||
|
run_dir: Path | str,
|
||||||
|
group_title: str,
|
||||||
|
*,
|
||||||
|
max_chars: int = 28_000,
|
||||||
|
) -> tuple[str, Literal["competitor_analysis_md", "none"]]:
|
||||||
|
"""
|
||||||
|
读取 ``run_dir/competitor_analysis.md``,抽取该细类在各章大模型小节下的归纳,拼接为一段 Markdown。
|
||||||
|
|
||||||
|
若文件不存在或无任何匹配小节,返回 ``("", "none")``。
|
||||||
|
"""
|
||||||
|
root = Path(run_dir)
|
||||||
|
path = root / "competitor_analysis.md"
|
||||||
|
cap = max(512, int(max_chars))
|
||||||
|
if not path.is_file():
|
||||||
|
return "", "none"
|
||||||
|
try:
|
||||||
|
full = path.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
return "", "none"
|
||||||
|
|
||||||
|
parts = extract_level4_sections_by_group_title(full, group_title)
|
||||||
|
if not parts:
|
||||||
|
return "", "none"
|
||||||
|
|
||||||
|
intro = (
|
||||||
|
f"> **说明**:以下为同任务《竞品分析报告》正文中、细类「**{group_title.strip()}**」下 "
|
||||||
|
"「#### …」小节的**大模型归纳**摘录(按正文出现顺序拼接),"
|
||||||
|
"覆盖矩阵/价盘/促销/评论与场景等块中**已生成**的段落;"
|
||||||
|
"若某块未开 LLM 或未产出对应小节,则不会出现在此摘录中。\n\n"
|
||||||
|
)
|
||||||
|
sep = "\n\n---\n\n"
|
||||||
|
body = intro + sep.join(parts)
|
||||||
|
if len(body) <= cap:
|
||||||
|
return body, "competitor_analysis_md"
|
||||||
|
tail = "\n\n…(已截断)\n"
|
||||||
|
room = max(400, cap - len(tail))
|
||||||
|
return body[: room].rstrip() + tail, "competitor_analysis_md"
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"extract_level4_sections_by_group_title",
|
||||||
|
"load_report_matrix_group_evidence_markdown",
|
||||||
|
]
|
||||||
39
backend/pipeline/tests/test_report_matrix_group_evidence.py
Normal file
39
backend/pipeline/tests/test_report_matrix_group_evidence.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""从宿主报告 MD 按细类抽取大模型小节。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from pipeline.reporting.report_matrix_group_evidence import (
|
||||||
|
extract_level4_sections_by_group_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportMatrixGroupEvidenceTests(SimpleTestCase):
|
||||||
|
def test_extract_multiple_blocks_in_order(self) -> None:
|
||||||
|
md = """#### 细类要点归纳(大模型)
|
||||||
|
|
||||||
|
> 说明
|
||||||
|
|
||||||
|
#### 饼干
|
||||||
|
A 段矩阵归纳。
|
||||||
|
|
||||||
|
#### 饮料
|
||||||
|
别的细类。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 细类评价与关注词要点归纳(大模型)
|
||||||
|
|
||||||
|
#### 饼干
|
||||||
|
B 段评论归纳。
|
||||||
|
"""
|
||||||
|
parts = extract_level4_sections_by_group_title(md, "饼干")
|
||||||
|
self.assertEqual(len(parts), 2)
|
||||||
|
self.assertIn("A 段矩阵归纳", parts[0])
|
||||||
|
self.assertIn("B 段评论归纳", parts[1])
|
||||||
|
|
||||||
|
def test_no_match(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
extract_level4_sections_by_group_title("## 二\n", "饼干"),
|
||||||
|
[],
|
||||||
|
)
|
||||||
@ -28,6 +28,9 @@ from ..reporting.brief_strategy_scope import (
|
|||||||
resolve_strategy_matrix_group_index,
|
resolve_strategy_matrix_group_index,
|
||||||
)
|
)
|
||||||
from ..reporting.md_document_export import markdown_to_docx_bytes, markdown_to_pdf_bytes
|
from ..reporting.md_document_export import markdown_to_docx_bytes, markdown_to_pdf_bytes
|
||||||
|
from ..reporting.report_matrix_group_evidence import (
|
||||||
|
load_report_matrix_group_evidence_markdown,
|
||||||
|
)
|
||||||
from ..reporting.report_strategy_excerpt import load_report_strategy_excerpt
|
from ..reporting.report_strategy_excerpt import load_report_strategy_excerpt
|
||||||
from ..reporting.strategy_draft import build_strategy_draft_markdown
|
from ..reporting.strategy_draft import build_strategy_draft_markdown
|
||||||
from ..serializers import PipelineJobSerializer, StrategyDraftRequestSerializer
|
from ..serializers import PipelineJobSerializer, StrategyDraftRequestSerializer
|
||||||
@ -184,6 +187,18 @@ class JobStrategyDraftView(APIView):
|
|||||||
raw_sa if isinstance(raw_sa, dict) else None
|
raw_sa if isinstance(raw_sa, dict) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
report_matrix_evidence_md = ""
|
||||||
|
report_matrix_evidence_src = "none"
|
||||||
|
if scope_idx is not None and 0 <= scope_idx < len(matrix_groups):
|
||||||
|
gnm = (matrix_groups[scope_idx].get("group") or "").strip()
|
||||||
|
if gnm:
|
||||||
|
report_matrix_evidence_md, report_matrix_evidence_src = (
|
||||||
|
load_report_matrix_group_evidence_markdown(
|
||||||
|
job.run_dir,
|
||||||
|
gnm,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
gen_at = timezone.now().isoformat()
|
gen_at = timezone.now().isoformat()
|
||||||
generator = (vd.get("generator") or "rules").strip()
|
generator = (vd.get("generator") or "rules").strip()
|
||||||
excerpt_src = "none"
|
excerpt_src = "none"
|
||||||
@ -203,6 +218,8 @@ class JobStrategyDraftView(APIView):
|
|||||||
generated_at_iso=gen_at,
|
generated_at_iso=gen_at,
|
||||||
strategy_decisions=strategy_decisions,
|
strategy_decisions=strategy_decisions,
|
||||||
report_strategy_excerpt=report_excerpt,
|
report_strategy_excerpt=report_excerpt,
|
||||||
|
report_matrix_group_evidence_md=report_matrix_evidence_md
|
||||||
|
or None,
|
||||||
report_config=rc_job,
|
report_config=rc_job,
|
||||||
)
|
)
|
||||||
src = "llm_text_ai_crawler_v1"
|
src = "llm_text_ai_crawler_v1"
|
||||||
@ -235,6 +252,8 @@ class JobStrategyDraftView(APIView):
|
|||||||
"report_strategy_excerpt_chars": len(report_excerpt or ""),
|
"report_strategy_excerpt_chars": len(report_excerpt or ""),
|
||||||
"matrix_groups": matrix_groups,
|
"matrix_groups": matrix_groups,
|
||||||
"strategy_scope_applied": strategy_scope_applied,
|
"strategy_scope_applied": strategy_scope_applied,
|
||||||
|
"report_matrix_group_evidence_source": report_matrix_evidence_src,
|
||||||
|
"report_matrix_group_evidence_chars": len(report_matrix_evidence_md or ""),
|
||||||
}
|
}
|
||||||
return Response(body)
|
return Response(body)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user