mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
fix(llm): 评论细类归纳载荷移除 focus_hit_lines 并对齐系统提示
build_comment_groups_llm_payload 不再传入关注词或生成子串命中摘要;COMMENT_GROUPS_SYSTEM 仅依据 effective_text_lines 与 sample_text_snippets。探针合并与 PROBE 提示词去掉 focus_hit_lines 表述;runner/演示脚本去掉无效的 comment_focus_words 装配。 Made-with: Cursor
This commit is contained in:
parent
30ea9783b3
commit
396ccd941a
@ -220,12 +220,11 @@ def build_promo_groups_llm_payload(
|
||||
def build_comment_groups_llm_payload(
|
||||
*,
|
||||
feedback_groups: list[tuple[str, list[dict[str, str]], list[str]]],
|
||||
focus_words: tuple[str, ...],
|
||||
merged_rows: list[dict[str, str]],
|
||||
sku_header: str,
|
||||
title_h: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""供 ``generate_comment_group_summaries_llm``。"""
|
||||
"""供 ``generate_comment_group_summaries_llm``:仅含评价文本单元与短摘录,不含关注词子串计数摘要。"""
|
||||
if not feedback_groups:
|
||||
return []
|
||||
sku_meta: dict[str, tuple[str, str, str]] = {}
|
||||
@ -245,10 +244,6 @@ def build_comment_groups_llm_payload(
|
||||
for gname, cr, tu in feedback_groups:
|
||||
if not tu and not cr:
|
||||
continue
|
||||
gh = _group_keyword_hits(cr, tu, focus_words=focus_words)
|
||||
focus_hit_lines = [
|
||||
f"「{w}」{n} 次" for w, n in gh.most_common(14) if n > 0
|
||||
]
|
||||
snippets: list[str] = []
|
||||
for row in cr[:48]:
|
||||
txt = _cell(row, _COMMENT_CSV_BODY, "tagCommentContent")
|
||||
@ -277,7 +272,6 @@ def build_comment_groups_llm_payload(
|
||||
"group": gname,
|
||||
"comment_flat_rows": f"评价行 {len(cr)};有效文本单元 {len(tu)}",
|
||||
"effective_text_lines": eff,
|
||||
"focus_hit_lines": focus_hit_lines,
|
||||
"sample_text_snippets": snippets,
|
||||
}
|
||||
)
|
||||
|
||||
@ -100,14 +100,14 @@ PROBE_TEXT_MINING_SYSTEM = """你是用户研究与文本挖掘方向的助手
|
||||
- ``probe_status``:``ok`` 表示该细类已完成分词与统计;``skipped`` 表示样本过少等未下钻。
|
||||
- ``word_freq_top`` / ``tfidf_top`` / ``cooccurrence_top``:统计型特征(开放词表)。
|
||||
- ``lda``:无监督自动归纳的主题词;**仅为探索**,同一词可出现在多主题,**禁止**当作严格品类或固定标签。
|
||||
- ``focus_hit_lines`` / ``sample_text_snippets``:与正式管线摘取方式**类似**,仅用于**对照语境**;若与关键词突出度焦点冲突,以**整句原文**为准,并在段末或「使用注意」中可点明「统计与语义可能不一致」。
|
||||
- ``sample_text_snippets``:与正式报告管线同源的评价短摘录(若有),仅用于**对照语境**;若与关键词突出度焦点冲突,以**整句原文**为准,并在段末或「使用注意」中可点明「统计与语义可能不一致」。
|
||||
|
||||
**任务**:对 ``groups`` 中**每一项**输出对应 Markdown(**顺序与输入一致**):
|
||||
- 对 ``probe_status == "ok"``:以 ``#### `` + 与该条 ``group`` 字段**完全一致**的细类名作为小节标题(勿用 ``##`` 一级标题);每段约 **100~260 字**。
|
||||
- 内容须包含:①用 **1~2 句**概括该细类评论**主要讨论焦点**(综合词频与关键词突出度,**不要罗列具体数字**);② **1~2 句**说明共现词对**暗示**哪些维度常一起出现(**非因果**);③若 ``lda.topics`` 非空,**1~2 句**说明主题粗分侧重点,并**明确**算法无监督、**不**与矩阵细类一一对应;④用 **1~2 句**体现 ``sample_text_snippets`` / ``focus_hit_lines`` 中的**用户语气与关切**(以**转述**为主);若必须引用原文,**全小节合计**仅 **一处**极短引号内容(**≤40 字**,**不要**输出 ``【细类…SKU…店铺…】`` 等长前缀);若无可用摘录则写明;⑤ **使用场景(仅从评论推断)**:用 **0~2 句**概括**何时、何地、何人、如何搭配**等(如早餐、加餐、控糖人群、配牛奶等)——**只能**依据本细类 ``word_freq``/``tfidf``/``cooccurrence``/``lda`` 与摘录中**已出现或可合理概括**的信息;**禁止**套用正式报告「场景分组」或其它外部场景分类;若统计与摘录中**均无**场景线索,**一句**写明「评论中未体现清晰使用场景」即可。
|
||||
- 内容须包含:①用 **1~2 句**概括该细类评论**主要讨论焦点**(综合词频与关键词突出度,**不要罗列具体数字**);② **1~2 句**说明共现词对**暗示**哪些维度常一起出现(**非因果**);③若 ``lda.topics`` 非空,**1~2 句**说明主题粗分侧重点,并**明确**算法无监督、**不**与矩阵细类一一对应;④用 **1~2 句**体现 ``sample_text_snippets`` 中的**用户语气与关切**(以**转述**为主);若必须引用原文,**全小节合计**仅 **一处**极短引号内容(**≤40 字**,**不要**输出 ``【细类…SKU…店铺…】`` 等长前缀);若无可用摘录则写明;⑤ **使用场景(仅从评论推断)**:用 **0~2 句**概括**何时、何地、何人、如何搭配**等(如早餐、加餐、控糖人群、配牛奶等)——**只能**依据本细类 ``word_freq``/``tfidf``/``cooccurrence``/``lda`` 与摘录中**已出现或可合理概括**的信息;**禁止**套用正式报告「场景分组」或其它外部场景分类;若统计与摘录中**均无**场景线索,**一句**写明「评论中未体现清晰使用场景」即可。
|
||||
- 对 ``probe_status == "skipped"``:该小节仅 **一句**说明原因。
|
||||
|
||||
**禁止**:编造数据中未出现的品牌、价格、医学功效或疗效承诺;不要把 ``keyword`` 监测词写进「用户原话」;不要输出 Markdown 表格;不要声称本段与「正式报告第八章末」完全同源——本任务为**补充分析解读**。**禁止**把输入里的 ``sample_text_snippets`` / ``focus_hit_lines`` **逐条罗列**、**多条整段复制**到输出(那不是归纳,是重复贴评论)。
|
||||
**禁止**:编造数据中未出现的品牌、价格、医学功效或疗效承诺;不要把 ``keyword`` 监测词写进「用户原话」;不要输出 Markdown 表格;不要声称本段与「正式报告第八章末」完全同源——本任务为**补充分析解读**。**禁止**把输入里的 ``sample_text_snippets`` **逐条罗列**、**多条整段复制**到输出(那不是归纳,是重复贴评论)。
|
||||
|
||||
全文末可另起一段 **「使用注意」**(简短):点明开放词表统计与人工阅读差异、主题归纳局限、小样本细类不可靠。
|
||||
|
||||
@ -381,23 +381,6 @@ def _narrative_stub(
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _effective_focus_words(run_dir: Path) -> tuple[str, ...]:
|
||||
"""与 ``runner.write_competitor_analysis_for_run_dir`` 一致:优先 ``effective_report_config.json``。"""
|
||||
p = run_dir / "effective_report_config.json"
|
||||
if p.is_file():
|
||||
try:
|
||||
eff = json.loads(p.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
eff = None
|
||||
if isinstance(eff, dict):
|
||||
fw_src = eff.get("comment_focus_words") or list(jcr.COMMENT_FOCUS_WORDS)
|
||||
fw_tuple = tuple(
|
||||
str(x).strip() for x in fw_src if str(x).strip()
|
||||
) or jcr.COMMENT_FOCUS_WORDS
|
||||
return fw_tuple
|
||||
return jcr.COMMENT_FOCUS_WORDS
|
||||
|
||||
|
||||
def _truncate_probe_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""压缩摘录长度,避免单次 JSON 顶满上下文。"""
|
||||
out: dict[str, Any] = dict(payload)
|
||||
@ -407,6 +390,7 @@ def _truncate_probe_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
groups.append(g)
|
||||
continue
|
||||
g2 = dict(g)
|
||||
g2.pop("focus_hit_lines", None)
|
||||
sn = g2.get("sample_text_snippets")
|
||||
if isinstance(sn, list):
|
||||
# 条数略减,降低模型「照抄罗列」倾向;仍以转述为主见系统提示
|
||||
@ -421,7 +405,6 @@ def _merge_snippets_from_comment_groups(
|
||||
*,
|
||||
merged_rows: list[dict[str, str]],
|
||||
comment_rows: list[dict[str, str]],
|
||||
run_dir: Path,
|
||||
) -> None:
|
||||
"""把正式 ``build_comment_groups_llm_payload`` 中的摘录并入补充分析行(原地修改)。"""
|
||||
sku_h = MERGED_FIELD_TO_CSV_HEADER["sku_id"]
|
||||
@ -431,10 +414,8 @@ def _merge_snippets_from_comment_groups(
|
||||
comment_rows=comment_rows,
|
||||
sku_header=sku_h,
|
||||
)
|
||||
fw = _effective_focus_words(run_dir)
|
||||
pl = jcr.build_comment_groups_llm_payload(
|
||||
feedback_groups=fb,
|
||||
focus_words=fw,
|
||||
merged_rows=merged_rows,
|
||||
sku_header=sku_h,
|
||||
title_h=title_h,
|
||||
@ -448,9 +429,6 @@ def _merge_snippets_from_comment_groups(
|
||||
if not src:
|
||||
continue
|
||||
row["comment_flat_rows"] = src.get("comment_flat_rows")
|
||||
fh = src.get("focus_hit_lines")
|
||||
if isinstance(fh, list):
|
||||
row["focus_hit_lines"] = [str(x) for x in fh[:8]]
|
||||
sn = src.get("sample_text_snippets")
|
||||
if isinstance(sn, list):
|
||||
row["sample_text_snippets"] = [str(x)[:220] for x in sn[:8]]
|
||||
@ -689,15 +667,14 @@ def build_markdown(
|
||||
probe_rows,
|
||||
merged_rows=merged,
|
||||
comment_rows=comments,
|
||||
run_dir=run_dir,
|
||||
)
|
||||
llm_payload: dict[str, Any] = {
|
||||
"schema_version": 1,
|
||||
"keyword": kw,
|
||||
"probe_note": (
|
||||
"中文分词 + 停用词;关键词突出度/共现/主题归纳为统计库;"
|
||||
"与正式报告的关注词规则统计、已废弃的预设口语短语情感口径均不同;"
|
||||
"评价摘录字段合并自 build_comment_groups_llm_payload,供语境对照;"
|
||||
"与正式报告第八章第二节图表中的关注词计数等规则统计、已废弃的预设口语短语情感口径均不同;"
|
||||
"评价短摘录合并自 build_comment_groups_llm_payload(与正式管线同源,不含关注词子串计数摘要),供语境对照;"
|
||||
"「使用场景」若出现,须仅能从本 JSON 内统计与摘录推断,不接入正式场景分组。"
|
||||
),
|
||||
"groups": probe_rows,
|
||||
|
||||
@ -354,13 +354,8 @@ def main() -> None:
|
||||
comment_rows=comment_rows,
|
||||
sku_header=sku_h,
|
||||
)
|
||||
fw_src = eff_rc.get("comment_focus_words") or list(jcr.COMMENT_FOCUS_WORDS)
|
||||
fw_tuple = tuple(
|
||||
str(x).strip() for x in fw_src if str(x).strip()
|
||||
) or jcr.COMMENT_FOCUS_WORDS
|
||||
pl_cg = jcr.build_comment_groups_llm_payload(
|
||||
feedback_groups=fb,
|
||||
focus_words=fw_tuple,
|
||||
merged_rows=merged,
|
||||
sku_header=sku_h,
|
||||
title_h=title_h,
|
||||
@ -374,7 +369,7 @@ def main() -> None:
|
||||
return generate_comment_group_summaries_llm(pl_cg, keyword=keyword)
|
||||
|
||||
_run_one(
|
||||
"§8 细类评价与关注词(comment_groups)",
|
||||
"§8 细类评价要点(comment_groups)",
|
||||
_cg,
|
||||
live=args.live,
|
||||
preview_chars=args.preview_chars,
|
||||
|
||||
@ -679,13 +679,8 @@ def write_competitor_analysis_for_run_dir(
|
||||
comment_rows=comment_rows,
|
||||
sku_header=sku_h,
|
||||
)
|
||||
fw_src = eff_rc.get("comment_focus_words") or list(jcr.COMMENT_FOCUS_WORDS)
|
||||
fw_tuple = tuple(
|
||||
str(x).strip() for x in fw_src if str(x).strip()
|
||||
) or jcr.COMMENT_FOCUS_WORDS
|
||||
pl_cg = jcr.build_comment_groups_llm_payload(
|
||||
feedback_groups=fb_cg,
|
||||
focus_words=fw_tuple,
|
||||
merged_rows=merged_rows,
|
||||
sku_header=sku_h,
|
||||
title_h=title_h,
|
||||
|
||||
@ -50,15 +50,14 @@ def generate_matrix_group_summaries_llm(
|
||||
|
||||
|
||||
COMMENT_GROUPS_SYSTEM = """你是用户研究与品类顾问。输入为 JSON:``keyword`` 与 ``groups``。
|
||||
每个 group 含 ``group``(与 第五章矩阵一致的细分类目名)、``comment_flat_rows``、``effective_text_lines``、
|
||||
``focus_hit_lines``(关注词子串命中摘要,与 **第八章第二节**(关注词与场景路径)左栏同源)、``sample_text_snippets``(评价短摘录,已截断)。
|
||||
摘录行通常以 ``【细类:…|SKU:…|品名:…|店铺:…】`` 开头:细类可与本 group 名对照,**品名/SKU/店铺**表示该句具体出自哪条链接;归纳时若引用原话,**须交代是「哪家店、哪条 SKU、哪款品名」上的反馈**,勿只写「有用户说口感差」而不指代产品。
|
||||
关注词命中为子串统计,可能与句意不一致;**请以整句语义**判断褒贬(如「软硬适中」「没那么甜」常为满意表述,不得据此写成质地问题)。
|
||||
每个 group 含 ``group``(与 第五章矩阵一致的细分类目名)、``comment_flat_rows``、``effective_text_lines``(该细类下从评价中抽取的短文本单元)、``sample_text_snippets``(带 SKU/品名/店铺前缀的评价短摘录,已截断)。
|
||||
摘录行通常以 ``【细类:…|SKU:…|品名:…|店铺:…】`` 开头:**品名/SKU/店铺**表示该句具体出自哪条链接;归纳时若引用原话,**须交代是「哪家店、哪条 SKU、哪款品名」上的反馈**,勿只写「有用户说口感差」而不指代产品。
|
||||
请**以整句语义**判断褒贬(如「软硬适中」「没那么甜」常为满意表述),不得仅凭片段词就写成负面结论。
|
||||
|
||||
请**为每个细类**输出一小段 Markdown(全部 groups 都要写,顺序与输入一致):
|
||||
- 以 ``#### `` + 与该 group 字段**完全一致**的细类名作为小节标题(不要使用 ``##`` 一级标题);
|
||||
- 每段约 **100~220 字**中文:归纳该细类下**消费者在讨论什么**(口感、价格、物流、功效疑虑等)、**关注词命中反映的诉求**;勿编造摘录中未出现的品牌、医学结论;
|
||||
- **去重与可证(本章仅评论侧)**:本段**只**依据评价/关注词摘录,**禁止**把 ``keyword``、品类常识或商品标题卖点套话写成「用户评价」;**禁止**各细类段首复用同一句总括(如「整体上满足了消费者对低 GI、高蛋白、便携性的需求」);每段开头句式须**有变化**,并至少一句体现**该细类与相邻细类在讨论焦点上的差异**。**利益/诉求词**(低 GI、高蛋白、便携、代餐、控糖等)**仅当**在 ``sample_text_snippets``、``effective_text_lines`` 或 ``focus_hit_lines`` 的**原文**中可子串命中或可明确同义(便携↔随身、小包装、单片、独立装等)时才写;若上述字段中**未**出现「蛋白」「便携」「随身」「小包装」「单片」等,则**不得**写「高蛋白」「便携性」等;**禁止**为凑齐常见卖点组合而脑补未在输入中出现的词。
|
||||
- 每段约 **100~220 字**中文:归纳该细类下**消费者在讨论什么**(口感、价格、物流、功效疑虑等);勿编造摘录中未出现的品牌、医学结论;
|
||||
- **去重与可证(本章仅评论侧)**:本段**只**依据 ``sample_text_snippets`` 与 ``effective_text_lines`` 中的**原文**,**禁止**把 ``keyword``、品类常识或商品标题卖点套话写成「用户评价」;**禁止**各细类段首复用同一句总括(如「整体上满足了消费者对低 GI、高蛋白、便携性的需求」);每段开头句式须**有变化**,并至少一句体现**该细类与相邻细类在讨论焦点上的差异**。**利益/诉求词**(低 GI、高蛋白、便携、代餐、控糖等)**仅当**在上述字段的**原文**中可子串命中或可明确同义(便携↔随身、小包装、单片、独立装等)时才写;若**未**出现「蛋白」「便携」「随身」「小包装」「单片」等,则**不得**写「高蛋白」「便携性」等;**禁止**为凑齐常见卖点组合而脑补未在输入中出现的词。
|
||||
- **禁止**输出 Markdown 表格、禁止逐条复述全部评价;
|
||||
- 若 ``effective_text_lines`` 很少,明确写「样本较少,归纳供启发」。
|
||||
|
||||
@ -66,7 +65,7 @@ COMMENT_GROUPS_SYSTEM = """你是用户研究与品类顾问。输入为 JSON:
|
||||
|
||||
|
||||
COMMENT_GROUPS_USER_PREFIX = (
|
||||
"请根据以下 JSON 撰写竞品报告第八章末「细类评论与关注词要点归纳」正文(Markdown)。\n\n"
|
||||
"请根据以下 JSON 撰写竞品报告第八章末「细类评论要点归纳」正文(Markdown)。\n\n"
|
||||
)
|
||||
|
||||
|
||||
@ -85,7 +84,6 @@ def generate_comment_group_summaries_llm(
|
||||
eff_max: int,
|
||||
sn_n: int,
|
||||
sn_max: int,
|
||||
fh_n: int,
|
||||
) -> dict[str, Any]:
|
||||
g2: dict[str, Any] = {
|
||||
"group": g.get("group"),
|
||||
@ -101,11 +99,6 @@ def generate_comment_group_summaries_llm(
|
||||
g2["sample_text_snippets"] = [str(x)[:sn_max] for x in sn[:sn_n]]
|
||||
else:
|
||||
g2["sample_text_snippets"] = []
|
||||
fh = g.get("focus_hit_lines")
|
||||
if isinstance(fh, list):
|
||||
g2["focus_hit_lines"] = [str(x) for x in fh[:fh_n]]
|
||||
else:
|
||||
g2["focus_hit_lines"] = []
|
||||
return g2
|
||||
|
||||
ctx = llm_context_window_size()
|
||||
@ -115,24 +108,24 @@ def generate_comment_group_summaries_llm(
|
||||
est = estimate_chat_input_tokens(system, user_p)
|
||||
return est < 15_500
|
||||
|
||||
levels: list[tuple[int, int, int, int, int]] = [
|
||||
(14, 260, 10, 200, 10),
|
||||
(12, 220, 8, 180, 8),
|
||||
(10, 180, 8, 160, 6),
|
||||
(8, 150, 6, 140, 6),
|
||||
(6, 120, 5, 120, 5),
|
||||
(5, 100, 4, 100, 4),
|
||||
(4, 80, 3, 80, 3),
|
||||
(3, 70, 3, 70, 3),
|
||||
(3, 50, 2, 60, 2),
|
||||
levels: list[tuple[int, int, int, int]] = [
|
||||
(14, 260, 10, 200),
|
||||
(12, 220, 8, 180),
|
||||
(10, 180, 8, 160),
|
||||
(8, 150, 6, 140),
|
||||
(6, 120, 5, 120),
|
||||
(5, 100, 4, 100),
|
||||
(4, 80, 3, 80),
|
||||
(3, 70, 3, 70),
|
||||
(3, 50, 2, 60),
|
||||
]
|
||||
user = ""
|
||||
chosen = levels[-1]
|
||||
for level in levels:
|
||||
chosen = level
|
||||
eff_n, eff_max, sn_n, sn_max, fh_n = level
|
||||
eff_n, eff_max, sn_n, sn_max = level
|
||||
trimmed = [
|
||||
_compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max, fh_n=fh_n)
|
||||
_compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max)
|
||||
for g in groups
|
||||
if isinstance(g, dict)
|
||||
]
|
||||
@ -144,9 +137,9 @@ def generate_comment_group_summaries_llm(
|
||||
break
|
||||
else:
|
||||
tail = "\n\n…(JSON 已截断以适配上下文;仅依据可见字段撰写。)\n"
|
||||
eff_n, eff_max, sn_n, sn_max, fh_n = chosen
|
||||
eff_n, eff_max, sn_n, sn_max = chosen
|
||||
trimmed = [
|
||||
_compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max, fh_n=fh_n)
|
||||
_compact_one(g, eff_n=eff_n, eff_max=eff_max, sn_n=sn_n, sn_max=sn_max)
|
||||
for g in groups
|
||||
if isinstance(g, dict)
|
||||
]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""§8 末「细类评论与关注词要点归纳」大模型:单细类烟测(mock 网关,不调真实 API)。"""
|
||||
"""§8 末「细类评论要点归纳」大模型:单细类烟测(mock 网关,不调真实 API)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@ -44,7 +44,6 @@ def _payload_single_category_biscuit() -> tuple[list[dict], str, str]:
|
||||
)
|
||||
pl = jcr.build_comment_groups_llm_payload(
|
||||
feedback_groups=fb,
|
||||
focus_words=jcr.COMMENT_FOCUS_WORDS,
|
||||
merged_rows=merged,
|
||||
sku_header=sku_h,
|
||||
title_h=title_h,
|
||||
@ -68,7 +67,7 @@ class CommentGroupSummariesLlmTests(SimpleTestCase):
|
||||
|
||||
mock_llm.assert_called_once()
|
||||
_sys, user = mock_llm.call_args[0]
|
||||
self.assertIn("细类评论与关注词要点归纳", user)
|
||||
self.assertIn("细类评论要点归纳", user)
|
||||
self.assertIn("低GI测试", user)
|
||||
raw = user.split("正文(Markdown)。\n\n", 1)[-1]
|
||||
data = json.loads(raw.strip())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user