From 15ba8b3370032a341052d3ca3bc2bb8e9e11204c Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Tue, 14 Apr 2026 11:08:24 +0800 Subject: [PATCH] fix(llm): restore matrix/comment/price group advisor prompts and callers Re-add MATRIX/COMMENT/PRICE_GROUPS_SYSTEM and generate_*_group_summaries_llm removed in afe2748; align comment-group prompt with shop in attribution prefix. Default report_config now includes the three llm_*_summaries flags. Made-with: Cursor --- backend/pipeline/jd_runner.py | 3 + backend/pipeline/llm_generate.py | 133 +++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/backend/pipeline/jd_runner.py b/backend/pipeline/jd_runner.py index ae12e69..701e166 100644 --- a/backend/pipeline/jd_runner.py +++ b/backend/pipeline/jd_runner.py @@ -168,6 +168,9 @@ def get_default_report_config() -> dict[str, Any]: return { "llm_comment_sentiment": False, "llm_section_bridges": False, + "llm_matrix_group_summaries": False, + "llm_comment_group_summaries": False, + "llm_price_group_summaries": False, "comment_focus_words": list(jcr.COMMENT_FOCUS_WORDS), "comment_scenario_groups": [ {"label": lbl, "triggers": list(trs)} diff --git a/backend/pipeline/llm_generate.py b/backend/pipeline/llm_generate.py index 2d2e275..ca9188e 100644 --- a/backend/pipeline/llm_generate.py +++ b/backend/pipeline/llm_generate.py @@ -270,3 +270,136 @@ def generate_strategy_draft_markdown_llm( raw = json.dumps(payload, ensure_ascii=False) user = STRATEGY_USER_PREFIX + raw return _call_llm(STRATEGY_SYSTEM, user) + + +MATRIX_GROUPS_SYSTEM = """你是竞品分析顾问。输入为 JSON:``keyword`` 与 ``groups`` 数组。 +每个 group 含 ``group``(细分类目名)、``sku_count``、``price_stats``(该细类深入合并行可解析展示价的 min/max/median/mean/n,与 **§6「按细类价盘」** 分位数表同源;无则 n=0 或缺字段)、 +``lines``(该细类下若干 SKU 的标题/卖点/配料**摘录**,均来自页面抓取拼接,可能截断)。 + +请**为每个细类**输出一小段 Markdown(全部 groups 都要写,顺序与输入一致): +- 以 ``#### `` + 与该 group 字段**完全一致**的细类名作为小节标题(不要使用 ``##`` 一级标题); +- 每段约 **100~200 字**中文:**主体**归纳该细类下**卖点表述共性**、**配料类型/宣称共性**(摘录中无配料则写「配料摘录较少」);**品牌格局**可一句概括(仅依据摘录中可见品牌/系列,勿编造销量排名); +- **价带/价位**:若 ``price_stats.n`` 为大于 0 的整数,**仅允许**用该对象里的数值写价带(如 min~max、中位数),且须与 ``price_stats`` **完全一致**,**禁止**写「价格带未明确」「未体现具体价位」「多为中端」等**与上述数值相矛盾**的表述;若 n=0 或无可信数值,**不要猜测价位**,可写一句「深入样本可解析数值价不足,价盘以 **§6** 表格为准」; +- **禁止**输出 Markdown 表格、禁止逐条复述 SKU 明细表;勿编造功效、认证; +- 若 ``lines`` 很少,明确写「样本较少,归纳供启发」。 + +总输出约 **800~3500 字**(细类多则偏长)。仅输出正文 Markdown,不要用代码围栏包裹全文。""" + + +MATRIX_GROUPS_USER_PREFIX = ( + "请根据以下 JSON 撰写竞品报告第五章末「细类要点归纳」正文(Markdown)。\n\n" +) + + +def generate_matrix_group_summaries_llm( + groups: list[dict[str, Any]], *, keyword: str +) -> str: + trimmed: list[dict[str, Any]] = [] + for g in groups: + if not isinstance(g, dict): + continue + g2 = dict(g) + ln = g2.get("lines") + if isinstance(ln, list) and len(ln) > 22: + g2["lines"] = ln[:22] + trimmed.append(g2) + payload = {"keyword": keyword, "groups": trimmed} + raw = json.dumps(payload, ensure_ascii=False) + if len(raw) > 95_000: + for g2 in trimmed: + ln = g2.get("lines") + if isinstance(ln, list) and len(ln) > 12: + g2["lines"] = ln[:12] + raw = json.dumps({"keyword": keyword, "groups": trimmed}, ensure_ascii=False) + user = MATRIX_GROUPS_USER_PREFIX + raw + return _call_llm(MATRIX_GROUPS_SYSTEM, user) + + +COMMENT_GROUPS_SYSTEM = """你是用户研究与品类顾问。输入为 JSON:``keyword`` 与 ``groups``。 +每个 group 含 ``group``(与 §5 矩阵一致的细分类目名)、``comment_flat_rows``、``effective_text_lines``、 +``focus_hit_lines``(关注词子串命中摘要,与 §8.3 同源)、``sample_text_snippets``(评价短摘录,已截断)。 +摘录行通常以 ``【细类:…|SKU:…|品名:…|店铺:…】`` 开头:细类可与本 group 名对照,**品名/SKU/店铺**表示该句具体出自哪条链接;归纳时若引用原话,**须交代是「哪家店、哪条 SKU、哪款品名」上的反馈**,勿只写「有用户说口感差」而不指代产品。 + +请**为每个细类**输出一小段 Markdown(全部 groups 都要写,顺序与输入一致): +- 以 ``#### `` + 与该 group 字段**完全一致**的细类名作为小节标题(不要使用 ``##`` 一级标题); +- 每段约 **100~220 字**中文:归纳该细类下**消费者在讨论什么**(口感、价格、物流、功效疑虑等)、**关注词命中反映的诉求**;勿编造摘录中未出现的品牌、医学结论; +- **禁止**输出 Markdown 表格、禁止逐条复述全部评价; +- 若 ``effective_text_lines`` 很少,明确写「样本较少,归纳供启发」。 + +总输出约 **600~3200 字**。仅输出正文 Markdown,不要用代码围栏包裹全文。""" + + +COMMENT_GROUPS_USER_PREFIX = ( + "请根据以下 JSON 撰写竞品报告第八章末「细类评论与关注词要点归纳」正文(Markdown)。\n\n" +) + + +def generate_comment_group_summaries_llm( + groups: list[dict[str, Any]], *, keyword: str +) -> str: + trimmed: list[dict[str, Any]] = [] + for g in groups: + if not isinstance(g, dict): + continue + g2 = dict(g) + sn = g2.get("sample_text_snippets") + if isinstance(sn, list) and len(sn) > 16: + g2["sample_text_snippets"] = sn[:16] + fh = g2.get("focus_hit_lines") + if isinstance(fh, list) and len(fh) > 14: + g2["focus_hit_lines"] = fh[:14] + trimmed.append(g2) + payload = {"keyword": keyword, "groups": trimmed} + raw = json.dumps(payload, ensure_ascii=False) + if len(raw) > 95_000: + for g2 in trimmed: + sn = g2.get("sample_text_snippets") + if isinstance(sn, list) and len(sn) > 10: + g2["sample_text_snippets"] = sn[:10] + raw = json.dumps({"keyword": keyword, "groups": trimmed}, ensure_ascii=False) + user = COMMENT_GROUPS_USER_PREFIX + raw + return _call_llm(COMMENT_GROUPS_SYSTEM, user) + + +PRICE_GROUPS_SYSTEM = """你是定价与渠道顾问。输入为 JSON:``keyword`` 与 ``groups``。 +每个 group 含 ``group``(细分类目名,与 §5 矩阵、§6「按细类价盘」小节一致)、``sku_count``、``price_stats``(该细类可解析展示价的 min/max/median/mean/n,与 §6 各细类 Markdown 分位数表同源)、 +``listing_snippets``(若干「标题|标价|券后|详情价」摘录,来自合并表字段,已截断)。 + +请**为每个细类**输出一小段 Markdown(全部 groups 都要写,顺序与输入一致): +- 以 ``#### `` + 与该 group 字段**完全一致**的细类名作为小节标题; +- 每段约 **80~200 字**中文,**只写价盘与价差**:用 ``price_stats`` 概括价带/离散度(如 min~max、中位数、相对集中或拉得开);用 ``listing_snippets`` 归纳**标价 vs 券后 vs 详情价**是否常一致、是否常见券后低于标价、价差幅度的大致印象;**可一句**联系标题里**显式出现的规格数字**(如克重、件数)解释**价高/价差大是否可能来自大规格或组合装**——仅当摘录里确有数字时写,勿展开成宣称解读; +- **硬性禁止**(本章不是卖点章):不要列举或归纳「0 蔗糖 / 低 GI / 全麦 / 代餐 / 孕妇 / 控糖」等**营销宣称或场景关键词**;不要写配料、功效、品牌叙事、用户画像;这些若出现应留给报告 **§5 细类要点归纳** 或 **§7**; +- **禁止** Markdown 表格、禁止罗列全部 SKU;勿编造未出现的到手价、销量排名; +- 若 ``price_stats`` 中 n=0 或缺失,写「该细类无可解析数值价,从略」。 + +总输出约 **500~2800 字**。仅输出正文 Markdown,不要用代码围栏包裹全文。""" + + +PRICE_GROUPS_USER_PREFIX = ( + "请根据以下 JSON 撰写竞品报告第六章末「细类价盘要点归纳」正文(Markdown)。" + "本章只写**数值价带与标价/券后/详情价关系**,勿写卖点宣称关键词归纳。\n\n" +) + + +def generate_price_group_summaries_llm( + groups: list[dict[str, Any]], *, keyword: str +) -> str: + trimmed: list[dict[str, Any]] = [] + for g in groups: + if not isinstance(g, dict): + continue + g2 = dict(g) + sn = g2.get("listing_snippets") + if isinstance(sn, list) and len(sn) > 14: + g2["listing_snippets"] = sn[:14] + trimmed.append(g2) + payload = {"keyword": keyword, "groups": trimmed} + raw = json.dumps(payload, ensure_ascii=False) + if len(raw) > 95_000: + for g2 in trimmed: + sn = g2.get("listing_snippets") + if isinstance(sn, list) and len(sn) > 8: + g2["listing_snippets"] = sn[:8] + raw = json.dumps({"keyword": keyword, "groups": trimmed}, ensure_ascii=False) + user = PRICE_GROUPS_USER_PREFIX + raw + return _call_llm(PRICE_GROUPS_SYSTEM, user)