mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +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
|
||||
|
||||
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/节选已含则可写;否则用「头部/同类竞品」等泛称或「待业务指定对标」。
|
||||
- **用户侧表述**:**禁止**虚构评价原文、访谈引语、带引号的「用户说…」;细则见下文「§2 针对痛点要怎么做」表**痛点简述**列。
|
||||
- **促销与活动**:**禁止**编造活动名、具体规则、补贴比例;细则见下文促销与第八章探针相关条款。
|
||||
@ -44,7 +44,12 @@ STRATEGY_DATA_RULES = """**全局禁止编造(适用于输出全文各节、
|
||||
|
||||
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}
|
||||
|
||||
@ -114,11 +119,15 @@ def generate_strategy_draft_markdown_llm(
|
||||
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,
|
||||
) -> str:
|
||||
"""
|
||||
``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(
|
||||
job_id=job_id,
|
||||
@ -130,6 +139,7 @@ def generate_strategy_draft_markdown_llm(
|
||||
report_config=report_config,
|
||||
)
|
||||
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)
|
||||
@ -159,6 +169,12 @@ def generate_strategy_draft_markdown_llm(
|
||||
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:
|
||||
@ -172,6 +188,7 @@ def generate_strategy_draft_markdown_llm(
|
||||
"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)
|
||||
),
|
||||
|
||||
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,
|
||||
)
|
||||
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.strategy_draft import build_strategy_draft_markdown
|
||||
from ..serializers import PipelineJobSerializer, StrategyDraftRequestSerializer
|
||||
@ -184,6 +187,18 @@ class JobStrategyDraftView(APIView):
|
||||
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()
|
||||
generator = (vd.get("generator") or "rules").strip()
|
||||
excerpt_src = "none"
|
||||
@ -203,6 +218,8 @@ class JobStrategyDraftView(APIView):
|
||||
generated_at_iso=gen_at,
|
||||
strategy_decisions=strategy_decisions,
|
||||
report_strategy_excerpt=report_excerpt,
|
||||
report_matrix_group_evidence_md=report_matrix_evidence_md
|
||||
or None,
|
||||
report_config=rc_job,
|
||||
)
|
||||
src = "llm_text_ai_crawler_v1"
|
||||
@ -235,6 +252,8 @@ class JobStrategyDraftView(APIView):
|
||||
"report_strategy_excerpt_chars": len(report_excerpt or ""),
|
||||
"matrix_groups": matrix_groups,
|
||||
"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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user