fix(pipeline): 策略节选纳入 8.3 情感归纳嵌套四级标题

load_report_matrix_group_evidence 先剔除 8.3 再按 #### 细类抽取,避免嵌套 #### 正向体验主题等导致正文为空;单独解析 8.3 并拼入摘录。补充单测。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-23 09:57:05 +08:00
parent 991dec3c4b
commit 6c37a2def2
2 changed files with 135 additions and 24 deletions

View File

@ -4,8 +4,10 @@
报告生成侧约定矩阵/价盘/促销/评论/场景等 LLM 小节均以 ``#### `` + 与矩阵一致的细类名为小节标题 报告生成侧约定矩阵/价盘/促销/评论/场景等 LLM 小节均以 ``#### `` + 与矩阵一致的细类名为小节标题
``generate_group_summaries`` 系统提示 ``generate_group_summaries`` 系统提示
**8.3 评价正/负向主题**细类下内层小节使用 ``#####``(由 ``demote_sentiment_inner_h4_to_h5_for_matrix_group`` 处理),
避免与外层 ``#### 细类名`` 同级导致本节正文抽取为空。 **第八章 8.3**``generate_comment_sentiment_analysis_llm``在每组 ``#### {细类}`` 下还会再嵌套
``#### 正向体验主题`` 等四级标题(见 ``generate_sections.SENTIMENT_LLM_SYSTEM``
通用抽取在遇到下一行 ``####`` 时即结束,会把 8.3 正文误判为空;故 8.3 单独解析后再拼回。
""" """
from __future__ import annotations from __future__ import annotations
@ -13,6 +15,77 @@ import re
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
# 与 ``jd_report.build_competitor_markdown`` 中 8.3 小节标题一致。
_SENTIMENT_83_HEADING = "### 8.3 评价正/负向主题(按细类 · 大模型)"
# 与 ``generate_sections.SENTIMENT_LLM_SYSTEM``「建议结构」四级标题一致;嵌套于 8.3 每组 ``#### 细类`` 之下。
_SENTIMENT_LLM_INNER_LEVEL4 = frozenset(
{
"正向体验主题",
"负向评价主题归因",
"混合评价中的典型张力",
"使用注意",
}
)
def _md_splice_out_sentiment_83_section(md: str) -> str:
"""去掉 8.3 整节,避免按细类抽取时把嵌套 ``####`` 误判为同级边界;其它章不变。"""
i = md.find(_SENTIMENT_83_HEADING)
if i < 0:
return md
tail = md[i + len(_SENTIMENT_83_HEADING) :]
m = re.search(r"^##\s+", tail, re.MULTILINE)
if not m:
return md[:i].rstrip() + "\n\n"
cut = i + len(_SENTIMENT_83_HEADING) + m.start()
return md[:i].rstrip() + "\n\n" + md[cut:].lstrip("\n")
def extract_sentiment_83_level4_body(md: str, group_title: str) -> str:
"""
仅在 ``### 8.3 …`` 节内,抽取 ``#### {group_title}`` 下正文(**不含**该标题行)。
允许正文内出现 ``#### 正向体验主题`` 等情感归纳子标题;遇下一 peer ``####``(另一细类)或 ``###``/``##`` 则结束。
"""
title = (group_title or "").strip()
if not title or not (md or "").strip():
return ""
i = md.find(_SENTIMENT_83_HEADING)
if i < 0:
return ""
tail = md[i + len(_SENTIMENT_83_HEADING) :]
m_end = re.search(r"^##\s+", tail, re.MULTILINE)
chunk = tail[: m_end.start()] if m_end else tail
lines = chunk.splitlines()
n = len(lines)
j = 0
while j < n:
line = lines[j]
m4 = re.match(r"^####\s+(.+?)\s*$", line)
if m4 and m4.group(1).strip() == title:
j += 1
body_lines: list[str] = []
while j < n:
nxt = lines[j]
mpeer = re.match(r"^####\s+(.+?)\s*$", nxt)
if mpeer:
inner = mpeer.group(1).strip()
if inner in _SENTIMENT_LLM_INNER_LEVEL4:
body_lines.append(nxt)
j += 1
continue
break
if re.match(r"^###\s", nxt) or re.match(r"^##\s", nxt):
break
body_lines.append(nxt)
j += 1
return "\n".join(body_lines).strip()
j += 1
return ""
def extract_level4_sections_by_group_title(md: str, group_title: str) -> list[str]: def extract_level4_sections_by_group_title(md: str, group_title: str) -> list[str]:
""" """
@ -72,14 +145,19 @@ def load_report_matrix_group_evidence_markdown(
except OSError: except OSError:
return "", "none" return "", "none"
parts = extract_level4_sections_by_group_title(full, group_title) without_83 = _md_splice_out_sentiment_83_section(full)
parts = extract_level4_sections_by_group_title(without_83, group_title)
s83 = extract_sentiment_83_level4_body(full, group_title)
if s83:
parts.append(s83)
if not parts: if not parts:
return "", "none" return "", "none"
intro = ( intro = (
f"> **说明**:以下为同任务《竞品分析报告》正文中、细类「**{group_title.strip()}**」下 " f"> **说明**:以下为同任务《竞品分析报告》正文中、细类「**{group_title.strip()}**」下 "
"「#### …」小节的**大模型归纳**摘录(按正文出现顺序拼接)," "「#### …」小节的**大模型归纳**摘录(按正文出现顺序拼接),"
"覆盖矩阵/价盘/促销/评论与场景等块中**已生成**的段落;" "覆盖矩阵/价盘/促销/评论与场景等块中**已生成**的段落,"
"并含 **§8.3 评价正/负向主题(按细类 · 大模型)** 内该细类段落(允许嵌套四级小标题);"
"若某块未开 LLM 或未产出对应小节,则不会出现在此摘录中。\n\n" "若某块未开 LLM 或未产出对应小节,则不会出现在此摘录中。\n\n"
) )
sep = "\n\n---\n\n" sep = "\n\n---\n\n"
@ -93,5 +171,6 @@ def load_report_matrix_group_evidence_markdown(
__all__ = [ __all__ = [
"extract_level4_sections_by_group_title", "extract_level4_sections_by_group_title",
"extract_sentiment_83_level4_body",
"load_report_matrix_group_evidence_markdown", "load_report_matrix_group_evidence_markdown",
] ]

View File

@ -1,11 +1,15 @@
"""从宿主报告 MD 按细类抽取大模型小节。""" """从宿主报告 MD 按细类抽取大模型小节。"""
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from tempfile import TemporaryDirectory
from django.test import SimpleTestCase from django.test import SimpleTestCase
from pipeline.llm.generate_sections import demote_sentiment_inner_h4_to_h5_for_matrix_group
from pipeline.reporting.report_matrix_group_evidence import ( from pipeline.reporting.report_matrix_group_evidence import (
extract_level4_sections_by_group_title, extract_level4_sections_by_group_title,
extract_sentiment_83_level4_body,
load_report_matrix_group_evidence_markdown,
) )
@ -39,23 +43,51 @@ B 段评论归纳。
[], [],
) )
def test_extract_8_3_sentiment_when_inner_subsections_are_h5(self) -> None: def test_sentiment_83_nested_level4(self) -> None:
"""8.3 在 #### 细类 下须用 ##### 子标题,否则抽取在首个 #### 子节处断开。""" md = """## 八、消费者反馈
inner = demote_sentiment_inner_h4_to_h5_for_matrix_group(
"#### 正向体验主题\n\n正文A\n\n#### 负向评价主题归因\n\n正文B\n"
)
md = (
"### 8.3 评价正/负向主题\n\n"
"#### 饼干\n\n"
f"{inner}\n"
)
parts = extract_level4_sections_by_group_title(md, "饼干")
self.assertEqual(len(parts), 1)
self.assertIn("正文A", parts[0])
self.assertIn("正文B", parts[0])
def test_demote_known_h4_titles_only(self) -> None: ### 8.3 评价正/负向主题(按细类 · 大模型)
raw = "#### 正向体验主题\nx\n#### 饼干\ny\n"
out = demote_sentiment_inner_h4_to_h5_for_matrix_group(raw) > 说明
self.assertIn("##### 正向体验主题", out)
self.assertIn("#### 饼干", out) #### 饼干
#### 正向体验主题
酥脆好评
#### 负向评价主题归因
略贵
#### 西式糕点
#### 正向体验主题
别的细类
## 九、策略
"""
body = extract_sentiment_83_level4_body(md, "饼干")
self.assertIn("酥脆好评", body)
self.assertIn("正向体验主题", body)
self.assertIn("略贵", body)
self.assertNotIn("别的细类", body)
def test_load_includes_83_after_splice(self) -> None:
md = """#### 饼干
矩阵段
### 8.3 评价正/负向主题(按细类 · 大模型)
#### 饼干
#### 正向体验主题
情感段
## 九、策略
"""
with TemporaryDirectory() as td:
p = Path(td) / "competitor_analysis.md"
p.write_text(md, encoding="utf-8")
out, src = load_report_matrix_group_evidence_markdown(td, "饼干")
self.assertEqual(src, "competitor_analysis_md")
self.assertIn("矩阵段", out)
self.assertIn("情感段", out)