xhs_factory/llm_service.py
zhoujie 358b957f5d feat(llm): 增强 LLM 服务的健壮性与容错能力
- 新增模型降级机制,当主模型失败时自动尝试备选模型列表【FALLBACK_MODELS】
- 增强 `_chat` 方法,支持空返回检测、json_mode 回退和多重错误处理
- 重构 `_parse_json` 方法,实现五重容错解析策略以应对不同模型的输出格式
- 为 `generate_copy`、`generate_copy_with_reference` 和 `analyze_hotspots` 方法添加重试逻辑,在 JSON 解析失败时自动关闭 json_mode 重试

🔧 chore(config): 更新默认模型配置与安全令牌

- 将默认 LLM 模型从 `gemini-3-flash-preview` 更改为 `deepseek-v3`
- 更新 `xsec_token` 安全令牌

 feat(sd): 集成 ReActor 换脸功能并扩展人设主题池

- 在 `SDService` 中新增头像管理静态方法 (`load_face_image`, `save_face_image`) 和 ReActor 参数构建方法
- 为 `txt2img` 方法添加 `face_image` 参数,支持在生成图片时自动换脸
- 在 `main.py` 的 Web UI 中新增头像上传、预览与管理界面
- 扩展 `generate_images` 函数,支持根据复选框状态启用换脸功能
- 重构人设系统,为 24 种预设人设分别定义专属的【主题池】和【评论关键词池】,并实现人设切换时的自动联动更新
- 在自动化发布 (`auto_publish_once`) 和定时调度 (`_scheduler_loop`) 中集成换脸选项

📝 docs(main): 添加新图片资源

- 新增图片资源文件:`beauty.png`, `my_face.png`, `myself.jpg`, `zjz.png`
2026-02-09 23:08:10 +08:00

575 lines
27 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.

