feat(pipeline): remove llm section bridges from report generation

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-14 15:48:23 +08:00
parent c244a2902e
commit f994555f9f
7 changed files with 7 additions and 114 deletions

View File

@ -33,6 +33,3 @@ CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
# MA_SKIP_LLM_KEYWORD_SUGGEST=1 # MA_SKIP_LLM_KEYWORD_SUGGEST=1
# MA_ENABLE_LLM_COMMENT_SENTIMENT=1 # MA_ENABLE_LLM_COMMENT_SENTIMENT=1
# MA_SKIP_LLM_COMMENT_SENTIMENT=1 # MA_SKIP_LLM_COMMENT_SENTIMENT=1
# 各章标题后插入大模型「衔接分析」(任务 report_config.llm_section_bridges 或本项设为 1
# MA_ENABLE_LLM_SECTION_BRIDGES=1
# MA_SKIP_LLM_SECTION_BRIDGES=1

View File

@ -1787,7 +1787,6 @@ def build_competitor_markdown(
"- **用途/场景**:对每条评价独立判断是否命中预设场景词;一条可计入多个场景,统计的是「提及该场景的评价条数」而非用户数。", "- **用途/场景**:对每条评价独立判断是否命中预设场景词;一条可计入多个场景,统计的是「提及该场景的评价条数」而非用户数。",
"- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;关注词与场景**仅按细类**以条形图展示(场景图为**占该细类有效文本比例 %**);见 §8.38.4。", "- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;关注词与场景**仅按细类**以条形图展示(场景图为**占该细类有效文本比例 %**);见 §8.38.4。",
"- **细类划分§5§8****仅**依据合并表 ``detail_category_path``;该列为空或无法解析出可读细类段的 SKU **不参与**竞品矩阵与按细类评价统计(相关评价条亦**不进入**按细类图表)。", "- **细类划分§5§8****仅**依据合并表 ``detail_category_path``;该列为空或无法解析出可读细类段的 SKU **不参与**竞品矩阵与按细类评价统计(相关评价条亦**不进入**按细类图表)。",
"- **各章衔接(可选)**:若任务配置 ``llm_section_bridges``(或部署侧环境变量启用),则在「## 一」至「## 九」各章二级标题后插入大模型撰写的**衔接分析**段落,便于阅读过渡;**定量结论仍以正文表格与摘要 JSON 为准**。",
"- **检索结果规模**:来自京东 PC 搜索返回的「结果条数」类指标,表示平台侧申报的匹配数量级,**不等于**动销、库存或独立 SKU 数。", "- **检索结果规模**:来自京东 PC 搜索返回的「结果条数」类指标,表示平台侧申报的匹配数量级,**不等于**动销、库存或独立 SKU 数。",
"", "",
"### 1.4 主要局限", "### 1.4 主要局限",

View File

@ -6,7 +6,6 @@ from __future__ import annotations
import json import json
import os import os
import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -71,25 +70,6 @@ def merge_llm_report_with_rules_charts(llm_md: str, rules_md: str) -> str:
return merge_llm_supplement_with_rules_report(llm_md, rules_md) return merge_llm_supplement_with_rules_report(llm_md, rules_md)
def inject_section_bridges_into_markdown(md: str, bridges: dict[str, str]) -> str:
"""
## 一、」…「## 九、」各章标题行之后插入 ``#### 衔接分析(大模型)`` 段落。
自第九章向前替换避免多次插入导致偏移错位
"""
out = md
for key in "九八七六五四三二一":
content = (bridges.get(key) or "").strip()
if not content:
continue
pat = re.compile(rf"^(## {key}、[^\n]*)\n", re.MULTILINE)
def _repl(m: re.Match[str], _c: str = content) -> str:
return m.group(0) + "\n#### 衔接分析(大模型)\n\n" + _c + "\n\n"
out = pat.sub(_repl, out, count=1)
return out
def _flat_comment_texts(comment_rows: list[dict[str, str]]) -> list[str]: def _flat_comment_texts(comment_rows: list[dict[str, str]]) -> list[str]:
"""全部非空评价正文(与报告统计同源)。""" """全部非空评价正文(与报告统计同源)。"""
out: list[str] = [] out: list[str] = []
@ -169,7 +149,6 @@ def get_default_report_config() -> dict[str, Any]:
jcr, _ = _jd_crawler_modules() jcr, _ = _jd_crawler_modules()
return { return {
"llm_comment_sentiment": True, "llm_comment_sentiment": True,
"llm_section_bridges": True,
"llm_matrix_group_summaries": True, "llm_matrix_group_summaries": True,
"llm_comment_group_summaries": True, "llm_comment_group_summaries": True,
"llm_price_group_summaries": True, "llm_price_group_summaries": True,
@ -528,48 +507,6 @@ def write_competitor_analysis_for_run_dir(
llm_comment_groups_section_md=llm_comment_gr_md or None, llm_comment_groups_section_md=llm_comment_gr_md or None,
) )
bridge_record: dict[str, Any] = {
"schema_version": 1,
"attempted": False,
}
skip_bridge = os.environ.get(
"MA_SKIP_LLM_SECTION_BRIDGES", ""
).strip().lower() in ("1", "true", "yes")
env_bridge = os.environ.get(
"MA_ENABLE_LLM_SECTION_BRIDGES", ""
).strip().lower() in ("1", "true", "yes")
want_bridge = bool(eff_rc.get("llm_section_bridges")) or env_bridge
if want_bridge and not skip_bridge:
from .llm_generate import (
generate_section_bridges_llm,
split_competitor_report_for_bridges,
)
parts = split_competitor_report_for_bridges(md)
if parts:
bridge_record["attempted"] = True
try:
bridges = generate_section_bridges_llm(
keyword=kw, brief=brief_final, sections=parts
)
bridge_record["keys_received"] = sorted(bridges.keys())
md = inject_section_bridges_into_markdown(md, bridges)
bridge_record["ok"] = True
except Exception as e:
bridge_record["ok"] = False
bridge_record["error"] = str(e)
else:
bridge_record["skipped"] = "no_h2_sections_matched"
elif skip_bridge:
bridge_record["skipped"] = "MA_SKIP_LLM_SECTION_BRIDGES"
elif not want_bridge:
bridge_record["skipped"] = "not_enabled"
(run_dir / "section_bridge_llm.json").write_text(
json.dumps(bridge_record, ensure_ascii=False, indent=2),
encoding="utf-8",
)
out_md = run_dir / "competitor_analysis.md" out_md = run_dir / "competitor_analysis.md"
out_md.write_text(md, encoding="utf-8") out_md.write_text(md, encoding="utf-8")
return run_dir return run_dir

View File

@ -5,7 +5,6 @@
- §6 ``generate_price_group_summaries_llm`` - §6 ``generate_price_group_summaries_llm``
- §8.2``generate_comment_sentiment_analysis_llm`` - §8.2``generate_comment_sentiment_analysis_llm``
- §8末细类评价``generate_comment_group_summaries_llm`` - §8末细类评价``generate_comment_group_summaries_llm``
- 各章衔接``generate_section_bridges_llm``基于无 LLM 插入的规则稿切分
- §8.5 类全文补充独立长文``generate_competitor_report_markdown_llm`` - §8.5 类全文补充独立长文``generate_competitor_report_markdown_llm``
cd backend cd backend
@ -124,7 +123,7 @@ def main() -> None:
"--only", "--only",
type=str, type=str,
default="", default="",
help="逗号分隔子集sentiment,matrix,price,comment_groups,bridges,report_supplement", help="逗号分隔子集sentiment,matrix,price,comment_groups,report_supplement",
) )
parser.add_argument( parser.add_argument(
"--preview-chars", "--preview-chars",
@ -144,7 +143,6 @@ def main() -> None:
"matrix", "matrix",
"price", "price",
"comment_groups", "comment_groups",
"bridges",
"report_supplement", "report_supplement",
} }
if not only: if not only:
@ -166,8 +164,6 @@ def main() -> None:
generate_competitor_report_markdown_llm, generate_competitor_report_markdown_llm,
generate_matrix_group_summaries_llm, generate_matrix_group_summaries_llm,
generate_price_group_summaries_llm, generate_price_group_summaries_llm,
generate_section_bridges_llm,
split_competitor_report_for_bridges,
) )
if "sentiment" in only: if "sentiment" in only:
@ -256,18 +252,8 @@ def main() -> None:
if not args.live: if not args.live:
print(f" payload groups={len(pl_cg)}", flush=True) print(f" payload groups={len(pl_cg)}", flush=True)
brief = jcr.build_competitor_brief( if "report_supplement" in only:
run_dir=run_dir, brief = jcr.build_competitor_brief(
keyword=keyword,
merged_rows=merged,
search_export_rows=search_rows,
comment_rows=comment_rows,
meta=meta,
report_config=eff_rc,
)
if "bridges" in only:
md_base = jcr.build_competitor_markdown(
run_dir=run_dir, run_dir=run_dir,
keyword=keyword, keyword=keyword,
merged_rows=merged, merged_rows=merged,
@ -275,29 +261,7 @@ def main() -> None:
comment_rows=comment_rows, comment_rows=comment_rows,
meta=meta, meta=meta,
report_config=eff_rc, report_config=eff_rc,
llm_sentiment_section_md=None,
llm_matrix_section_md=None,
llm_price_groups_section_md=None,
llm_comment_groups_section_md=None,
) )
parts = split_competitor_report_for_bridges(md_base)
def _br() -> str:
m = generate_section_bridges_llm(
keyword=keyword, brief=brief, sections=parts
)
return json.dumps(m, ensure_ascii=False, indent=2)
_run_one(
"各章衔接 bridgesJSON 键一~九)",
_br,
live=args.live,
preview_chars=min(args.preview_chars, 1200),
)
if not args.live:
print(f" sections keys={sorted(parts.keys())}", flush=True)
if "report_supplement" in only:
def _rp() -> str: def _rp() -> str:
return generate_competitor_report_markdown_llm(brief, keyword) return generate_competitor_report_markdown_llm(brief, keyword)

