feat(emoji): 优化表情选择逻辑并引入上下文数量限制

重构了内置插件中的表情发送逻辑,以提高选择的准确性和效率。

旧的机制依赖于预设的情感标签,这可能不准确或缺失。新的实现改为让 LLM 直接从一部分随机抽样的表情包描述中进行选择,这使得决策更贴近上下文。

主要变更:
- 将基于情感标签的选择改为基于表情包描述的选择,使表情推荐更精准。
- 新增 `max_context_emojis` 配置项,用于控制每次传递给 LLM 的表情包候选项数量,从而减少 token 消耗并提高响应速度。
This commit is contained in:
tt-P607
2025-09-11 17:29:42 +08:00
parent 0cb2fa3373
commit de90d452cc
3 changed files with 91 additions and 110 deletions

View File

@@ -385,6 +385,7 @@ class EmojiConfig(ValidatedConfigBase):
content_filtration: bool = Field(default=False, description="内容过滤")
filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示")
enable_emotion_analysis: bool = Field(default=True, description="启用情感分析")
max_context_emojis: int = Field(default=30, description="每次随机传递给LLM的表情包最大数量0为全部")
class MemoryConfig(ValidatedConfigBase):

View File

@@ -1,6 +1,7 @@
import random
from typing import Tuple
from collections import deque
import json
# 导入新插件系统
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
@@ -10,7 +11,7 @@ from src.common.logger import get_logger
# 导入API模块 - 标准Python包方式
from src.plugin_system.apis import llm_api, message_api
from src.chat.emoji_system.emoji_manager import get_emoji_manager
from src.chat.emoji_system.emoji_manager import get_emoji_manager, MaiEmoji
from src.chat.utils.utils_image import image_path_to_base64
from src.config.config import global_config
@@ -72,125 +73,103 @@ class EmojiAction(BaseAction):
reason = self.action_data.get("reason", "表达当前情绪")
logger.info(f"{self.log_prefix} 发送表情原因: {reason}")
# 2. 获取所有表情包
# 2. 获取所有有效的表情包对象
emoji_manager = get_emoji_manager()
all_emojis_obj = [e for e in emoji_manager.emoji_objects if not e.is_deleted]
all_emojis_obj: list[MaiEmoji] = [e for e in emoji_manager.emoji_objects if not e.is_deleted and e.description]
if not all_emojis_obj:
logger.warning(f"{self.log_prefix} 无法获取任何表情包")
return False, "无法获取任何表情包"
logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包")
return False, "无法获取任何带有描述的有效表情包"
# 3. 准备情感数据和后备列表
emotion_map = {}
all_emojis_data = []
for emoji in all_emojis_obj:
b64 = image_path_to_base64(emoji.full_path)
if not b64:
continue
desc = emoji.description
emotions = emoji.emotion
# 使用 emoji 对象的 hash 作为唯一标识符
all_emojis_data.append((b64, desc, emoji.hash))
for emo in emotions:
if emo not in emotion_map:
emotion_map[emo] = []
emotion_map[emo].append((b64, desc, emoji.hash))
if not all_emojis_data:
logger.warning(f"{self.log_prefix} 无法加载任何有效的表情包数据")
return False, "无法加载任何有效的表情包数据"
available_emotions = list(emotion_map.keys())
chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = None, None, None
if not available_emotions:
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
# 随机选择一个不在历史记录中的表情
selectable_emojis = [e for e in all_emojis_data if e[2] not in self._sent_emoji_history]
if not selectable_emojis: # 如果都发过了,就从全部里面随机选
selectable_emojis = all_emojis_data
chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = random.choice(selectable_emojis)
# 3. 根据新配置项决定抽样数量
sample_size = global_config.emoji.max_context_emojis
if sample_size > 0 and len(all_emojis_obj) > sample_size:
sampled_emojis = random.sample(all_emojis_obj, sample_size)
else:
# 获取最近的5条消息内容用于判断
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
messages_text = ""
if recent_messages:
messages_text = message_api.build_readable_messages(
messages=recent_messages,
timestamp_mode="normal_no_YMD",
truncate=False,
show_actions=False,
)
sampled_emojis = all_emojis_obj # 0表示全部
# 4. 构建prompt让LLM选择多个情感
prompt = f"""
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的 **3个** 情感标签,并按匹配度从高到低排序。
这是最近的聊天记录:
{messages_text}
这是理由:“{reason}
这里是可用的情感标签:{available_emotions}
请直接返回一个包含3个最匹配情感标签的有序列表例如["开心", "激动", "有趣"],不要进行任何解释或添加其他多余的文字。
"""
# 4. 为抽样的表情包创建带编号的描述列表
prompt_emoji_list = []
for i, emoji in enumerate(sampled_emojis):
prompt_emoji_list.append(f"{i + 1}. {emoji.description}")
prompt_emoji_str = "\n".join(prompt_emoji_list)
chosen_emoji_obj: MaiEmoji = None
# 5. 调用LLM
models = llm_api.get_available_models()
chat_model_config = models.get("planner")
if not chat_model_config:
logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置无法调用LLM")
return False, "未找到 'planner' 模型配置"
success, chosen_emotions_str, _, _ = await llm_api.generate_with_model(
prompt, model_config=chat_model_config, request_type="emoji_selection"
# 5. 获取最近的5条消息内容用于判断
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
messages_text = ""
if recent_messages:
messages_text = message_api.build_readable_messages(
messages=recent_messages,
timestamp_mode="normal_no_YMD",
truncate=False,
show_actions=False,
)
selected_emoji_info = None
if success:
try:
# 解析LLM返回的列表
import json
chosen_emotions = json.loads(chosen_emotions_str)
if isinstance(chosen_emotions, list):
logger.info(f"{self.log_prefix} LLM选择的情感候选项: {chosen_emotions}")
# 遍历候选项,找到第一个不在历史记录中的表情
for emotion in chosen_emotions:
matched_key = next((key for key in emotion_map if emotion in key), None)
if matched_key:
# 从匹配到的表情中,随机选一个不在历史记录的
candidate_emojis = [e for e in emotion_map[matched_key] if e[2] not in self._sent_emoji_history]
if candidate_emojis:
selected_emoji_info = random.choice(candidate_emojis)
break # 找到后立即跳出循环
else:
logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_emotions_str}")
except (json.JSONDecodeError, TypeError):
logger.warning(f"{self.log_prefix} 解析LLM返回的情感列表失败: {chosen_emotions_str}")
# 6. 构建prompt让LLM选择编号
prompt = f"""
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个带编号的表情包描述列表中选择最匹配的 **3个** 表情包,并按匹配度从高到低返回它们的编号。
这是最近的聊天记录:
{messages_text}
这是理由:“{reason}
这里是可用的表情包详细描述列表:
{prompt_emoji_str}
请直接返回一个包含3个最匹配表情包编号的有序JSON列表例如[10, 2, 5],不要进行任何解释或添加其他多余的文字。
"""
if selected_emoji_info:
chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = selected_emoji_info
logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_desc}")
else:
if not success:
logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包")
# 7. 调用LLM
models = llm_api.get_available_models()
chat_model_config = models.get("planner")
if not chat_model_config:
logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置无法调用LLM")
return False, "未找到 'planner' 模型配置"
success, chosen_indices_str, _, _ = await llm_api.generate_with_model(
prompt, model_config=chat_model_config, request_type="emoji_selection"
)
selected_emoji_obj = None
if success:
try:
chosen_indices = json.loads(chosen_indices_str)
if isinstance(chosen_indices, list):
logger.info(f"{self.log_prefix} LLM选择的表情编号候选项: {chosen_indices}")
for index in chosen_indices:
if isinstance(index, int) and 1 <= index <= len(sampled_emojis):
candidate_emoji = sampled_emojis[index - 1]
if candidate_emoji.hash not in self._sent_emoji_history:
selected_emoji_obj = candidate_emoji
break
else:
logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择")
selectable_emojis = [e for e in all_emojis_data if e[2] not in self._sent_emoji_history]
if not selectable_emojis:
selectable_emojis = all_emojis_data
chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = random.choice(selectable_emojis)
logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_indices_str}")
except (json.JSONDecodeError, TypeError):
logger.warning(f"{self.log_prefix} 解析LLM返回的编号列表失败: {chosen_indices_str}")
# 7. 发送表情包并更新历史记录
if chosen_emoji_b64 and chosen_emoji_hash:
success = await self.send_emoji(chosen_emoji_b64)
if success:
self._sent_emoji_history.append(chosen_emoji_hash)
logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_desc}")
logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}")
return True, f"发送表情包: {chosen_emoji_desc}"
if selected_emoji_obj:
chosen_emoji_obj = selected_emoji_obj
logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_obj.description}")
else:
if not success:
logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包")
else:
logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择")
selectable_emojis = [e for e in all_emojis_obj if e.hash not in self._sent_emoji_history]
if not selectable_emojis:
selectable_emojis = all_emojis_obj
chosen_emoji_obj = random.choice(selectable_emojis)
# 8. 发送表情包并更新历史记录
if chosen_emoji_obj:
emoji_base64 = image_path_to_base64(chosen_emoji_obj.full_path)
if emoji_base64:
send_success = await self.send_emoji(emoji_base64)
if send_success:
self._sent_emoji_history.append(chosen_emoji_obj.hash)
logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_obj.description}")
logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}")
return True, f"发送表情包: {chosen_emoji_obj.description}"
logger.error(f"{self.log_prefix} 表情包发送失败")
return False, "表情包发送失败"

View File

@@ -1,5 +1,5 @@
[inner]
version = "6.8.3"
version = "6.8.4"
#----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读----
#如果你想要修改配置文件请递增version的值
@@ -246,6 +246,7 @@ steal_emoji = true # 是否偷取表情包让MoFox-Bot可以将一些表情
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中
max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最大数量0为全部
[memory]
enable_memory = true # 是否启用记忆系统