Files
yu-zhi-ran/scripts/prompt_loader.py
T
Yuzhiran Dev 6585909ffc feat: optimize all 13 prompts by function+scenario; fix topic_selector hardcode
Prompt optimizations:
- topics_trends: 10→8 topics, simplified output, anti-hallucination
- topic_generate: stronger structure, user-pain focus
- topic_selector_gaps: add SEO+social instruction, clean JSON output
- section_expansion: shorter (removed over-detailed AI套话 list), cleaner
- title_optimize_*: 3 titles per line (no numbering), cleaned up
- outline_generation: simplified structure, strong anti-generic-titles
- compliance_fix/polish: expanded fix types, stronger output requirements
- sources_optimization: cleaner JSON schema
- tags_generation: added 'no thinking output' instruction
- research_summary: simplified output structure
- clean_ai_verbosity/clean_thinking_patterns: unchanged (rule-based)

topic_selector.py: hardcoded prompt → get_prompt('topic_selector_gaps')
_PROMPT_DEFAULTS seeds updated to match DB
2026-05-27 11:07:56 +08:00

246 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.
#!/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": {
"content": "你是一个资深作者,正在写一篇关于「{topic_title}」的文章。请写「{section_title}」这一节。\n\n今天日期:{date}\n\n笔记要点:\n{content}\n\n输出要求(200-800字,3-6段):\n- 每段3-5句,段间空行分隔\n- 用「你」或「我」视角,自然口语化\n- 从具体场景或痛点切入,禁止以「引言」「核心观点」「总结」「开头钩子」这类标签开头\n- 每个论点配具体案例或数据,不要空谈道理\n- 回答读者「所以呢」——这对他有什么用\n\n格式:\n- 纯段落文字,不要标题/列表/标记(#、-、*、1.等)\n- 不要出现「首先其次最后」「总的来说」「值得注意的是」「综上所述」等AI套话\n- 来源URL请放在文末括号内",
"temperature": 0.75, "max_tokens": 3000,
"variables": ["topic_title", "section_title", "date", "content"],
},
"title_optimize_zhihu": {
"content": "你是一个知乎高赞标题专家。为以下文章起3个标题。\n\n文章主题:{title}\n核心观点:{core}\n受众痛点:{pain}\n领域:{field}\n\n要求:\n- 信息密度高,包含搜索关键词\n- 优先使用数字、对比、悬念、痛点\n- 20字以内\n- 不要「如何...」开头\n- 有独特判断和立场,能引发讨论\n- 制造「不点开就亏了」的紧迫感\n\n直接输出3个标题,每行一个,不要序号和说明。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core", "pain", "field"],
},
"title_optimize_wechat": {
"content": "你是一个公众号标题专家。为以下文章起3个标题。\n\n文章主题:{title}\n核心观点:{core}\n\n要求:\n- 包含身份标签(如「打工人」「30岁后」「普通上班族」)\n- 包含情绪钩子(焦虑/反常识/后悔/稀缺)\n- 包含微信SEO关键词\n- 15-25字,口语化\n- 忌笼统,越具体越好\n- 不要感叹号堆砌\n\n参考公式:\n- 「身份+痛点+方案」:打工人学了一堆AI工具,为什么还在加班?\n- 「反常识+数据」:用了AI效率反而更低了?73%的人掉进了这个坑\n- 「结果+身份」:每天省出2小时后,我才发现自己以前有多傻\n\n直接输出3个标题,每行一个,不要序号和说明。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core"],
},
"title_optimize_xhs": {
"content": "你是一个小红书标题专家。为以下文章起3个标题。\n\n标题:{title}\n核心观点:{core}\n\n要求:\n- 18字以内\n- 爆款公式:身份/场景+数字+结果,或痛点+方案+反差\n- 包含小红书SEO关键词\n- 1个精确emoji(不要用🔥💥❌❓这几个滥用的)\n- 有场景感、结果感\n- 忌笼统:不要「必看/收藏/码住/绝了」\n\n直接输出3个标题,每行一个,不要序号和说明。",
"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大纲要求:\n- 5-8章,每章有完整段落要点(非单句)\n- 结构递进:认知升级型或问题解决型\n- 每章标题自带信息量+好奇心,不要「引言」「总结」这类通用标题\n- 每章的要点必须是2-4句有内容的段落,不是一行关键词\n- 开头从具体场景切入,不要空洞的开场白\n- 把「独特视角」融入各章,而不是单独列\n\n输出格式:每章以「## 标题」开头,下面跟2-4段要点文字。\n不要输出其他说明。",
"temperature": 0.7, "max_tokens": 4000,
"variables": ["date", "year", "title", "field", "core", "pain", "angle", "cases_summary"],
},
"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": "你是一个内容策略分析师。分析当前中文互联网可持续生活领域的真实热点,与以下配置进行对比。\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"],
},
"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()