View File

@ -17,7 +17,6 @@ from .models import (
_REPORT_CONFIG_ALLOWED_KEYS = frozenset( _REPORT_CONFIG_ALLOWED_KEYS = frozenset(
{ {
"llm_comment_sentiment", "llm_comment_sentiment",
"llm_section_bridges",
"llm_matrix_group_summaries", "llm_matrix_group_summaries",
"llm_price_group_summaries", "llm_price_group_summaries",
"llm_comment_group_summaries", "llm_comment_group_summaries",
@ -31,6 +30,8 @@ _REPORT_CONFIG_ALLOWED_KEYS = frozenset(
def validate_report_config_body(value: dict) -> dict: def validate_report_config_body(value: dict) -> dict:
if not isinstance(value, dict): if not isinstance(value, dict):
raise serializers.ValidationError("须为 JSON 对象") raise serializers.ValidationError("须为 JSON 对象")
value = dict(value)
value.pop("llm_section_bridges", None)
extra = set(value.keys()) - _REPORT_CONFIG_ALLOWED_KEYS extra = set(value.keys()) - _REPORT_CONFIG_ALLOWED_KEYS
if extra: if extra:
raise serializers.ValidationError( raise serializers.ValidationError(
@ -39,9 +40,6 @@ def validate_report_config_body(value: dict) -> dict:
if "llm_comment_sentiment" in value and value["llm_comment_sentiment"] is not None: if "llm_comment_sentiment" in value and value["llm_comment_sentiment"] is not None:
if not isinstance(value["llm_comment_sentiment"], bool): if not isinstance(value["llm_comment_sentiment"], bool):
raise serializers.ValidationError("llm_comment_sentiment 须为 true 或 false") raise serializers.ValidationError("llm_comment_sentiment 须为 true 或 false")
if "llm_section_bridges" in value and value["llm_section_bridges"] is not None:
if not isinstance(value["llm_section_bridges"], bool):
raise serializers.ValidationError("llm_section_bridges 须为 true 或 false")
for k in ( for k in (
"llm_matrix_group_summaries", "llm_matrix_group_summaries",
"llm_price_group_summaries", "llm_price_group_summaries",
@ -61,7 +59,6 @@ _ARTIFACT_FILES: tuple[tuple[str, str], ...] = (
("comments", "comments_flat.csv"), ("comments", "comments_flat.csv"),
("detail_ware", "detail_ware_export.csv"), ("detail_ware", "detail_ware_export.csv"),
("report", "competitor_analysis.md"), ("report", "competitor_analysis.md"),
("section_bridge_llm", "section_bridge_llm.json"),
) )

View File

@ -17,7 +17,6 @@ function splitTriggers(text) {
*/ */
const REPORT_CONFIG_PASSTHROUGH_BOOL_KEYS = [ const REPORT_CONFIG_PASSTHROUGH_BOOL_KEYS = [
'llm_comment_sentiment', 'llm_comment_sentiment',
'llm_section_bridges',
'llm_matrix_group_summaries', 'llm_matrix_group_summaries',
'llm_price_group_summaries', 'llm_price_group_summaries',
'llm_comment_group_summaries', 'llm_comment_group_summaries',

View File

@ -278,7 +278,7 @@ watch(
<h3 class="report-config-title">报告里的评价统计怎么算</h3> <h3 class="report-config-title">报告里的评价统计怎么算</h3>
<p class="hint-top report-config-hint"> <p class="hint-top report-config-hint">
关注词场景词组外部市场表等<strong>可以不改</strong>留空并保存即沿用内置规则大模型相关布尔项 关注词场景词组外部市场表等<strong>可以不改</strong>留空并保存即沿用内置规则大模型相关布尔项
<code>llm_comment_sentiment</code><code>llm_section_bridges</code>不再单独占勾选框若任务里已有会在保存时保留要改请展开高级 JSON <code>llm_comment_sentiment</code>不再单独占勾选框若任务里已有会在保存时保留要改请展开高级 JSON
</p> </p>
<div class="report-config-actions"> <div class="report-config-actions">
<button <button
@ -315,7 +315,7 @@ watch(
<summary>高级 JSON 编辑一般不需要</summary> <summary>高级 JSON 编辑一般不需要</summary>
<p class="rc-help"> <p class="rc-help">
打开时会根据上面表单生成内容改完后点写回表单再保存可在此加入 打开时会根据上面表单生成内容改完后点写回表单再保存可在此加入
<code>llm_comment_sentiment</code><code>llm_section_bridges</code><code>llm_matrix_group_summaries</code> <code>llm_comment_sentiment</code><code>llm_matrix_group_summaries</code>
等布尔字段须为 <code>true</code>/<code>false</code>页顶重新生成报告默认已使用 等布尔字段须为 <code>true</code>/<code>false</code>页顶重新生成报告默认已使用
<code>generator:&quot;llm&quot;</code>若只要规则稿请勾选本次仅用规则引擎 <code>generator:&quot;llm&quot;</code>若只要规则稿请勾选本次仅用规则引擎
</p> </p>