[fix] max_tokens 参数处理 以及 过滤机器人自己的消息

Merge pull request #35 from SXP-Simon/fix/user-title
This commit is contained in:
Helian Nuits
2025-11-04 21:05:38 +08:00
committed by GitHub
9 changed files with 100 additions and 27 deletions
+18
View File
@@ -117,6 +117,24 @@
"default": 2,
"hint": "重试之间的基准等待时间(秒),实际等待时间为基值乘以尝试次数。"
},
"topic_max_tokens": {
"type": "int",
"description": "(兼容部分提供商)话题分析最大 Token 数",
"default": 12288,
"hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)话题分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。"
},
"golden_quote_max_tokens": {
"type": "int",
"description": "(兼容部分提供商)金句分析最大 Token 数",
"default": 4096,
"hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)金句分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。"
},
"user_title_max_tokens": {
"type": "int",
"description": "(兼容部分提供商)用户称号分析最大 Token 数",
"default": 4096,
"hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)用户称号分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。"
},
"custom_api_key": {
"type": "string",
"description": "自定义 LLM 服务 API Key (选填)",
@@ -29,7 +29,7 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
def get_max_tokens(self) -> int:
"""获取最大token数"""
return 1500
return self.config_manager.get_golden_quote_max_tokens()
def get_temperature(self) -> float:
"""获取温度参数"""
+1 -1
View File
@@ -29,7 +29,7 @@ class TopicAnalyzer(BaseAnalyzer):
def get_max_tokens(self) -> int:
"""获取最大token数"""
return 10000
return self.config_manager.get_topic_max_tokens()
def get_temperature(self) -> float:
"""获取温度参数"""
+32 -13
View File
@@ -26,7 +26,7 @@ class UserTitleAnalyzer(BaseAnalyzer):
def get_max_tokens(self) -> int:
"""获取最大token数"""
return 1500
return self.config_manager.get_user_title_max_tokens()
def get_temperature(self) -> float:
"""获取温度参数"""
@@ -136,22 +136,42 @@ class UserTitleAnalyzer(BaseAnalyzer):
logger.error(f"创建用户称号对象失败: {e}")
return []
def prepare_user_data(self, messages: List[Dict], user_analysis: Dict) -> Dict:
def prepare_user_data(self, messages: List[Dict], user_analysis: Dict, top_users: List[Dict] = None) -> Dict:
"""
准备用户数据
Args:
messages: 群聊消息列表
user_analysis: 用户分析统计
top_users: 活跃用户列表(从get_top_users获取)
Returns:
准备好的用户数据字典
"""
try:
# 获取机器人QQ号用于过滤
bot_qq_id = self.config_manager.get_bot_qq_id()
user_summaries = []
# 如果提供了top_users列表,只分析这些活跃用户
if top_users:
logger.info(f"使用get_top_users筛选出的 {len(top_users)} 个活跃用户进行称号分析")
target_user_ids = {str(user['user_id']) for user in top_users}
else:
# 兼容旧逻辑:如果没有提供top_users,则使用所有消息数>=5的用户
logger.info("未提供活跃用户列表,使用消息数>=5的用户")
target_user_ids = {user_id for user_id, stats in user_analysis.items()
if stats["message_count"] >= 5}
for user_id, stats in user_analysis.items():
if stats["message_count"] < 5: # 过滤活跃度太低的用户
# 过滤机器人自己的消息
if bot_qq_id and str(user_id) == str(bot_qq_id):
logger.debug(f"过滤掉机器人QQ号: {user_id}")
continue
# 只处理活跃用户
if user_id not in target_user_ids:
continue
# 分析用户特征
@@ -164,18 +184,16 @@ class UserTitleAnalyzer(BaseAnalyzer):
"qq": int(user_id),
"message_count": stats["message_count"],
"avg_chars": round(avg_chars, 1),
"emoji_ratio": round(stats["emoji_count"] / stats["message_count"], 2),
"night_ratio": round(night_messages / stats["message_count"], 2),
"reply_ratio": round(stats["reply_count"] / stats["message_count"], 2)
"emoji_ratio": round(stats["emoji_count"] / stats["message_count"], 2) if stats["message_count"] > 0 else 0,
"night_ratio": round(night_messages / stats["message_count"], 2) if stats["message_count"] > 0 else 0,
"reply_ratio": round(stats["reply_count"] / stats["message_count"], 2) if stats["message_count"] > 0 else 0
})
if not user_summaries:
return {"user_summaries": []}
# 按消息数量排序,取前N名
max_user_titles = self.get_max_count()
# 按消息数量排序
user_summaries.sort(key=lambda x: x["message_count"], reverse=True)
user_summaries = user_summaries[:max_user_titles]
return {"user_summaries": user_summaries}
@@ -183,7 +201,7 @@ class UserTitleAnalyzer(BaseAnalyzer):
logger.error(f"准备用户数据失败: {e}")
return {"user_summaries": []}
async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None) -> Tuple[List[UserTitle], TokenUsage]:
async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None, top_users: List[Dict] = None) -> Tuple[List[UserTitle], TokenUsage]:
"""
分析用户称号
@@ -191,19 +209,20 @@ class UserTitleAnalyzer(BaseAnalyzer):
messages: 群聊消息列表
user_analysis: 用户分析统计
umo: 模型唯一标识符
top_users: 活跃用户列表(从get_top_users获取,可选)
Returns:
(用户称号列表, Token使用统计)
"""
try:
# 准备用户数据
user_data = self.prepare_user_data(messages, user_analysis)
# 准备用户数据,传入活跃用户列表
user_data = self.prepare_user_data(messages, user_analysis, top_users)
if not user_data["user_summaries"]:
logger.info("没有符合条件的用户,返回空结果")
return [], TokenUsage()
logger.info(f"开始分析 {len(user_data['user_summaries'])} 个用户的称号")
logger.info(f"开始分析 {len(user_data['user_summaries'])}活跃用户的称号")
return await self.analyze(user_data, umo)
except Exception as e:
+6 -4
View File
@@ -57,7 +57,7 @@ class LLMAnalyzer:
logger.error(f"话题分析失败: {e}")
return [], TokenUsage()
async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None) -> Tuple[List[UserTitle], TokenUsage]:
async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None, top_users: List[Dict] = None) -> Tuple[List[UserTitle], TokenUsage]:
"""
使用LLM分析用户称号
保持原有接口,委托给专门的UserTitleAnalyzer处理
@@ -66,13 +66,14 @@ class LLMAnalyzer:
messages: 群聊消息列表
user_analysis: 用户分析统计
umo: 模型唯一标识符
top_users: 活跃用户列表(可选)
Returns:
(用户称号列表, Token使用统计)
"""
try:
logger.info("开始用户称号分析")
return await self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo)
return await self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo, top_users)
except Exception as e:
logger.error(f"用户称号分析失败: {e}")
return [], TokenUsage()
@@ -96,7 +97,7 @@ class LLMAnalyzer:
logger.error(f"金句分析失败: {e}")
return [], TokenUsage()
async def analyze_all_concurrent(self, messages: List[Dict], user_analysis: Dict, umo: str = None) -> Tuple[List[SummaryTopic], List[UserTitle], List[GoldenQuote], TokenUsage]:
async def analyze_all_concurrent(self, messages: List[Dict], user_analysis: Dict, umo: str = None, top_users: List[Dict] = None) -> Tuple[List[SummaryTopic], List[UserTitle], List[GoldenQuote], TokenUsage]:
"""
并发执行所有分析任务(话题、用户称号、金句)
@@ -104,6 +105,7 @@ class LLMAnalyzer:
messages: 群聊消息列表
user_analysis: 用户分析统计
umo: 模型唯一标识符
top_users: 活跃用户列表(可选)
Returns:
(话题列表, 用户称号列表, 金句列表, 总Token使用统计)
@@ -114,7 +116,7 @@ class LLMAnalyzer:
# 并发执行三个分析任务
results = await asyncio.gather(
self.topic_analyzer.analyze_topics(messages, umo),
self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo),
self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo, top_users),
self.golden_quote_analyzer.analyze_golden_quotes(messages, umo),
return_exceptions=True
)
+15
View File
@@ -17,6 +17,9 @@ class UserAnalyzer:
def analyze_users(self, messages: List[Dict]) -> Dict[str, Dict]:
"""分析用户活跃度"""
# 获取机器人QQ号用于过滤
bot_qq_id = self.config_manager.get_bot_qq_id()
user_stats = defaultdict(lambda: {
"message_count": 0,
"char_count": 0,
@@ -29,6 +32,11 @@ class UserAnalyzer:
for msg in messages:
sender = msg.get("sender", {})
user_id = str(sender.get("user_id", ""))
# 跳过机器人自己的消息,避免进入统计
if bot_qq_id and user_id == str(bot_qq_id):
continue
nickname = InfoUtils.get_user_nickname(self.config_manager, sender)
user_stats[user_id]["message_count"] += 1
@@ -69,8 +77,15 @@ class UserAnalyzer:
def get_top_users(self, user_analysis: Dict[str, Dict], limit: int = 10) -> List[Dict]:
"""获取最活跃的用户"""
# 获取机器人QQ号用于过滤
bot_qq_id = self.config_manager.get_bot_qq_id()
users = []
for user_id, stats in user_analysis.items():
# 过滤机器人自己
if bot_qq_id and str(user_id) == str(bot_qq_id):
continue
users.append({
"user_id": user_id,
"nickname": stats["nickname"],
+2 -2
View File
@@ -38,7 +38,7 @@ async def call_provider_with_retry(context, config_manager, prompt: str, max_tok
for attempt in range(1, retries + 1):
try:
if custom_api_key and custom_api_base and custom_model:
logger.info(f"使用自定义LLM提供商: {custom_api_base} model={custom_model}")
logger.info(f"使用自定义LLM提供商: {custom_api_base} model={custom_model}, max_tokens={max_tokens}, temperature={temperature}")
logger.debug(f"自定义LLM提供商 prompt 长度: {len(prompt) if prompt else 0}")
logger.debug(f"自定义LLM提供商 prompt 前100字符: {prompt[:100] if prompt else 'None'}...")
@@ -102,7 +102,7 @@ async def call_provider_with_retry(context, config_manager, prompt: str, max_tok
if not provider or provider_id == 'unknown':
logger.warning(f"获取的提供商不正确 (Provider ID: {provider_id})")
logger.info(f"使用LLM provider: {provider}")
logger.info(f"使用LLM provider: {provider}, max_tokens={max_tokens}, temperature={temperature}")
if not provider:
logger.error("provider 为空,无法调用 text_chat,直接返回 None")
return None
+12
View File
@@ -87,6 +87,18 @@ class ConfigManager:
"""获取LLM请求重试退避基值(秒),实际退避会乘以尝试次数"""
return self.config.get("llm_backoff", 2)
def get_topic_max_tokens(self) -> int:
"""获取话题分析最大token数"""
return self.config.get("topic_max_tokens", 12288)
def get_golden_quote_max_tokens(self) -> int:
"""获取金句分析最大token数"""
return self.config.get("golden_quote_max_tokens", 4096)
def get_user_title_max_tokens(self) -> int:
"""获取用户称号分析最大token数"""
return self.config.get("user_title_max_tokens", 4096)
def get_custom_api_key(self) -> str:
"""获取自定义 LLM 服务的 API Key"""
return self.config.get("custom_api_key", "")
+13 -6
View File
@@ -8,7 +8,7 @@ from ...src.models.data_models import GroupStatistics, SummaryTopic, UserTitle,
from ...src.core.message_handler import MessageHandler
from ...src.analysis.llm_analyzer import LLMAnalyzer
from ...src.analysis.statistics import UserAnalyzer
from astrbot.api import logger
class MessageAnalyzer:
"""消息分析器 - 整合所有分析功能"""
@@ -36,6 +36,11 @@ class MessageAnalyzer:
# 用户分析
user_analysis = self.user_analyzer.analyze_users(messages)
# 获取活跃用户列表 - 使用get_top_users方法,limit从配置中读取
max_user_titles = self.config_manager.get_max_user_titles()
top_users = self.user_analyzer.get_top_users(user_analysis, limit=max_user_titles)
logger.info(f"获取到 {len(top_users)} 个活跃用户用于称号分析(配置上限: {max_user_titles})")
# LLM分析 - 使用并发方式
topics = []
@@ -50,9 +55,9 @@ class MessageAnalyzer:
# 如果三个分析都启用,使用并发执行
if topic_enabled and user_title_enabled and golden_quote_enabled:
# 并发执行所有三个分析任务
# 并发执行所有三个分析任务,传入活跃用户列表
topics, user_titles, golden_quotes, total_token_usage = await self.llm_analyzer.analyze_all_concurrent(
messages, user_analysis, umo=unified_msg_origin
messages, user_analysis, umo=unified_msg_origin, top_users=top_users
)
else:
# 如果只启用部分分析,则按需执行
@@ -63,7 +68,10 @@ class MessageAnalyzer:
total_token_usage.total_tokens += topic_tokens.total_tokens
if user_title_enabled:
user_titles, title_tokens = await self.llm_analyzer.analyze_user_titles(messages, user_analysis, umo=unified_msg_origin)
# 传入活跃用户列表
user_titles, title_tokens = await self.llm_analyzer.analyze_user_titles(
messages, user_analysis, umo=unified_msg_origin, top_users=top_users
)
total_token_usage.prompt_tokens += title_tokens.prompt_tokens
total_token_usage.completion_tokens += title_tokens.completion_tokens
total_token_usage.total_tokens += title_tokens.total_tokens
@@ -86,6 +94,5 @@ class MessageAnalyzer:
}
except Exception as e:
from astrbot.api import logger
logger.error(f"消息分析失败: {e}")
return None
return None