feat(pipeline): 策略 LLM 并入宿主报告按细类的大模型归纳节选

从 competitor_analysis.md 抽取 #### 细类名 下正文;收窄矩阵分组时传入 report_matrix_group_evidence_md,并在系统提示中约定与 structured_brief 及第九章节选的分工。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-20 17:55:50 +08:00
parent 6143ad6180
commit 36ad21de6e
4 changed files with 172 additions and 2 deletions

View File

@ -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)
),

View 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",
]

View 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", "饼干"),
[],
)

View File

@ -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)