feat: 提示词和 AI 参数移至数据库管理

- AiSetting 模型新增 6 个提示词字段 + 3 个阶段温度字段
- main.py 迁移逻辑改为检测 ai_settings 表的新字段并填充默认值
- generate.py 从数据库读取生成提示词,去掉硬编码
- optimizer 从 API 读取各阶段提示词和温度,删除 prompts.py
- crawler 从 API 读取提取/改写提示词和温度,删除 prompts.py
- settings/active 端点去掉 token 认证(供爬虫/优化器使用)
- 后台设置页新增提示词编辑区和温度调节控件
- 新增 _ensure_default_settings 自动创建默认配置
This commit is contained in:
bwstudio
2026-06-13 17:27:09 +08:00
parent 6d015bb56d
commit db8cea02f8
11 changed files with 457 additions and 195 deletions
+80 -20
View File
@@ -1,35 +1,90 @@
"""LLM 处理:调用 NVIDIA NIMOpenAI 兼容 API)进行笑话提取、改写和分类"""
"""LLM 处理:调用 NVIDIA NIMOpenAI 兼容 API)进行笑话提取、改写。"""
import json
import os
import httpx
from openai import OpenAI
class AiService:
def __init__(self, api_base: str, api_key: str, model_name: str, temperature: float = 0.7, max_tokens: int = 2048):
self.client = OpenAI(base_url=api_base, api_key=api_key)
self.model = model_name
self.temperature = temperature
self.max_tokens = max_tokens
def __init__(self, api_base: str, username: str, password: str):
self.api_base = api_base.rstrip("/")
self.username = username
self.password = password
self.token = None
self.client = None
self.model = ""
self.ai_config = None
def _login(self) -> str:
resp = httpx.post(
f"{self.api_base}/api/auth/login",
json={"username": self.username, "password": self.password},
timeout=30,
)
resp.raise_for_status()
return resp.json()["access_token"]
def _get(self, path: str) -> dict | list:
resp = httpx.get(
f"{self.api_base}{path}",
headers={"Authorization": f"Bearer {self.token}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()
def setup(self):
"""登录并从 API 获取 AI 配置(含提示词和温度)"""
self.token = self._login()
self.ai_config = self._get("/api/admin/settings/active")
self.client = OpenAI(
base_url=self.ai_config["api_base"],
api_key=self.ai_config["api_key"],
)
self.model = self.ai_config["model_name"]
def _get_prompt(self, key: str, default: str) -> str:
if self.ai_config:
val = self.ai_config.get(key)
if val and val.strip():
return val
return default
def extract_jokes(self, page_content: str, known_types: list[str], known_crowds: list[str]) -> list[dict]:
"""从页面内容中提取笑话,返回结构化数据。"""
from crawler.prompts import EXTRACTION_SYSTEM_PROMPT, EXTRACTION_USER_PROMPT
system_prompt = self._get_prompt("crawler_extract_prompt",
"""你是一个笑话提取专家。从给定的网页文本中识别并提取所有笑话、幽默段子或有趣内容。
要求:
1. 只返回真正的笑话内容,不要提取普通文章或新闻
2. 每条笑话需要包含:title(简短标题)、content(完整笑话内容)、type(类型)、crowd(人群)
3. 如果网页中没有笑话,返回空数组 []
4. 永远返回合法的 JSON 格式,根节点为数组或包含 jokes 键的对象""")
user_prompt = EXTRACTION_USER_PROMPT.format(
page_content=page_content[:8000],
known_types=", ".join(known_types),
known_crowds=", ".join(known_crowds),
)
user_prompt = f"""网页内容:
---
{page_content[:8000]}
---
已知笑话类型:{', '.join(known_types)}
已知人群分类:{', '.join(known_crowds)}
请提取所有笑话,以 JSON 格式返回,示例:
[
{{"title": "程序员的幽默", "content": "程序员去相亲...", "types": ["谐音梗", "段子"], "crowds": ["职场", "大学生"]}},
{{"title": "...", "content": "...", "types": ["..."], "crowds": ["..."]}}
]
注意:types 和 crowds 是数组,可以填多个。
只返回 JSON,不要其他文字。"""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": EXTRACTION_SYSTEM_PROMPT},
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=self.temperature,
max_tokens=self.max_tokens,
temperature=self.ai_config.get("temperature", 0.7) if self.ai_config else 0.7,
max_tokens=self.ai_config.get("max_tokens", 2048) if self.ai_config else 2048,
)
raw = response.choices[0].message.content
@@ -37,13 +92,19 @@ class AiService:
def rewrite_joke(self, content: str) -> str:
"""润色单条笑话内容。"""
from crawler.prompts import REWRITE_SYSTEM_PROMPT, REWRITE_USER_PROMPT
system_prompt = self._get_prompt("crawler_rewrite_prompt",
"""你是一个幽默作家,负责润色和改写笑话。
要求:
1. 保持笑话的核心笑点不变
2. 语言更通顺、更幽默
3. 字数控制在原内容的 80%-120% 之间
4. 不要添加任何解释说明""")
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": REWRITE_SYSTEM_PROMPT},
{"role": "user", "content": REWRITE_USER_PROMPT.format(content=content)},
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"请润色以下笑话:\n\n{content}"},
],
temperature=0.8,
max_tokens=500,
@@ -60,7 +121,6 @@ class AiService:
jokes = data
return [j for j in jokes if isinstance(j, dict) and j.get("content")]
except json.JSONDecodeError:
# 尝试提取 markdown 代码块
if "```json" in raw:
raw = raw.split("```json")[1].split("```")[0]
elif "```" in raw:
+5 -7
View File
@@ -57,15 +57,13 @@ class Processor:
self.token = self._login()
print("[*] 登录成功")
ai_config = self._get("/api/admin/settings/active")
self.ai = AiService(
api_base=ai_config["api_base"],
api_key=ai_config["api_key"],
model_name=ai_config["model_name"],
temperature=ai_config.get("temperature", 0.7),
max_tokens=ai_config.get("max_tokens", 2048),
api_base=self.api_base,
username=self.username,
password=self.password,
)
print(f"[*] AI 配置: {ai_config['model_name']}")
self.ai.setup()
print(f"[*] AI 配置: {self.ai.model}")
self.types = self._get("/api/categories/types")
self.crowds = self._get("/api/categories/crowds")
-41
View File
@@ -1,41 +0,0 @@
"""AI 提示词模板。"""
# ===== 笑话提取 =====
EXTRACTION_SYSTEM_PROMPT = """你是一个笑话提取专家。从给定的网页文本中识别并提取所有笑话、幽默段子或有趣内容。
要求:
1. 只返回真正的笑话内容,不要提取普通文章或新闻
2. 每条笑话需要包含:title(简短标题)、content(完整笑话内容)、type(类型)、crowd(人群)
3. 如果网页中没有笑话,返回空数组 []
4. 永远返回合法的 JSON 格式,根节点为数组或包含 jokes 键的对象"""
EXTRACTION_USER_PROMPT = """网页内容:
---
{page_content}
---
已知笑话类型:{known_types}
已知人群分类:{known_crowds}
请提取所有笑话,以 JSON 格式返回,示例:
[
{{"title": "程序员的幽默", "content": "程序员去相亲...", "types": ["谐音梗", "段子"], "crowds": ["职场", "大学生"]}},
{{"title": "...", "content": "...", "types": ["..."], "crowds": ["..."]}}
]
注意:types 和 crowds 是数组,可以填多个。
只返回 JSON,不要其他文字。"""
# ===== 笑话改写 =====
REWRITE_SYSTEM_PROMPT = """你是一个幽默作家,负责润色和改写笑话。
要求:
1. 保持笑话的核心笑点不变
2. 语言更通顺、更幽默
3. 字数控制在原内容的 80%-120% 之间
4. 不要添加任何解释说明"""
REWRITE_USER_PROMPT = """请润色以下笑话:
{content}
只返回润色后的笑话文字,不要其他内容。"""