Files
yu-zhi-ran/scripts/prompt_loader.py
T

246 lines
19 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})中文互联网上最值得创作的10个话题。\n\n要求:\n1. 覆盖领域:{domains}\n2. 从真实用户角度出发\n3. 每个话题需包含:\n - \"domain\": 领域\n - \"topic\": 话题名称\n - \"reason\": 为什么现在讨论这个(1句话,有具体事件/数据支撑)\n - \"hot_keywords\": 3-5个搜索词(含1-2个长尾词)\n - \"platform\": 最适合分发的平台(知乎/小红书/微信/多平台)\n - \"seo_angle\": 从什么角度切入能获得搜索流量(1句话)\n - \"engagement\": 高/中/低\n\n输出 JSON 数组。只输出 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输出一个选题,格式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{gaps}\n\n每个选题需包含:\n- \"title\": 标题(20字内,包含核心关键词,有吸引力)\n- \"field\": 所属领域\n- \"core_concept\": 核心观点(一句话说清独特价值)\n- \"audience_pain\": 受众痛点(真实用户的困惑/焦虑/需求)\n- \"unique_angle\": 独特视角(差异化切入点,含SEO关键词潜力)\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【输出要求】\n输出3-6段纯粹、流畅的段落文字,每节内容根据平台需求控制在200-800字之间。\n\n格式:\n- 禁止任何标题/列表/格式标记(#、-、*、1.、**等)\n- 每段3-5句,段间空行分隔\n- 用「你」或「我」视角,自然口语化。全文统一使用「你」称呼读者,不要混用「你们」\n- **禁止以「引言」「核心观点」「受众痛点」「总结」「开头钩子」「独特视角」这类结构标签开头**——直接从场景或痛点切入\n\n内容要求(让文章在各平台能被推荐):\n- 开头直接切入场景或痛点,一句话抓住注意力,不要铺垫\n- 每个观点配具体案例或数据(用「据统计」「调研显示」等),不要空泛说理\n- 有独特判断和立场,避免正确废话\n- 回答「所以呢」——读者看完能带走什么\n- **避免AI套话**:不要出现「一个真实的XX案例很能说明问题」「说回到XX这件事」「这就是XX被XX后的样子」「如果你也XX」「值得注意的是」「首先其次最后」「综上所述」\n- 结尾有情绪感召力,让人想点赞/收藏/转发\n- 引用数据或案例时,在行内用(来源:报告/案例名称)标注\n\n直接输出段落正文,不要任何附加说明。\n\n【注意:如果你是写最后一节,在正文写完后加一行 --- 分隔,然后写 **参考资料**,每行一个来源:- 来源名称(简要说明)。前面几节不要加这个。】",
"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个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core", "pain", "field"],
},
"title_optimize_wechat": {
"content": "你是一个10万+公众号爆款标题专家。为以下文章起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个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
"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个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core"],
},
"research_summary": {
"content": "你是一个行业研究员+内容策略师,擅长从案例中发现真洞察+抢占热点的敏锐嗅觉,能判断什么内容对真实读者最有价值且正被市场热议。\n\n今天是{now}。当前年份:{year}年。\n\n基于以下选题和相关案例,写出能支撑文章核心观点、对读者真正有用的研究发现。\n\n## 选题\n标题:{title}\n领域:{field}\n核心观点:{core}\n受众痛点:{pain}\n独特视角:{angle}\n{search_section}\n## 相关案例({n}个)\n{cases_text}\n\n## 输出要求(按顺序):\n1. **国内外最新热点关联**:... **所有数据必须是{year-1}-{year}年最新数据,禁用一切过时数据**\n2. **核心发现**:2-3个真正有价值的洞察。每条需包含这个发现对读者意味着什么,以及支撑数据(附数据来源)\n3. **独特观点储备**:哪些角度别人没写过、可以讲出差异化?提供至少一个反向/冷门视角\n4. **SEO关键词建议**...\n5. **讨论点**:哪个观点最有争议或最可能引发讨论/转发/评论?\n6. **市场价值判断**...\n\n风格:说人话,直击要点,像资深编辑在给作者做 briefing。避免「首先其次最后」「综上所述」。直接输出内容,不要输出思考过程。",
"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### 结构\n- 5-8章,每章2-4个要点\n- 结构要有递进:要么认知升级型,要么问题解决型\n- 把独特视角和受众痛点融入各章,不单独列\n- 每章标题自带信息量+好奇心,不要「引言」「总结」这类通用标题\n- 开头要有\"钩子\"(hook)抓住读者,结尾要有可转发/收藏的总结\n\n### 数据与热点\n- **全文必须使用{year-1}-{year}年最新数据**,禁用一切过时数据\n- 每个观点必须配最新的国内外热点事件/数据/政策来佐证\n- 体现当前行业正在讨论的核心议题,拒绝泛泛而谈\n\n### 独特风格\n- 有自己的判断和立场,不是搬运观点\n- 每章至少一个\"反常识\"\"很少有人提\"的洞察\n- 避免同质化表达、老生常谈\n\n### SEO\n- H2/H3自然包含用户搜索时会用的短语\n- 确保大纲覆盖2-3个高价值搜索词,包含1个长尾词\n- 每章标题对用户搜索意图有回应\n\n### 市场价值\n- 读完每章读者能拿走一个实际有用的东西(方法/清单/思维框架/判断标准)\n- 避免「信息增量为零」的空洞章节\n\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- 替换敏感词时选择意思相近的替代词,不删节重要信息\n- 如果问题是AI套话,直接删除或改写那些词句\n- 如果问题是人称混用,统一为「你」\n- 如果问题是缺少配图,在关键位置插入 `<p></p>` 空段落占位,配图由后续流程统一处理",
"temperature": 0.3, "max_tokens": 8000,
"variables": ["issues_desc", "html"],
},
"compliance_polish": {
"content": "你是一个专业的内容润色助手。请润色以下文章,提升表达的自然感和可读性。\n\n原文:\n{html}\n\n要求:\n- 保持原文事实、数据、章节结构不变\n- 输出相同的HTML格式(保留<h2>, <h3>, <p>标签)\n- 提升表达的自然感,让它更像是人写的\n- 避免AI常见表达模式(「首先其次最后」「总的来说」「值得注意的是」等)\n- 短句化,读起来更流畅",
"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\": [{\"name\": \"...\", \"status\": \"保留/淘汰/替换\", \"reason\": \"...\"}],\n \"suggested_new_categories\": [{\"name\": \"...\", \"search_query\": \"...\", \"reason\": \"...\"}],\n \"suggested_new_sources\": [{\"name\": \"...\", \"type\": \"web_search\", \"query\": \"...\", \"focus\": \"...\"}],\n \"summary\": \"一句话总结本次优化建议\"\n}\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()