feat(emoji): 优化表情选择逻辑并引入上下文数量限制
重构了内置插件中的表情发送逻辑,以提高选择的准确性和效率。 旧的机制依赖于预设的情感标签,这可能不准确或缺失。新的实现改为让 LLM 直接从一部分随机抽样的表情包描述中进行选择,这使得决策更贴近上下文。 主要变更: - 将基于情感标签的选择改为基于表情包描述的选择,使表情推荐更精准。 - 新增 `max_context_emojis` 配置项,用于控制每次传递给 LLM 的表情包候选项数量,从而减少 token 消耗并提高响应速度。
This commit is contained in:
@@ -385,6 +385,7 @@ class EmojiConfig(ValidatedConfigBase):
|
|||||||
content_filtration: bool = Field(default=False, description="内容过滤")
|
content_filtration: bool = Field(default=False, description="内容过滤")
|
||||||
filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示")
|
filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示")
|
||||||
enable_emotion_analysis: bool = Field(default=True, description="启用情感分析")
|
enable_emotion_analysis: bool = Field(default=True, description="启用情感分析")
|
||||||
|
max_context_emojis: int = Field(default=30, description="每次随机传递给LLM的表情包最大数量,0为全部")
|
||||||
|
|
||||||
|
|
||||||
class MemoryConfig(ValidatedConfigBase):
|
class MemoryConfig(ValidatedConfigBase):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import random
|
import random
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
import json
|
||||||
|
|
||||||
# 导入新插件系统
|
# 导入新插件系统
|
||||||
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
||||||
@@ -10,7 +11,7 @@ from src.common.logger import get_logger
|
|||||||
|
|
||||||
# 导入API模块 - 标准Python包方式
|
# 导入API模块 - 标准Python包方式
|
||||||
from src.plugin_system.apis import llm_api, message_api
|
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.chat.utils.utils_image import image_path_to_base64
|
||||||
from src.config.config import global_config
|
from src.config.config import global_config
|
||||||
|
|
||||||
@@ -72,125 +73,103 @@ class EmojiAction(BaseAction):
|
|||||||
reason = self.action_data.get("reason", "表达当前情绪")
|
reason = self.action_data.get("reason", "表达当前情绪")
|
||||||
logger.info(f"{self.log_prefix} 发送表情原因: {reason}")
|
logger.info(f"{self.log_prefix} 发送表情原因: {reason}")
|
||||||
|
|
||||||
# 2. 获取所有表情包
|
# 2. 获取所有有效的表情包对象
|
||||||
emoji_manager = get_emoji_manager()
|
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:
|
if not all_emojis_obj:
|
||||||
logger.warning(f"{self.log_prefix} 无法获取任何表情包")
|
logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包")
|
||||||
return False, "无法获取任何表情包"
|
return False, "无法获取任何带有描述的有效表情包"
|
||||||
|
|
||||||
# 3. 准备情感数据和后备列表
|
# 3. 根据新配置项决定抽样数量
|
||||||
emotion_map = {}
|
sample_size = global_config.emoji.max_context_emojis
|
||||||
all_emojis_data = []
|
if sample_size > 0 and len(all_emojis_obj) > sample_size:
|
||||||
|
sampled_emojis = random.sample(all_emojis_obj, sample_size)
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
# 获取最近的5条消息内容用于判断
|
sampled_emojis = all_emojis_obj # 0表示全部
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. 构建prompt让LLM选择多个情感
|
# 4. 为抽样的表情包创建带编号的描述列表
|
||||||
prompt = f"""
|
prompt_emoji_list = []
|
||||||
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的 **3个** 情感标签,并按匹配度从高到低排序。
|
for i, emoji in enumerate(sampled_emojis):
|
||||||
这是最近的聊天记录:
|
prompt_emoji_list.append(f"{i + 1}. {emoji.description}")
|
||||||
{messages_text}
|
|
||||||
|
prompt_emoji_str = "\n".join(prompt_emoji_list)
|
||||||
这是理由:“{reason}”
|
chosen_emoji_obj: MaiEmoji = None
|
||||||
这里是可用的情感标签:{available_emotions}
|
|
||||||
请直接返回一个包含3个最匹配情感标签的有序列表,例如:["开心", "激动", "有趣"],不要进行任何解释或添加其他多余的文字。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 5. 调用LLM
|
# 5. 获取最近的5条消息内容用于判断
|
||||||
models = llm_api.get_available_models()
|
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
|
||||||
chat_model_config = models.get("planner")
|
messages_text = ""
|
||||||
if not chat_model_config:
|
if recent_messages:
|
||||||
logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM")
|
messages_text = message_api.build_readable_messages(
|
||||||
return False, "未找到 'planner' 模型配置"
|
messages=recent_messages,
|
||||||
|
timestamp_mode="normal_no_YMD",
|
||||||
success, chosen_emotions_str, _, _ = await llm_api.generate_with_model(
|
truncate=False,
|
||||||
prompt, model_config=chat_model_config, request_type="emoji_selection"
|
show_actions=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
selected_emoji_info = None
|
# 6. 构建prompt让LLM选择编号
|
||||||
if success:
|
prompt = f"""
|
||||||
try:
|
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个带编号的表情包描述列表中选择最匹配的 **3个** 表情包,并按匹配度从高到低返回它们的编号。
|
||||||
# 解析LLM返回的列表
|
这是最近的聊天记录:
|
||||||
import json
|
{messages_text}
|
||||||
chosen_emotions = json.loads(chosen_emotions_str)
|
|
||||||
if isinstance(chosen_emotions, list):
|
这是理由:“{reason}”
|
||||||
logger.info(f"{self.log_prefix} LLM选择的情感候选项: {chosen_emotions}")
|
这里是可用的表情包详细描述列表:
|
||||||
# 遍历候选项,找到第一个不在历史记录中的表情
|
{prompt_emoji_str}
|
||||||
for emotion in chosen_emotions:
|
请直接返回一个包含3个最匹配表情包编号的有序JSON列表,例如:[10, 2, 5],不要进行任何解释或添加其他多余的文字。
|
||||||
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}")
|
|
||||||
|
|
||||||
if selected_emoji_info:
|
# 7. 调用LLM
|
||||||
chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = selected_emoji_info
|
models = llm_api.get_available_models()
|
||||||
logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_desc}")
|
chat_model_config = models.get("planner")
|
||||||
else:
|
if not chat_model_config:
|
||||||
if not success:
|
logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM")
|
||||||
logger.warning(f"{self.log_prefix} 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:
|
else:
|
||||||
logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择")
|
logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_indices_str}")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
selectable_emojis = [e for e in all_emojis_data if e[2] not in self._sent_emoji_history]
|
logger.warning(f"{self.log_prefix} 解析LLM返回的编号列表失败: {chosen_indices_str}")
|
||||||
if not selectable_emojis:
|
|
||||||
selectable_emojis = all_emojis_data
|
|
||||||
chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = random.choice(selectable_emojis)
|
|
||||||
|
|
||||||
# 7. 发送表情包并更新历史记录
|
if selected_emoji_obj:
|
||||||
if chosen_emoji_b64 and chosen_emoji_hash:
|
chosen_emoji_obj = selected_emoji_obj
|
||||||
success = await self.send_emoji(chosen_emoji_b64)
|
logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_obj.description}")
|
||||||
if success:
|
else:
|
||||||
self._sent_emoji_history.append(chosen_emoji_hash)
|
if not success:
|
||||||
logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_desc}")
|
logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包")
|
||||||
logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}")
|
else:
|
||||||
return True, f"发送表情包: {chosen_emoji_desc}"
|
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} 表情包发送失败")
|
logger.error(f"{self.log_prefix} 表情包发送失败")
|
||||||
return False, "表情包发送失败"
|
return False, "表情包发送失败"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[inner]
|
[inner]
|
||||||
version = "6.8.3"
|
version = "6.8.4"
|
||||||
|
|
||||||
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
||||||
#如果你想要修改配置文件,请递增version的值
|
#如果你想要修改配置文件,请递增version的值
|
||||||
@@ -246,6 +246,7 @@ steal_emoji = true # 是否偷取表情包,让MoFox-Bot可以将一些表情
|
|||||||
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
|
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
|
||||||
filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
|
filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
|
||||||
enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中
|
enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中
|
||||||
|
max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最大数量,0为全部
|
||||||
|
|
||||||
[memory]
|
[memory]
|
||||||
enable_memory = true # 是否启用记忆系统
|
enable_memory = true # 是否启用记忆系统
|
||||||
|
|||||||
Reference in New Issue
Block a user