From 36ad21de6eb73d7f89f9464de2ddc62f38e876a7 Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Mon, 20 Apr 2026 17:55:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(pipeline):=20=E7=AD=96=E7=95=A5=20LLM=20?= =?UTF-8?q?=E5=B9=B6=E5=85=A5=E5=AE=BF=E4=B8=BB=E6=8A=A5=E5=91=8A=E6=8C=89?= =?UTF-8?q?=E7=BB=86=E7=B1=BB=E7=9A=84=E5=A4=A7=E6=A8=A1=E5=9E=8B=E5=BD=92?= =?UTF-8?q?=E7=BA=B3=E8=8A=82=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从 competitor_analysis.md 抽取 #### 细类名 下正文;收窄矩阵分组时传入 report_matrix_group_evidence_md,并在系统提示中约定与 structured_brief 及第九章节选的分工。 Made-with: Cursor --- backend/pipeline/llm/generate_strategy.py | 21 +++- .../reporting/report_matrix_group_evidence.py | 95 +++++++++++++++++++ .../test_report_matrix_group_evidence.py | 39 ++++++++ backend/pipeline/views/job_report_views.py | 19 ++++ 4 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 backend/pipeline/reporting/report_matrix_group_evidence.py create mode 100644 backend/pipeline/tests/test_report_matrix_group_evidence.py diff --git a/backend/pipeline/llm/generate_strategy.py b/backend/pipeline/llm/generate_strategy.py index 6a4eeaf..43dbfb2 100644 --- a/backend/pipeline/llm/generate_strategy.py +++ b/backend/pipeline/llm/generate_strategy.py @@ -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) ), diff --git a/backend/pipeline/reporting/report_matrix_group_evidence.py b/backend/pipeline/reporting/report_matrix_group_evidence.py new file mode 100644 index 0000000..bf75156 --- /dev/null +++ b/backend/pipeline/reporting/report_matrix_group_evidence.py @@ -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", +] diff --git a/backend/pipeline/tests/test_report_matrix_group_evidence.py b/backend/pipeline/tests/test_report_matrix_group_evidence.py new file mode 100644 index 0000000..bf0e0c5 --- /dev/null +++ b/backend/pipeline/tests/test_report_matrix_group_evidence.py @@ -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", "饼干"), + [], + ) diff --git a/backend/pipeline/views/job_report_views.py b/backend/pipeline/views/job_report_views.py index a5b5201..71b975a 100644 --- a/backend/pipeline/views/job_report_views.py +++ b/backend/pipeline/views/job_report_views.py @@ -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)