High priority: - Fix concurrent race condition for view_count/like_count (atomic update) - Add route request ID tracking to prevent race conditions - Filter get_joke by status=approved (no pending content leak) - Add error feedback for like button Performance: - Optimize random joke query (avoid full table sort) - Limit page_size max to 100 (DoS prevention) Medium: - Add localStorage quota error handling - Handle empty AI response gracefully - Fix generate content title extraction Low: - Add rejected_jokes to stats API - Update dashboard to show rejected count
71 lines
2.7 KiB
Python
71 lines
2.7 KiB
Python
"""LLM 处理:调用 NVIDIA NIM(OpenAI 兼容 API)进行笑话提取、改写和分类。"""
|
||
|
||
import json
|
||
import os
|
||
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 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
|
||
|
||
user_prompt = EXTRACTION_USER_PROMPT.format(
|
||
page_content=page_content[:8000],
|
||
known_types=", ".join(known_types),
|
||
known_crowds=", ".join(known_crowds),
|
||
)
|
||
|
||
response = self.client.chat.completions.create(
|
||
model=self.model,
|
||
messages=[
|
||
{"role": "system", "content": EXTRACTION_SYSTEM_PROMPT},
|
||
{"role": "user", "content": user_prompt},
|
||
],
|
||
temperature=self.temperature,
|
||
max_tokens=self.max_tokens,
|
||
)
|
||
|
||
raw = response.choices[0].message.content
|
||
return self._parse_json(raw)
|
||
|
||
def rewrite_joke(self, content: str) -> str:
|
||
"""润色单条笑话内容。"""
|
||
from crawler.prompts import REWRITE_SYSTEM_PROMPT, REWRITE_USER_PROMPT
|
||
|
||
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)},
|
||
],
|
||
temperature=0.8,
|
||
max_tokens=500,
|
||
)
|
||
return response.choices[0].message.content.strip()
|
||
|
||
def _parse_json(self, raw: str) -> list[dict]:
|
||
"""安全解析 LLM 返回的 JSON。"""
|
||
try:
|
||
data = json.loads(raw)
|
||
if isinstance(data, dict):
|
||
jokes = data.get("jokes") or data.get("items") or [data]
|
||
else:
|
||
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:
|
||
raw = raw.split("```")[1].split("```")[0]
|
||
try:
|
||
return json.loads(raw.strip())
|
||
except Exception:
|
||
return [] |