Files
beige/backend/app/services/ai_engine/_text_prompt.py
yangqianqian df1856d793 上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事
- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:08:13 +08:00

291 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
_text_prompt.py — 文案 prompt 组装 / JSON 解析 / 本地模板兜底
方法层(全品类共用):人设/变量池/5步框架/四段结构/反AI味规则
数据层(每产品各异):由 product dict 动态注入,代码不出现具体品牌/成分名
"""
from __future__ import annotations
import json
import random
import re
# ── Q4 佛系反推销人设全品类共用数据层产品名从product动态注入──────────
_PERSONA = """你是一个日常生活分享博主,不是品牌推广号。
核心人设:佛系、不推销、真实记录日常。
内核:不主动劝买,靠真实体验让读者自己动心;写的是生活,产品只是生活的一部分。
比例70% 写生活场景和使用感受30% 提产品,绝不颠倒。
收尾铁律:每条结尾方式必须不同,禁止使用"东西放这了/买不买跟我没关系"这类被用滥的固定句式。"""
# ── Q1 叙事切入配方(反同质化核心·北哥四象限人群法,全品类通用不写死)────────
# 方法层:只给"切入维度类型",具体人群/场景由模型按本产品卖点+目标人群填充
# 北哥四象限:场景/习惯/适配/时机——主人群维度按条轮转保证N条开篇不撞
_AUDIENCE_DIMENSIONS: list[str] = [
"使用场景人群(按该产品最高频的使用场景圈定一类人)",
"使用习惯人群(懒人省心/进阶讲究等按习惯分的一类人)",
"适配特质人群(该产品特别适配的某类特质或需求人群)",
"使用时机人群(按使用时间/阶段/契机圈定的一类人)",
"性价比人群(看重划算、对价格敏感的一类人)",
]
# 情绪触发类型(通用,模型按产品填具体内容)
_EMOTION_TRIGGERS: list[str] = [
"被身边人提醒或夸到才注意到的瞬间",
"自己用的时候突然意识到的变化",
"对比之前踩过的雷或走过的弯路",
]
# 真实小波折类型(增可信,通用不写死品类)
_REAL_FLAWS: list[str] = [
"一开始用法没掌握或用量不对",
"第一眼对包装或价格有点误解",
"起效没想象中快、用了阵子才感觉到",
]
def _pick_combo(index: int = 0) -> dict[str, str]:
"""抽一组通用叙事切入配方维度类型非写死内容主人群维度按index轮转N条不撞"""
return {
"identity": _AUDIENCE_DIMENSIONS[index % len(_AUDIENCE_DIMENSIONS)],
"emotion": random.choice(_EMOTION_TRIGGERS),
"flaw": random.choice(_REAL_FLAWS),
}
# ── Q5 negative词prompt级别负向约束不让AI写进正文 ─────────────────────
_NEGATIVE_WORDS = (
"神器、福音、救急单品、遮羞布、日常维稳、精简底妆、"
"不仅而且、焕发、守护、尽享、日常维稳、"
"按头安利、绝绝子、闭眼冲、杀疯了、YYDS"
)
# ── emoji 规则适度有表情倩倩姐2026-06-08拍板像真人发的小红书──────────
_EMOJI_RULES = """
【emoji表情适度有表情必须遵守
- 卖点小标题前加 emoji✅ 卖点1 / ✨ 卖点2 / 🌿 卖点3每条卖点1个符合语义
- 正文段落可点缀少量 emoji 烘托情绪(如 🥹 😭 🤍 💛每段最多1-2个不堆砌
- 结尾话题标签前后带表情,如 "#好物分享 🛒"
- emoji 服务情绪和分点,不要每句都加;整条正文 emoji 总量控制在 6-12 个
- 常用小红书 emoji 池:✅✨🌿💧📦🔍💛🤍🥹😭🛒💯(按语义选,不乱用)
""".strip()
# ── Q2 5步框架 + Q3 四段结构 ─────────────────────────────────────────────
_STRUCTURE_RULES = """
【5步框架必须严格遵循
① Hook暴击低价/痛点:第一句戳中场景或价格锚点,吊足读者好奇
② 痛点共鸣2-3句描写使用前的真实困境结合本条的情绪触发并按北哥要害②召集多类人
③ 救星登场:自然带出产品,口吻是"碰巧发现/朋友安利/囤货时顺手",不是"推荐给你"
④ 卖点罗列每条加✅小标题3条以内卖点翻译成使用感受不是功效列表
⑤ 收尾(每条必须从下方策略池随机选一种,同批次不得重复同一种,禁止"东西放这了/买不买跟我没关系"此类固定句式):
【收尾策略池·每条选不同策略】
A·留白式感受只说自己现在的状态不提买不买"反正现在出门我都带着它,心里踏实多了"
B·反问读者把感受抛回给读者"你们有没有那种一用就回不去的小东西?"
C·场景延续把故事延伸到未来某个细节"下次再有人问起,我就知道该推荐啥了"
D·克制回购暗示轻描淡写说自己行为"第一份用完了,已经在备第二份"
E·纯记录收笔像日记最后一句不引导不评价"大概就这样,记录一下"
F·引导搜索仅在有品牌词时使用自然提一句"感兴趣可以搜搜『{品牌词}",不催单
【正文叙事骨架(开篇方式由本条叙事主线决定,禁止三套同一开头)】
段1·开篇分叉按本条叙事主线三选一这是三套拉开差异的关键
- 痛点先行→开篇即戳使用前的困境/情绪(结合主切入人群维度,按要害②召集多类人)
- 场景先行→开篇先白描一个真实高频使用场景,痛点在场景里自然带出,不喊口号
- 成分背书先行→开篇用成分原理/测评对比视角切入("为什么有用/亲测对比"),痛点后置
段2·实测记录真实使用过程带上本条的真实小波折真实感来源用第三方视角背书北哥要害③
段3·种草核心产品带来的变化用感受描述而非功效声称核心成分逐条对应痛点北哥要害④
段4·引导收尾从收尾策略池随机选一种佛系口吻不强推末尾带精准购买意向标签北哥要害⑤
※ 卖点罗列的措辞/顺序/emoji 三套之间禁止雷同;身边人种草/被夸被问这类桥段三套不得共用同一套。
【字数】正文350-400字不含标题tags3-5处段落空行增强可读性
""".strip()
# ── Q8 标题公式5类结构每批次覆盖不同类型不重复──────────────────────
_TITLE_FORMULA = """
【标题公式5类每条用不同类型禁止同批重复
特征型:「{适配特征}+用了{产品}+{感受}」例→"久坐党用了这把椅子,腰一整天不酸了"
价格型:「{价格锚点}+{产品}+{效果感受}」例→"三位数买到大牌平替,用完第一份就回购"
场景型:「{使用场景}+{产品}+{可感知变化}」例→"通勤路上靠这杯,到工位整个人都醒了"
夸张型:「疑问/感叹+{夸张感受}+{产品}」例→"这什么神仙东西,用一次就回不去了"
标题党型:「{反常识/意外信息}+真相是{翻转}」例→"被夸最近状态好,没说的是我换了它一个月"
硬性约束标题≤20字禁止出现"绝绝子/YYDS/杀疯了";不直接写功效词(美白/祛斑等)。
硬性约束·标题必须有具体锚点:禁止"有点反差/真的绝/太惊艳/有点东西"这类空泛无信息量的标题,
必须落到一个具体的人群/场景/数字/反差细节上(读者扫一眼就知道讲什么、关我什么事)。
""".strip()
_ANTI_AI = f"""
【反AI味必须遵守】
- 禁止固定开篇套话:不许"姐妹们/宝子们/今天给大家分享"开头
- 禁止以下AI味词出现在正文{_NEGATIVE_WORDS}
- 禁止人群重叠:{'{count}'}条文案主切入人群维度不能重复靠Q1切入配方按条轮转保证
- 禁止场景重复同批次文案开篇场景不能雷同不能N条都挤在同一个使用场景
- 避免生硬推销词:按头安利/绝绝子/闭眼冲 不能出现
""".strip()
# ── 北哥出单强化层·五大要害(实测出单命门,与富贵框架叠加,冲突以本层为准)──
_BEIGE_FIVE = """
【北哥出单强化层·五大要害(最高优先级,与上面框架叠加,冲突时以本层为准)】
要害①·标题广覆盖+悬念钩子:标题要罩住尽量多的人,不要一上来就锁死单一窄人群
(只喊一类窄人群会把其他潜在人群挡在外面);用悬念/反差/疑问做钩子留个扣子让人想点进来;
精准筛人交给标签层,不靠标题筛人。
要害②·开篇召集痛点:正文要把这个产品能解决的痛点尽量多列几个、召集多类人
"不管你是…还是…"的覆盖感),让更多人对号入座,不要只写一类人的一个痛点。
注意:召集痛点不必都堆在第一句——开篇方式由本条的叙事主线决定(见下方叙事主线),
痛点先行的可开篇即戳痛点,场景先行/成分背书先行的则在叙事展开中自然召集多类人,
绝不能因为这条要害就把三套都写成同一个"赶时间/痛点"开头。
要害③·第三方闺蜜真实背书:种草借第三方视角(闺蜜/同事/家人说、被人问、别人推荐),
"别人替你证言"的真实感,不是自己反复夸自己回购多少次;自夸味=广告味=掉信任。
要害④·成分逐条对应痛点每个核心成分都明确对上它解决的那个痛点成分1→痛点1成分2→痛点2
逐条映射讲清"为什么有用",翻译成用户能感知的结果,不堆术语、不把成分名堆在一起不解释。
要害⑤·精准购买意向标签埋词tags 不要凑流量泛词(#好物分享#真实测评 这类谁都能用的大词价值低),
要埋"有明确购买意向的人才会搜"的精准词——目标人群+品类+核心卖点/场景组合成精准长尾标签,
至少2-3个精准埋词。
这5条是北哥实测出单的命门务必逐条命中。
""".strip()
# ── 系统 prompt合并Q2/Q3/Q4/Q7/Q8 + 北哥五段数据层走build_prompt动态注入──
COPY_SYSTEM = f"""{_PERSONA}
合规红线(全品类通用):
- 禁用"美白、祛斑、速效、医用、药妆",不得暗示治疗或改善疾病
- 不得承诺效果不得出现before/after变白对比暗示
- 图文合规避免社交App界面、点赞评论等截图元素
{_ANTI_AI.format(count="N")}
{_BEIGE_FIVE}
{_TITLE_FORMULA}
{_EMOJI_RULES}
{_STRUCTURE_RULES}
返回纯JSON数组每条字段
title标题≤20字/ content正文严格350-400字按emoji规则适度带表情/ tagslist3-5个#话题)
angle本条角度标签/ coverTitle封面大字≤10字/ imageBrief配图方向
硬性格式只输出JSON不要markdown代码块字符串内用中文引号「」。"""
# ── 第2环标杆爆款配方 → 文案 prompt借方法层结构禁抄竞品原话──────────────
_BM_DIM_LABELS: list[tuple[str, str]] = [
("title_formula", "标题公式"),
("opening_hook", "开篇钩子"),
("content_structure", "内容结构"),
("selling_point_style", "卖点表达风格"),
("emotion_tone", "情绪基调"),
("topic_tags", "话题标签思路"),
]
def _build_benchmark_block(refs: list[dict]) -> str:
"""把标杆 8维配方渲染成 prompt 块。只借'方法结构',硬约束禁抄竞品品牌/功效原话。"""
if not refs:
return ""
lines: list[str] = []
for i, f in enumerate(refs[:3], 1): # 最多取3条标杆避免prompt膨胀
dims = [f"{label}=「{str(f.get(key, '')).strip()}"
for key, label in _BM_DIM_LABELS if str(f.get(key, "")).strip()]
if dims:
lines.append(f" 标杆{i}" + "".join(dims))
if not lines:
return ""
body = "\n".join(lines)
return (
"\n【对标爆款配方参考(只借方法层结构,绝不照抄)】\n"
f"{body}\n"
"硬性约束:仅参考上面的标题套路/开篇方式/结构节奏/情绪基调来组织本产品文案;"
"禁止照搬竞品品牌名、产品名、功效原话或具体数字;产品信息一律以本产品为准。"
)
def build_prompt(product: dict, count: int, extra_rules: str = "",
strategy_narrative: str = "") -> str:
"""
组装文案生成 user_prompt。
数据层product 动态注入name/selling_points/style_tone/text_angles/custom_prompt
方法层:已在 COPY_SYSTEM 固定,这里只注入产品数据+随机变量
strategy_narrative三套正交叙事主线A痛点/B场景/C成分由调用方按套传入
"""
name = product.get("name", "产品")
selling = "".join(product.get("selling_points") or ["核心卖点待录入"])
style = product.get("style_tone", "素人日常分享风")
angles = product.get("text_angles") or []
custom = (product.get("custom_prompt") or "").strip()
brand_kw = (product.get("brand_keyword") or "").strip()
# Q1每条按index轮转主人群维度+随机情绪/波折,传给模型作切入约束
combos = [_pick_combo(i) for i in range(count)]
combos_text = "\n".join(
f"{i+1}条:主切入人群维度=「{c['identity']}」·情绪触发=「{c['emotion']}」·真实小波折=「{c['flaw']}"
for i, c in enumerate(combos)
)
angle_hint = f"文案角度要覆盖:{''.join(angles)}(每条用不同角度)。" if angles else ""
brand_rule = (
f"每条正文中自然植入品牌词「{brand_kw}」至少一次(融入语境,不生硬)。"
f"标题以悬念/钩子优先(北哥要害①),品牌词不必硬塞进标题;"
f"若要进标题须自然不抢戏,禁止把品牌词堆在标题开头当详情页商品名(如'{brand_kw}{name}'式)。"
) if brand_kw else ""
benchmark_block = _build_benchmark_block(product.get("benchmark_refs") or [])
# strategy_narrative 是三套拉开差异的核心,加强标记顶到最高权重
narrative_block = (
f"\n========== 本套叙事主线(最高优先级·决定开篇与全文走向,禁止偏离)==========\n"
f"{strategy_narrative}\n"
f"==============================================================\n"
if strategy_narrative else ""
)
lines = [
f"产品:{name}",
f"核心卖点(必须翻译成用户能感知的生活化利益,禁止直接列功效/参数词;翻译范例:技术参数'XX配方/XX材质''用起来的某个具体好处''大容量''出门一整天都不用补'{selling}",
f"风格调性:{style}",
narrative_block,
angle_hint,
brand_rule,
custom,
benchmark_block,
f"\n【Q1切入配方·每条主切入人群维度不同模型按本产品把维度填成具体人群】",
combos_text,
extra_rules,
f"\n请严格按5步框架+叙事骨架生成 {count}开篇方式必须服从上方叙事主线每条350-400字返回纯JSON数组。",
]
return "\n".join(l for l in lines if l)
def parse_json_array(raw: str) -> list[dict]:
"""从模型输出提取 JSON 数组(容错 markdown 包裹)"""
text = re.sub(r"```(?:json)?", "", raw).strip()
start, end = text.find("["), text.rfind("]")
if start == -1 or end == -1:
return []
try:
return json.loads(text[start:end + 1])
except json.JSONDecodeError:
return []
def build_local_drafts(product: dict, count: int) -> list[dict]:
"""本地模板兜底(保证永不空手,遵循四段结构)
角度从 text_angles 循环取保证N条角度各不同不被 dedupe_copies 吞掉)
"""
name = product.get("name", "产品")
points = product.get("selling_points") or ["使用方便", "真实可感知"]
# 从产品档案取角度池,不够就用通用角度兜底,保证每条都不同
_fallback_angles = ["生活场景型", "成分分析型", "使用感受型", "性价比型", "痛点切入型"]
angles_pool = product.get("text_angles") or _fallback_angles
for i in range(count):
c = _pick_combo(i)
# 循环取不同角度(角度相同的两条会被 dedupe_copies 过滤掉,所以必须不重复)
angle = angles_pool[i % len(angles_pool)]
yield {
"title": f"发现一个{name}{angle}用法",
"content": (
f"最近开始用{name}{c['emotion']}\n\n"
f"用了一段时间,{points[i % len(points)]}这点最让我意外。\n\n"
f"说个小波折:{c['flaw']},后来才摸到感觉。\n\n"
f"反正现在用顺手了。✅"
),
"tags": [f"#{name}", "#真实测评", "#好物分享"],
"angle": angle,
"coverTitle": f"{name}{angle}",
"imageBrief": "封面产品近景,内页核心卖点+真实使用场景。",
}