"""
LLM 服务模块
封装对 OpenAI 兼容 API 的调用,包含文案生成、热点分析、评论回复等 Prompt
"""
import requests
import json
import re
import logging
logger = logging.getLogger(__name__)
# ================= Prompt 模板 =================
PROMPT_COPYWRITING = """
你是一个真实的小红书博主,正在用手机编辑一篇笔记。你不是内容专家,你只是一个想认真分享的普通人。
【你的写作状态】:
想象你刚体验完某件事(试了一个产品/去了一个地方/学到一个技巧),打开小红书想跟朋友们聊聊。你不会字字斟酌,就是把感受写出来。
【标题规则】(严格执行)
1. 长度限制:必须控制在 18 字以内含Emoji绝对不能超过 20 字!
2. 像你发朋友圈的语气,口语化、有情绪感。可以用疑问句、感叹句、省略句
3. 可以加1-2个emoji但不要堆砌
4. 禁止广告法违禁词("第一" "" "顶级"等)
5. 好的标题示例:"后悔没早买!这个真的绝了" "姐妹们被我找到了" "求求你们别再踩这个坑了"
6. 避免AI感标题不要用"震惊!" "必看!" "干货"这种过于营销的开头
【正文规则——像说话一样写】:
1. 想象你在跟闺蜜/朋友面对面聊天,把她说的话打下来就对了
2. 正文控制在 400-600 字
3. 不要像写作文一样"首先、其次、最后",用碎碎念的方式自然展开
4. 可以有小情绪:吐槽、感叹、自嘲、开心炸裂都行
5. emoji不要每句话都有穿插在情绪高点就好一段文字2-4个emoji足够
6. 真人笔记特征:
- 会有"话说" "对了" "哦对" 这种口语转折
- 会有"不是我说" "真的会谢" "笑不活了"这种网络表达
- 会有不完整的句子、省略号、波浪号
- 段落长短不一,有的段就一句话,有的段会稍长
7. 绝对禁止:
"值得一提的是" "需要注意的是" "总的来说" "综上所述"
"作为一个xxx" "在这里给大家分享"
❌ 排比句、对仗工整的总结
❌ 每段都很整齐的1234结构
"小伙伴们" "宝子们" 等过度热情的称呼(偶尔一次可以)
8. 结尾加 5-8 个相关话题标签(#)
【绘图 Prompt】
生成 Stable Diffusion 英文提示词,适配 JuggernautXL 模型:
- 人物要求(最重要!):如果画面中有人物,必须是东亚面孔的中国人,使用 asian girl/boy, chinese, east asian features, black hair, dark brown eyes, delicate facial features, fair skin, slim figure 等描述,绝对禁止出现西方人/欧美人特征
- 质量词masterpiece, best quality, ultra detailed, 8k uhd, high resolution
- 光影natural lighting, soft shadows, studio lighting, golden hour 等(根据场景选择)
- 风格photorealistic, cinematic, editorial photography, ins style, chinese social media aesthetic
- 构图dynamic angle, depth of field, bokeh 等
- 细节detailed skin texture, sharp focus, vivid colors
- 审美偏向:整体画面风格偏向东方审美、清新淡雅、小红书风格
不要使用括号权重语法,直接用英文逗号分隔描述。
返回 JSON 格式:
{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}
"""
PROMPT_HOTSPOT_ANALYSIS = """
你是一个有实战经验的小红书运营人。下面是搜索到的热门笔记信息:
{feed_data}
你要像一个真正做过内容运营的人来分析这些数据,说话方式接地气一点,不要写得像论文。
请分析以下方面:
1. **热门选题方向**:提炼 3-5 个最火的细分选题。不要只写大方向如"美妆""穿搭",要具体到用户真正感兴趣的切入角度
2. **标题拆解**:这些高赞标题的套路是什么?比如用了什么句式、什么情绪钩子、什么悬念。直接举例说明,不要写得很理论
3. **内容结构**:爆款笔记是怎么组织内容的?是先说痛点再给方案?还是直接晒结果?用大白话说清楚
4. **推荐模仿方案**:给出 3 个具体的选题建议。要具体到"如果是我来写,我会这样切入"的程度,不要泛泛而谈
注意:
- 你的分析是给一个普通创作者看的,不是给甲方看报告
- 说人话,不要堆砌专业术语
- "内容矩阵""赛道""心智占领""用户画像"这种运营黑话能不用就不用
- 建议要可执行,不要写"结合自身特色打造差异化内容"这种废话
返回 JSON 格式:
{{"hot_topics": ["...", "..."], "title_patterns": ["...", "..."], "content_structure": "...", "suggestions": [{{"topic": "...", "reason": "..."}}]}}
"""
PROMPT_COMMENT_REPLY = """
你现在是一个真实的小红书博主,你的人设是:{persona}
有粉丝在你的笔记下留言了,你需要像真人一样自然回复。
【核心模拟原则——极其重要】:
你必须模拟一个真实的、活生生的小红书用户在手机上随手打字的状态:
- 打字随意,不追求语法完美,偶尔可以有口语化的省略
- 真人不会每句话都加 emoji有时一个都不加也很正常
- 绝对不要用"哈哈哈"开头(这是 AI 最常见的暴露特征)
- 不要用书面化的转折词("不过""然而""但是""确实"),用口语("""""""是吧""对对对""哇塞"
- 回复可以很短,甚至只有几个字("谢谢宝!""对的呀""试试看~"
- 绝对不要用逗号和句号写出完整的完美句子,真人打字经常没有标点
- 不要用感叹号过多真人回复感叹号一般不超过1个
- 不要用""""""等语气词堆砌在一起
【回复策略】:
- 如果是夸奖/支持 → 简短感谢,可以适当撒娇或害羞("嘿嘿被你发现了""爱你""抱住"
- 如果是提问 → 直接回答,不要先说"这个问题很好"之类的废话
- 如果是质疑/负面 → 轻松带过或幽默化解,不要长篇大论解释
- 如果是分享经历 → 表示共鸣,简短回应
- 如果评论很简短(比如"好看""不错" → 你的回复也应该很简短
【禁止出现的 AI 特征】:
"谢谢你的xxx" "感谢你的分享/支持/喜欢"
"作为一个xxx" "我觉得xxx"
❌ 完整的主谓宾结构句子
❌ 连续使用3个以上emoji
"哈哈哈"开头
❌ 每句话都有标点符号
❌ 排比句、对仗句
"宝子""姐妹""家人们"等称呼(除非人设就是这种风格)
笔记标题:{post_title}
粉丝评论:{comment}
直接输出回复内容,不要有任何解释。回复长度请匹配对方评论的长度——对方说的短你也短,对方说的长你可以稍长一点。
"""
PROMPT_PROACTIVE_COMMENT = """
你必须完美伪装成一个真实的小红书用户。你的人设是:{persona}
你正在刷小红书,看到了一篇笔记,你想留一条看起来像真人写的评论。
【核心!!——你是一个在手机上随手打字的普通人】:
把自己想象成一个躺在沙发上刷手机的人,看到一篇笔记后随手评论。
你打字很随意,不会字字斟酌,想到什么说什么。
【真人评论特征——必须遵循】:
1. 长度自然变化:有时候一句话("救命也太好看了吧"有时候两三句极少超过50字
2. 真人打字习惯:
- 经常省略主语("看完立马下单了" 而不是 "我看完后立马下单了"
- 会用缩写和网络用语("绝绝子""yyds""蹲一个""dd""awsl"
- 感叹用语如 "天哪""救命""啊啊啊""绝了" 而不是文绉绉的"真的很棒"
- 偶尔打错字也ok但不要刻意
3. emoji 使用规则:
- 50%的概率不加任何emoji
- 加的话最多1-2个而且偏好 😭🫠❤️🥺😍 这类情绪化的
- 不要用 ✨💫🌟 这种博主式的装饰emoji
4. 绝对不要分点列举真人评论从不分1234条说
5. 不要用完整标点,真人评论经常没逗号句号
【评论类型——随机选择一种自然风格】:
- 分享真实感受("这个颜色实物真的绝了 上次路过柜台试了一下就走不动了"
- 提一个具体问题("这个是什么色号呀""博主身高多少 我怕买了不合适"
- 表达种草("看完直接去搜了""钱包在哭泣"
- 补充相关经验("我之前买过xxx 感觉跟这个搭也蛮好看的"
- 简短共鸣("真的!""笑死""太真实了""懂了"
【绝对禁止——这些是AI评论的特征】
"写得真好" "内容很有价值" "干货满满" "收藏了"
"博主太厉害了" "学到了" "受益匪浅" "非常实用"
"我也觉得xxx" "我认为xxx" 这种过于理性客观的表达
"首先...其次...最后..." 任何分点罗列
❌ 以"哈哈"开头
❌ 超过3个emoji
❌ 完整规范的标点使用
❌ 每句话都很完整很正式
❌ 同时出现""和emoji选一个就够了
❌ 把笔记标题的关键词重复一遍(比如笔记标题说"穿搭"你就评论"穿搭真好看"
【笔记信息】:
标题:{post_title}
正文摘要:{post_content}
【已有评论参考(避免重复)】:
{existing_comments}
请直接输出一条评论不要有任何解释或前缀。记住你是一个真人不是AI。
"""
PROMPT_COPY_WITH_REFERENCE = """
你是一个真实的小红书博主,正在参考一些热门笔记来写一篇自己的原创内容。
你不是在写营销文案,你只是觉得这些笔记写得不错,想借鉴思路写一篇自己的体验分享。
【参考笔记】:
{reference_notes}
【创作主题】:{topic}
【风格要求】:{style}
【标题规则】:
1. 长度限制:必须控制在 18 字以内含Emoji绝对不能超过 20 字!
2. 学习参考笔记标题的情绪感和口语感,但内容完全原创
3. 写得像你发给朋友看的那种,不要像广告
【正文规则——写得像真人】:
1. 想象你是刚体验完然后打开小红书写笔记,把你的真实感受和过程写出来
2. 正文控制在 400-600 字
3. 真人写法:
- 开头可以直接说事,不需要"嗨大家好"之类的开场白
- 中间夹杂一些个人感受和小吐槽("一开始还在犹豫 结果用了之后真香"
- 不要面面俱到什么优点都说一遍挑2-3个最有感触的重点说
- 可以适当说一两个小缺点,让内容更真实("唯一的缺点就是xxx 但瑕不掩瑜"
- 段落自然分割,有的段一两句,有的段稍长
4. emoji 穿插在情绪高点,不要每句都有,整篇 6-10 个足够
5. 绝对禁止:
❌ 排比句、对仗句("不仅...而且..." "既...又..."
"值得一提" "需要注意" "总结一下" 等总结性书面用语
❌ 每个段落都很工整的1234结构
❌ 面面俱到地罗列所有优点
6. 结尾加 5-8 个话题标签(#)
【绘图 Prompt】
生成 Stable Diffusion 英文提示词,适配 JuggernautXL 模型:
- 人物要求(最重要!):如果画面中有人物,必须是东亚面孔的中国人,使用 asian girl/boy, chinese, east asian features, black hair, dark brown eyes, delicate facial features, fair skin, slim figure 等描述,绝对禁止出现西方人/欧美人特征
- 必含质量词masterpiece, best quality, ultra detailed, 8k uhd
- 风格photorealistic, cinematic, editorial photography, chinese social media aesthetic
- 光影和细节natural lighting, sharp focus, vivid colors, detailed skin texture
- 审美偏向:整体画面风格偏向东方审美、清新淡雅、小红书风格
- 用英文逗号分隔,不用括号权重语法。
返回 JSON 格式:
{{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}}
"""
class LLMService:
"""LLM API 服务封装"""
# 当主模型返回空内容时,依次尝试的备选模型列表
FALLBACK_MODELS = ["deepseek-v3", "gemini-2.5-flash", "deepseek-v3.1"]
def __init__(self, api_key: str, base_url: str, model: str = "gpt-3.5-turbo"):
self.api_key = api_key
self.base_url = base_url.rstrip("/")
self.model = model
def _chat(self, system_prompt: str, user_message: str,
json_mode: bool = True, temperature: float = 0.8) -> str:
"""底层聊天接口含空返回检测、json_mode 回退、模型降级)"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
if json_mode:
user_message = user_message + "\n请以json格式返回。"
# 构建要尝试的模型列表:主模型 + 备选模型(去重)
models_to_try = [self.model] + [m for m in self.FALLBACK_MODELS if m != self.model]
last_error = None
for model_idx, current_model in enumerate(models_to_try):
payload = {
"model": current_model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
"temperature": temperature,
}
if json_mode:
payload["response_format"] = {"type": "json_object"}
try:
resp = requests.post(
f"{self.base_url}/chat/completions",
headers=headers, json=payload, timeout=90
)
resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"]
# 检测空返回 — 如果启用了 json_mode 且返回为空,回退去掉 response_format 重试
if not content or not content.strip():
if json_mode:
logger.warning("[%s] LLM 返回空内容 (json_mode=True),关闭 json_mode 回退重试...", current_model)
payload.pop("response_format", None)
resp2 = requests.post(
f"{self.base_url}/chat/completions",
headers=headers, json=payload, timeout=90
)
resp2.raise_for_status()
content = resp2.json()["choices"][0]["message"]["content"]
if not content or not content.strip():
# 当前模型完全无法返回内容,尝试下一个模型
if model_idx < len(models_to_try) - 1:
next_model = models_to_try[model_idx + 1]
logger.warning("[%s] 返回空内容,自动降级到模型: %s", current_model, next_model)
continue
raise RuntimeError(f"所有模型均返回空内容(已尝试: {', '.join(models_to_try[:model_idx+1])}")
if model_idx > 0:
logger.info("模型降级成功: %s%s", self.model, current_model)
return content
except requests.exceptions.HTTPError as e:
status = getattr(resp, 'status_code', 0)
body = getattr(resp, 'text', '')[:300]
# 某些模型/提供商不支持 response_format自动回退重试
if json_mode and status in (400, 422, 500):
logger.warning("[%s] json_mode 请求失败 (HTTP %s),关闭 response_format 回退重试...", current_model, status)
payload.pop("response_format", None)
try:
resp2 = requests.post(
f"{self.base_url}/chat/completions",
headers=headers, json=payload, timeout=90
)
resp2.raise_for_status()
content = resp2.json()["choices"][0]["message"]["content"]
if content and content.strip():
if model_idx > 0:
logger.info("模型降级成功: %s%s", self.model, current_model)
return content
except Exception:
pass
# 当前模型失败,尝试下一个
last_error = ConnectionError(f"LLM API 错误 ({status}): {body}")
if model_idx < len(models_to_try) - 1:
logger.warning("[%s] HTTP %s 失败,降级到: %s", current_model, status, models_to_try[model_idx + 1])
continue
raise last_error
except requests.exceptions.Timeout:
last_error = TimeoutError(f"[{current_model}] LLM 请求超时")
if model_idx < len(models_to_try) - 1:
logger.warning("[%s] 请求超时,降级到: %s", current_model, models_to_try[model_idx + 1])
continue
raise TimeoutError("LLM 请求超时,所有模型均超时,请检查网络")
except (ConnectionError, RuntimeError):
raise
except Exception as e:
last_error = RuntimeError(f"LLM 调用异常: {e}")
if model_idx < len(models_to_try) - 1:
logger.warning("[%s] 调用异常 (%s),降级到: %s", current_model, e, models_to_try[model_idx + 1])
continue
raise last_error
raise last_error or RuntimeError("LLM 调用失败: 未知错误")
def _parse_json(self, text: str) -> dict:
"""从 LLM 返回文本中解析 JSON多重容错"""
if not text or not text.strip():
raise ValueError("LLM 返回内容为空,无法解析 JSON")
raw = text.strip()
# 策略1: 去除 markdown 代码块
cleaned = re.sub(r"```(?:json)?\s*", "", raw)
cleaned = re.sub(r"```", "", cleaned).strip()
# 策略2: 直接解析
try:
return json.loads(cleaned)
except json.JSONDecodeError:
pass
# 策略3: 提取最外层的 { ... } 块
match = re.search(r'(\{[\s\S]*\})', cleaned)
if match:
try:
return json.loads(match.group(1))
except json.JSONDecodeError:
pass
# 策略4: 逐行查找 JSON 开始位置
for i, ch in enumerate(cleaned):
if ch == '{':
try:
return json.loads(cleaned[i:])
except json.JSONDecodeError:
pass
break
# 策略5: 尝试修复常见问题(尾部多余逗号、缺少闭合括号)
try:
# 去除尾部多余逗号
fixed = re.sub(r',\s*([}\]])', r'\1', cleaned)
return json.loads(fixed)
except json.JSONDecodeError:
pass
# 全部失败,打日志并抛出有用的错误信息
preview = raw[:500] if len(raw) > 500 else raw
logger.error("JSON 解析全部失败LLM 原始返回: %s", preview)
raise ValueError(
f"LLM 返回内容无法解析为 JSON。\n"
f"返回内容前200字: {raw[:200]}\n\n"
f"💡 可能原因: 模型不支持 JSON 输出格式,建议更换模型重试"
)
# ---------- 业务方法 ----------
def get_models(self) -> list[str]:
"""获取可用模型列表"""
url = f"{self.base_url}/models"
headers = {"Authorization": f"Bearer {self.api_key}"}
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
text = resp.text.strip()
if not text:
logger.warning("GET %s 返回空响应", url)
return []
data = resp.json()
return [item["id"] for item in data.get("data", [])]
except Exception as e:
logger.warning("获取模型列表失败 (%s): %s", url, e)
return []
def generate_copy(self, topic: str, style: str) -> dict:
"""生成小红书文案(含重试逻辑)"""
last_error = None
for attempt in range(2):
try:
# 第二次尝试不使用 json_mode兼容不支持的模型
use_json_mode = (attempt == 0)
content = self._chat(
PROMPT_COPYWRITING,
f"主题:{topic}\n风格:{style}",
json_mode=use_json_mode,
temperature=0.92,
)
data = self._parse_json(content)
# 强制标题长度限制
title = data.get("title", "")
if len(title) > 20:
title = title[:20]
data["title"] = title
# 去 AI 化后处理
if "content" in data:
data["content"] = self._humanize_content(data["content"])
return data
except (json.JSONDecodeError, ValueError) as e:
last_error = e
if attempt == 0:
logger.warning("文案生成 JSON 解析失败 (尝试 %d/2): %s,将关闭 json_mode 重试", attempt + 1, e)
continue
else:
logger.error("文案生成 JSON 解析失败 (尝试 %d/2): %s", attempt + 1, e)
raise RuntimeError(f"文案生成失败: LLM 返回无法解析为 JSON已重试 2 次。\n最后错误: {last_error}")
def generate_copy_with_reference(self, topic: str, style: str,
reference_notes: str) -> dict:
"""参考热门笔记生成文案(含重试逻辑)"""
prompt = PROMPT_COPY_WITH_REFERENCE.format(
reference_notes=reference_notes, topic=topic, style=style
)
last_error = None
for attempt in range(2):
try:
use_json_mode = (attempt == 0)
content = self._chat(
prompt, f"请创作关于「{topic}」的小红书笔记",
json_mode=use_json_mode, temperature=0.92,
)
data = self._parse_json(content)
title = data.get("title", "")
if len(title) > 20:
data["title"] = title[:20]
if "content" in data:
data["content"] = self._humanize_content(data["content"])
return data
except (json.JSONDecodeError, ValueError) as e:
last_error = e
if attempt == 0:
logger.warning("参考文案生成 JSON 解析失败 (尝试 %d/2): %s,将关闭 json_mode 重试", attempt + 1, e)
continue
else:
logger.error("参考文案生成 JSON 解析失败 (尝试 %d/2): %s", attempt + 1, e)
raise RuntimeError(f"参考文案生成失败: LLM 返回无法解析为 JSON已重试 2 次。\n最后错误: {last_error}")
def analyze_hotspots(self, feed_data: str) -> dict:
"""分析热门内容趋势(含重试逻辑)"""
prompt = PROMPT_HOTSPOT_ANALYSIS.format(feed_data=feed_data)
last_error = None
for attempt in range(2):
try:
use_json_mode = (attempt == 0)
content = self._chat(prompt, "请分析以上热门笔记数据",
json_mode=use_json_mode)
return self._parse_json(content)
except (json.JSONDecodeError, ValueError) as e:
last_error = e
if attempt == 0:
logger.warning("热点分析 JSON 解析失败 (尝试 %d/2): %s,将关闭 json_mode 重试", attempt + 1, e)
continue
else:
logger.error("热点分析 JSON 解析失败 (尝试 %d/2): %s", attempt + 1, e)
raise RuntimeError(f"热点分析失败: LLM 返回无法解析为 JSON已重试 2 次。\n最后错误: {last_error}")
@staticmethod
def _humanize_content(text: str) -> str:
"""后处理: 去除长文案中的 AI 书面痕迹"""
t = text
# 替换过于书面化的表达
ai_phrases = {
"值得一提的是": "对了",
"需要注意的是": "不过要注意",
"总的来说": "反正",
"综上所述": "总之",
"总而言之": "总之",
"不仅如此": "而且",
"与此同时": "然后",
"除此之外": "还有",
"众所周知": "",
"毋庸置疑": "",
"不言而喻": "",
"在这里给大家分享": "来分享",
"在此分享给大家": "分享一下",
"接下来让我们": "",
"话不多说": "",
"废话不多说": "",
"小伙伴们": "姐妹们",
}
for old, new in ai_phrases.items():
t = t.replace(old, new)
# 去掉 "首先" "其次" "最后" 的分点罗列感
t = re.sub(r'(?m)^首先[,:\s]*', '', t)
t = re.sub(r'(?m)^其次[,:\s]*', '', t)
t = re.sub(r'(?m)^最后[,:\s]*', '', t)
t = re.sub(r'(?m)^再者[,:\s]*', '', t)
# 去掉AI常见的空洞开头
for prefix in ["嗨大家好!", "嗨,大家好!", "大家好,", "大家好!", "哈喽大家好!"]:
if t.startswith(prefix):
t = t[len(prefix):].strip()
# 清理多余空行
t = re.sub(r'\n{3,}', '\n\n', t)
return t.strip()
@staticmethod
def _humanize(text: str) -> str:
"""后处理: 去除 AI 输出中常见的非人类痕迹"""
t = text.strip()
# 去掉前后引号包裹
if (t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'")):
t = t[1:-1].strip()
# 去掉 AI 常见的前缀
for prefix in ["回复:", "回复:", "评论:", "评论:", "以下是", "好的,"]:
if t.startswith(prefix):
t = t[len(prefix):].strip()
# 去掉末尾多余的句号(真人评论很少用句号结尾)
if t.endswith(""):
t = t[:-1]
# 限制连续 emoji最多2个
t = re.sub(r'([\U0001F600-\U0001F9FF\u2600-\u27BF])\1{2,}', r'\1\1', t)
return t
def generate_reply(self, persona: str, post_title: str, comment: str) -> str:
"""AI 生成评论回复"""
prompt = PROMPT_COMMENT_REPLY.format(
persona=persona, post_title=post_title, comment=comment
)
raw = self._chat(prompt, "请生成回复", json_mode=False, temperature=0.95)
return self._humanize(raw)
def generate_proactive_comment(self, persona: str, post_title: str,
post_content: str, existing_comments: str = "") -> str:
"""AI 生成主动评论"""
prompt = PROMPT_PROACTIVE_COMMENT.format(
persona=persona, post_title=post_title,
post_content=post_content,
existing_comments=existing_comments or "暂无评论",
)
raw = self._chat(prompt, "请生成评论", json_mode=False, temperature=0.95)
return self._humanize(raw)