Files
yuzhiran 45ad5443c1 feat(prompt): inject brand tone into 6 platform prompts
Section expansion: zhihu (unique data + debate), wechat (tech-humanity reflection), xiaohongshu (real usage data) Title optimize: all 3 platforms aligned with new brand direction

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:24:38 +08:00

261 lines
20 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""
Prompt 配置加载器
架构:DB 为主,代码仅作初始种子。
- _PROMPT_DEFAULTS: 仅用于首次自动写入 DB(种子数据),不作为运行时 fallback
- prompt_configs 表: 运行时唯一来源,修改 prompt 直接改 DB
- DB 不可用时: 退化到代码默认值(仅用于紧急回退,不是日常模式)
新增 prompt 流程:
1. 在 _PROMPT_DEFAULTS 添加定义
2. 重启后自动补入 DB(仅当该 key 不在 DB 中时)
3. 后续修改直接在 DB 操作,不再改代码
修改 prompt 流程:
直接 UPDATE prompt_configs SET content = '...' WHERE key = '...';
"""
import os, sys
from pathlib import Path
from typing import Dict, Any, Optional
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / 'platform' / 'backend'))
# ============================================================
# 种子数据(初始默认值)
# 修改 prompt 请直接在 DB 操作,不要改这里
# 仅在表里没有该 key 时用于首次填充
# ============================================================
_PROMPT_DEFAULTS = {
"topics_trends": {
"content": "你是中文互联网趋势分析师。今天是{date},请列出{domains}领域当前最值得创作的8个话题。\n\n输出要求:\n- 基于真实热点事件/数据/趋势,不要编造\n- 每个话题需包含:\n - \"domain\": 所属领域\n - \"topic\": 话题名称(15字内,有信息量)\n - \"reason\": 为什么现在讨论这个(1句话,有具体事件或数据支撑)\n - \"hot_keywords\": 3-5个搜索关键词(含1-2个长尾词)\n - \"platform\": 最适合平台(知乎/小红书/微信/多平台)\n - \"seo_angle\": 从什么角度切入能获得搜索流量(1句话)\n\n只输出 JSON 数组,不要其他文字。",
"temperature": 0.7, "max_tokens": 3000,
"variables": ["date", "domains"],
},
"topic_generate": {
"content": "你是一个内容策略师。基于以下信息,为「{target_category}」类别生成一个高质量选题。\n\n{data_section}\n{existing_hint}\n{trend_context}\n\n选题要求:\n- 从真实用户痛点出发,不写假大空的话题\n- 有独特观点,让人看完想转发\n- 包含SEO关键词潜力\n\n输出 JSON\n{\n \"title\": \"标题(20字内,含核心关键词)\",\n \"core_concept\": \"核心观点(一句话说清独特价值)\",\n \"audience_pain\": \"受众痛点(真实用户的困惑/焦虑/需求)\",\n \"unique_angle\": \"差异化切入点\",\n \"format\": \"内容形式:趋势洞察/实操指南/对比分析/案例解读\"\n}\n只输出 JSON,不要其他文字。",
"temperature": 0.6, "max_tokens": 2000,
"variables": ["target_category", "data_section", "existing_hint", "trend_context"],
},
"topic_selector_gaps": {
"content": "你是一个敏锐的内容策略师,擅长将热点转化为有价值、有传播力的选题。以下热点当前未覆盖,请为每个热点生成选题建议。\n\n注意:选题要兼具SEO价值(能在搜索中被找到)和社交传播力(能在平台引发讨论)。\n\n热点列表:\n{gaps}\n\n每个选题需包含:\n- \"title\": 标题(20字内,含核心关键词)\n- \"field\": 所属领域\n- \"core_concept\": 核心观点(一句话说清独特价值)\n- \"audience_pain\": 受众痛点(真实用户的困惑/焦虑/需求)\n- \"unique_angle\": 独特视角(差异化切入点)\n- \"target_platform\": 最适合平台(知乎/小红书/微信/多平台)\n- \"estimated_search_volume\": 搜索热度(高/中/低)\n\n只输出 JSON 数组,不要其他文字。",
"temperature": 0.6, "max_tokens": 2000,
"variables": ["gaps"],
},
"section_expansion_zhihu": {
"content": "写「{topic_title}」文章中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写400-800字,3-6段。每段围绕一个论点展开:先摆独家数据/一手案例,再分析深层原因,最后说清这跟读者有什么关系。\n\n注意:\n- 每个论点必须有具体数据或独家案例支撑——自采数据、一手实验对比、行业报告引用优先。不要只用公开信息\n- 用「你」称呼读者,像是在知乎回答里跟人讨论\n- 从具体问题或反直觉现象切入,不要以「引言/总结/核心观点」这类标签开头\n- 不只是介绍这个工具/技术,而是探讨它如何改变了人的行为方式或认知\n- 回答「所以呢」——这条信息对读者有什么用,有什么可操作的启发\n- 如果这个话题有争议点,不要回避,亮出你的立场\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些表达\n- 来源放在文末括号内\n- 纯段落,不要列表/标题/序号标记",
"temperature": 0.78, "max_tokens": 4000,
"variables": ["topic_title", "section_title", "date", "content"],
},
"section_expansion_wechat": {
"content": "写「{topic_title}」文章中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写300-500字,用「我」的口吻写一个真实的困惑、顿悟或观察——不是测评工具,而是记录这个技术/趋势如何改变了你的一种习惯、认知或生活方式。\n\n注意:\n- 从个人经历或感受切入,不是介绍工具功能,而是讲这个工具让你想到了什么、改变了什么\n- 每个观点背后要有一个具体故事或观察,虚实结合\n- 段与段之间有情绪的起承转合,不是逻辑推理\n- 读起来像一个人在深夜和朋友聊天时的反思,不是一篇产品测评\n- 可以有困惑和不确定——真实比正确重要\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些套路\n- 纯段落,不要标题/列表/序号标记",
"temperature": 0.8, "max_tokens": 3000,
"variables": ["topic_title", "section_title", "date", "content"],
},
"section_expansion_xiaohongshu": {
"content": "写「{topic_title}」笔记中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写100-200字,1-3小段。每段1-2句,直接给干货。\n\n注意:\n- 直接给结论或方法,不要铺垫\n- 每个结论后面跟一个具体数据/对比/结果(亲测可用才有说服力)\n- 可以用emoji(1个/段,不要堆砌)\n- 像博主在分享亲测有效的经验,有真实的「我试过」感\n- 不要讲道理,只讲「是什么」和「怎么做」\n- 用「你」或直接省略主语\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些套路\n- 纯段落,不要列表/标题/序号标记",
"temperature": 0.75, "max_tokens": 2000,
"variables": ["topic_title", "section_title", "date", "content"],
},
"title_optimize_zhihu": {
"content": "你是知乎老用户,经常上热榜。给这篇文章起3个标题候选:\n\n文章主题:{title}\n核心观点:{core}\n受众痛点:{pain}\n领域:{field}\n\n感觉像在跟人推荐一篇值得辩论的观点帖,不是在推荐一篇工具文。信息要密、20字内、有明确立场、有观点交锋感、让人想点进来说两句。不要「如何」开头。如果话题有争议性,在标题里亮出你的立场。\n\n每行一个,不要编号。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core", "pain", "field"],
},
"title_optimize_wechat": {
"content": "你是公众号编辑,擅长写有温度、有人味的文章。这篇文章面向{field}领域的读者,帮起3个标题:\n\n文章主题:{title}\n核心观点:{core}\n\n标题方向是「科技时代的个人叙事」——不是推荐工具,而是分享一个真实的人与技术之间的故事。要有身份代入感(打工人/30岁+/普通上班族/家长)、有情绪钩子(困惑/反常识/顿悟/后悔)、含微信搜索关键词。15-25字,口语化,越具体越好。参考:「AI帮我写周报的第3个月,我终于发现哪里不对了」\n\n每行一个,不要编号。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core", "field"],
},
"title_optimize_xhs": {
"content": "你是小红书博主,擅长写有数据、有结果感的干货笔记。给这篇笔记起3个标题:\n\n笔记主题:{title}\n核心:{core}\n\n每条18字以内,有具体场景和结果感(含数字/时间/效果)。可以加1个精确emoji(不要🔥💥❌❓)。参考:「两年换了8个AI工具,最后只留这3个」这种风格。\n\n每行一个,不要编号。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core"],
},
"research_summary": {
"content": "你是一个行业研究员+内容策略师。今天是{now},当前年份:{year}年。\n\n基于以下选题和相关案例,写出能支撑文章核心观点、对读者真正有用的研究发现。\n\n选题:\n标题:{title}\n领域:{field}\n核心观点:{core}\n受众痛点:{pain}\n独特视角:{angle}\n{search_section}\n\n相关案例({n}个):\n{cases_text}\n\n输出要求(按顺序):\n1. 国内外最新热点关联:与本选题相关的{year}年最新趋势,有具体事件或数据\n2. 3-5个有说服力的论据:每个论据含案例+数据+来源URL\n3. 受众痛点深挖:读者为什么会关心这个\n4. SEO关键词建议:3-5个关键词(含长尾词),搜索意图说明\n\n直接输出 Markdown 格式的研究笔记,不要输出其他说明。",
"temperature": 0.7, "max_tokens": 4000,
"variables": ["now", "year", "title", "field", "core", "pain", "angle", "search_section", "n", "cases_text"],
},
"outline_generation": {
"content": "你是一个资深内容编辑,擅长设计读者爱看+搜索引擎友好+平台愿意推荐的推文结构。\n\n今天是{date}。当前年份:{year}年。\n\n选题信息:\n标题:{title}\n领域:{field}\n核心观点:{core}\n受众痛点:{pain}\n独特视角:{angle}\n\n研究笔记:\n{cases_summary}\n\n重点布局的SEO关键词(在章节中自然融入):\n{seo_keywords}\n\n大纲要求:\n- 5-8章,每章有完整段落要点(非单句)\n- 结构递进:认知升级型或问题解决型\n- 每章标题自带信息量+好奇心,不要「引言」「总结」这类通用标题\n- 每章至少自然融入1个上述SEO关键词,涉及相关搜索意图\n- 每章的要点必须是2-4句有内容的段落,不是一行关键词\n- 开头从具体场景切入,不要空洞的开场白\n- 把「独特视角」融入各章,而不是单独列\n- GEO(生成式搜索优化):每章包含一个可引用的数据点或来源,增加被AI搜索引用的概率\n\n输出格式:每章以「## 标题」开头,下面跟2-4段要点文字。\n不要输出其他说明。",
"temperature": 0.7, "max_tokens": 4000,
"variables": ["date", "year", "title", "field", "core", "pain", "angle", "cases_summary", "seo_keywords"],
},
"compliance_fix": {
"content": "你是一个专业的内容合规与质量优化助手。以下文章存在需要修复的问题,请逐一修复并输出完整HTML。\n\n需修复的问题:\n{issues_desc}\n\n原文:\n{html}\n\n修复要求:\n- 只修复上述问题,不改变文章结构和核心内容\n- 保持<h2>, <h3>, <p>等标签结构不变\n- 替换敏感词时选择意思相近的替代词,不删节重要信息\n- AI套话:直接删除或改写「首先其次最后」「总的来说」「值得注意的是」「综上所述」等模式\n- 人称混用:统一为「你」\n- 缺少配图:在关键位置插入 <p></p> 空段落占位,配图由后续流程处理\n- 缺少互动/收藏引导:在文末自然加入(不要生硬)\n- 段落过长:将超过300字的段落拆分为2-3段\n\n输出完整的HTML,只输出HTML内容,不要其他文字说明。",
"temperature": 0.3, "max_tokens": 8000,
"variables": ["issues_desc", "html"],
},
"compliance_polish": {
"content": "你是一个专业的内容润色助手。请在保持原文事实和结构的前提下提升表达的自然感。\n\n原文:\n{html}\n\n要求:\n- 保持所有事实、数据、章节结构、HTML标签不变\n- 让语言更像是人写的,不是AI生成的\n- 删除或改写AI常见表达:「首先其次最后」「总的来说」「值得注意的是」「综上所述」「我们不难发现」「由此可见」\n- 短句化,读起来更流畅\n- 保持原文的段落划分和层次\n\n输出完整的HTML,不要其他文字说明。",
"temperature": 0.4, "max_tokens": 8000,
"variables": ["html"],
},
"sources_optimization": {
"content": "你是一个内容策略分析师。分析当前中文互联网AI与科技人文领域的真实热点,与以下配置进行对比。\n\n当前配置的类别({n}个):\n{cat_names}\n\n当前配置的信息源({n2}个):\n{src_summary}\n\n请完成:\n1. 评估每个类别是否仍符合{year}年中国市场真实热点(保留/淘汰/合并)\n2. 评估每个信息源是否可在国内正常访问\n3. 建议新增的类别(最多2条)\n4. 建议新增的信息源搜索词(最多3条,含具体搜索词)\n\n输出 JSON\n{\n \"category_assessment\": [{\"name\": \"...\", \"status\": \"保留/淘汰/合并\", \"reason\": \"...\"}],\n \"source_assessment\": [{\"source\": \"...\", \"accessible\": true/false, \"note\": \"...\"}],\n \"new_categories\": [{\"name\": \"...\", \"reason\": \"...\"}],\n \"new_search_queries\": [{\"query\": \"...\", \"target\": \"...\"}]\n}\n只输出JSON。",
"temperature": 0.5, "max_tokens": 3000,
"variables": ["n", "cat_names", "n2", "src_summary", "year"],
},
"topic_manual_analyze": {
"content": "你是一个专业的内容策略师。用户提供了一个选题思路,请分析并提炼为结构化的选题信息。\n\n用户输入的原始内容:\n{raw_input}\n\n参考链接(如有):\n{reference_links}\n\n请输出 JSON(只输出 JSON,不要其他文字):\n{\n \"title\": \"优化后的选题标题(20字内,含核心关键词,有吸引力)\",\n \"format\": \"内容形式(趋势洞察/实操指南/对比分析/案例解读/观点讨论)\",\n \"core_concept\": \"核心观点(一句话说清独特价值,20字内)\",\n \"audience_pain\": \"受众痛点(目标读者的真实困惑或需求,20字内)\",\n \"unique_angle\": \"差异化切入点(与常见文章不同的视角,20字内)\",\n \"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]\n}",
"temperature": 0.6, "max_tokens": 1500,
"variables": ["raw_input", "reference_links"],
},
"tags_generation": {
"content": "为以下文章生成{platform}标签(5-8个)。\n\n标题:{title}\n领域:{field}\n核心观点:{core}\n\n要求:\n- 每个标签2-5字\n- 包含1-2个搜索流量词(用户在{platform}会搜的词)\n- 包含1-2个热门话题词\n- 标签要有层次:大领域→小话题→具体场景\n- 不要重复意思相近的标签\n\n直接输出标签,空格分隔。不要输出思考过程和其他文字。",
"temperature": 0.3, "max_tokens": 500,
"variables": ["platform", "title", "field", "core"],
},
}
_DB_CACHE: Dict[str, Dict[str, Any]] = {}
_CACHE_LOADED = False
_DB_AVAILABLE = False # True if DB was successfully loaded at least once
def _seed_missing_prompts():
"""将代码默认值中不存在的 prompt 自动补入 DB"""
try:
if os.getenv('USE_POSTGRES', 'true') == 'true':
from app.database import SessionLocal
from app.models import PromptConfig
db = SessionLocal()
try:
existing_keys = {p.key for p in db.query(PromptConfig).all()}
for key, cfg in _PROMPT_DEFAULTS.items():
if key not in existing_keys:
pc = PromptConfig(
key=key,
content=cfg['content'],
temperature=cfg.get('temperature'),
max_tokens=cfg.get('max_tokens'),
enabled=True,
module_id='default',
variables=cfg.get('variables', []),
)
db.add(pc)
print(f"[prompt_loader] auto-seeded prompt: {key}")
if any(key not in existing_keys for key in _PROMPT_DEFAULTS):
db.commit()
finally:
db.close()
except Exception:
pass
def _ensure_db_loaded():
global _DB_CACHE, _CACHE_LOADED, _DB_AVAILABLE
if _CACHE_LOADED:
return
# 自动补缺:将代码默认值中不存在的 prompt 写入 DB
_seed_missing_prompts()
try:
if os.getenv('USE_POSTGRES', 'true') == 'true':
from app.database import SessionLocal
from app.models import PromptConfig
db = SessionLocal()
try:
for p in db.query(PromptConfig).filter(PromptConfig.enabled == True).all():
_DB_CACHE[p.key] = {
"content": p.content,
"temperature": p.temperature,
"max_tokens": p.max_tokens,
}
_DB_AVAILABLE = True
finally:
db.close()
except Exception:
pass
_CACHE_LOADED = True
def get_prompt(key: str, **kwargs) -> str:
"""获取 prompt 内容
优先级:DB → 代码默认值(仅 DB 不可用时)→ 空字符串
"""
_ensure_db_loaded()
cleaned = []
for k, v in kwargs.items():
cleaned.append((k, str(v)))
kwargs = dict(cleaned)
if key in _DB_CACHE:
content = _DB_CACHE[key]["content"]
for k, v in kwargs.items():
content = content.replace("{" + k + "}", str(v))
return content
# 仅 DB 不可用时回退代码默认值
if not _DB_AVAILABLE and key in _PROMPT_DEFAULTS:
content = _PROMPT_DEFAULTS[key]["content"]
for k, v in kwargs.items():
content = content.replace("{" + k + "}", str(v))
return content
return ""
def get_prompt_params(key: str) -> Dict[str, Any]:
_ensure_db_loaded()
if key in _DB_CACHE:
return {
"temperature": _DB_CACHE[key].get("temperature"),
"max_tokens": _DB_CACHE[key].get("max_tokens"),
}
if not _DB_AVAILABLE and key in _PROMPT_DEFAULTS:
return {
"temperature": _PROMPT_DEFAULTS[key].get("temperature"),
"max_tokens": _PROMPT_DEFAULTS[key].get("max_tokens"),
}
return {}
def seed_prompts(force: bool = False):
"""手动触发种子同步(用于 CLI 或 API)
force=True: 用代码默认值覆盖 DB
force=False: 仅补充 DB 中不存在的 key
"""
try:
from app.database import SessionLocal
from app.models import PromptConfig
db = SessionLocal()
try:
existing = {p.key: p for p in db.query(PromptConfig).all()}
for key, cfg in _PROMPT_DEFAULTS.items():
if force or key not in existing:
if key in existing:
p = existing[key]
p.content = cfg['content']
p.temperature = cfg.get('temperature')
p.max_tokens = cfg.get('max_tokens')
else:
p = PromptConfig(
key=key,
content=cfg['content'],
temperature=cfg.get('temperature'),
max_tokens=cfg.get('max_tokens'),
enabled=True,
module_id='default',
variables=cfg.get('variables', []),
)
db.add(p)
db.commit()
finally:
db.close()
return {"ok": True, "action": "force" if force else "seed"}
except Exception as e:
return {"ok": False, "error": str(e)}
def reload_prompts():
global _CACHE_LOADED, _DB_CACHE, _DB_AVAILABLE
_CACHE_LOADED = False
_DB_CACHE = {}
_DB_AVAILABLE = False
_ensure_db_loaded()