Files
Mofox-Core/docs/affinity_flow_chatter_optimization_summary.md
Windpicker-owo a6d2aee781 feat(affinity-flow): 通过标签扩展与提及分类增强兴趣匹配
- 实施扩展标签描述以实现更精确的语义匹配
- 增加强/弱提及分类,并附带独立的兴趣评分
- 重构机器人兴趣管理器,采用动态嵌入生成与缓存机制
- 通过增强的@提及处理功能优化消息处理
- 更新配置以支持回帖提升机制
- 将亲和力流量聊天重新组织为模块化结构,包含核心、规划器、主动响应和工具子模块
- 移除已弃用的规划器组件并整合功能
- 为napcat适配器插件添加数据库表初始化功能
- 修复元事件处理器中的心跳监控
2025-11-03 22:24:51 +08:00

20 KiB
Raw Blame History

Affinity Flow Chatter 插件优化总结

更新日期

2025年11月3日

优化概述

本次对 Affinity Flow Chatter 插件进行了全面的重构和优化主要包括目录结构优化、性能改进、bug修复和新功能添加。

<EFBFBD> 任务-1: 细化提及分数机制(强提及 vs 弱提及)

变更内容

将原有的统一提及分数细化为强提及弱提及两种类型,使用不同的分值。

原设计问题

旧逻辑

  • 所有提及方式使用同一个分值(mention_bot_interest_score
  • 被@、私聊、文本提到名字都是相同的重要性
  • 无法区分用户的真实意图

新设计

强提及Strong Mention

定义:用户明确想与bot交互

  • 被 @ 提及
  • 被回复
  • 私聊消息

分值strong_mention_interest_score = 2.5(默认)

弱提及Weak Mention

定义:在讨论中顺带提到bot

  • 消息中包含bot名字
  • 消息中包含bot别名

分值weak_mention_interest_score = 1.5(默认)

检测逻辑

def is_mentioned_bot_in_message(message) -> tuple[bool, float]:
    """
    Returns:
        tuple[bool, float]: (是否提及, 提及类型)
        提及类型: 0=未提及, 1=弱提及, 2=强提及
    """
    # 1. 检查私聊 → 强提及
    if is_private_chat:
        return True, 2.0
    
    # 2. 检查 @ → 强提及
    if is_at:
        return True, 2.0
    
    # 3. 检查回复 → 强提及
    if is_replied:
        return True, 2.0
    
    # 4. 检查文本匹配 → 弱提及
    if text_contains_bot_name_or_alias:
        return True, 1.0
    
    return False, 0.0

配置参数

config/bot_config.toml:

[affinity_flow]
# 提及bot相关参数
strong_mention_interest_score = 2.5  # 强提及(@/回复/私聊)
weak_mention_interest_score = 1.5    # 弱提及(文本匹配)

实际效果对比

场景1被@

用户: "@小狐 你好呀"
旧逻辑: 提及分 = 2.5
新逻辑: 提及分 = 2.5 (强提及) ✅ 保持不变

场景2回复bot

用户: [回复 小狐:...] "是的"
旧逻辑: 提及分 = 2.5
新逻辑: 提及分 = 2.5 (强提及) ✅ 保持不变

场景3私聊

用户: "在吗"
旧逻辑: 提及分 = 2.5
新逻辑: 提及分 = 2.5 (强提及) ✅ 保持不变

场景4文本提及

用户: "小狐今天没来吗"
旧逻辑: 提及分 = 2.5 (可能过高)
新逻辑: 提及分 = 1.5 (弱提及) ✅ 更合理

场景5讨论bot

用户A: "小狐这个bot挺有意思的"
旧逻辑: 提及分 = 2.5 (bot可能会插话)
新逻辑: 提及分 = 1.5 (弱提及,降低打断概率) ✅ 更自然

优势

  • 意图识别:区分"想对话"和"在讨论"
  • 减少误判:降低在他人讨论中插话的概率
  • 灵活调节:可以独立调整强弱提及的权重
  • 向后兼容:保持原有强提及的行为不变

影响文件

  • config/bot_config.toml:添加 strong/weak_mention_interest_score 配置
  • template/bot_config_template.toml:同步模板配置
  • src/config/official_configs.py:添加配置字段定义
  • src/chat/utils/utils.py:修改 is_mentioned_bot_in_message() 函数
  • src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py:使用新的强弱提及逻辑
  • docs/affinity_flow_guide.md:更新文档说明

<EFBFBD>🆔 任务0: 修改 Personality ID 生成逻辑

变更内容

bot_person_id 从固定值改为基于人设文本的 hash 生成,实现人设变化时自动触发兴趣标签重新生成。

原设计问题

旧逻辑

self.bot_person_id = person_info_manager.get_person_id("system", "bot_id")
# 结果md5("system_bot_id") = 固定值
  • personality_id 固定不变
  • 人设修改后不会重新生成兴趣标签
  • 需要手动清空数据库才能触发重新生成

新设计

新逻辑

personality_hash, _ = self._get_config_hash(bot_nickname, personality_core, personality_side, identity)
self.bot_person_id = personality_hash
# 结果md5(人设配置的JSON) = 动态值

Hash 生成规则

personality_config = {
    "nickname": bot_nickname,
    "personality_core": personality_core,
    "personality_side": personality_side,
    "compress_personality": global_config.personality.compress_personality,
}
personality_hash = md5(json_dumps(personality_config, sorted=True))

工作原理

  1. 初始化时:根据当前人设配置计算 hash 作为 personality_id
  2. 配置变化检测
    • 计算当前人设的 hash
    • 与上次保存的 hash 对比
    • 如果不同,触发重新生成
  3. 兴趣标签生成
    • bot_interest_manager 根据 personality_id 查询数据库
    • 如果 personality_id 不存在(人设变化了),自动生成新的兴趣标签
    • 保存时使用新的 personality_id

优势

  • 自动检测:人设改变后无需手动操作
  • 数据隔离:不同人设的兴趣标签分开存储
  • 版本管理:可以保留历史人设的兴趣标签(如果需要)
  • 逻辑清晰personality_id 直接反映人设内容

示例

人设 A:
  nickname: "小狐"
  personality_core: "活泼开朗"
  personality_side: "喜欢编程"
  → personality_id: a1b2c3d4e5f6...

人设 B (修改后):
  nickname: "小狐"
  personality_core: "冷静理性"  ← 改变
  personality_side: "喜欢编程"
  → personality_id: f6e5d4c3b2a1...  ← 自动生成新ID

结果:
- 数据库查询时找不到 f6e5d4c3b2a1 的兴趣标签
- 自动触发重新生成
- 新兴趣标签保存在 f6e5d4c3b2a1 下

影响范围

  • src/individuality/individuality.pypersonality_id 生成逻辑
  • src/chat/interest_system/bot_interest_manager.py:兴趣标签加载/保存(已支持)
  • 数据库:bot_personality_interests 表通过 personality_id 字段关联

📁 任务1: 优化插件目录结构

变更内容

将原本扁平的文件结构重组为分层目录,提高代码可维护性:

affinity_flow_chatter/
├── core/                          # 核心模块
│   ├── __init__.py
│   ├── affinity_chatter.py       # 主聊天处理器
│   └── affinity_interest_calculator.py  # 兴趣度计算器
│
├── planner/                       # 规划器模块
│   ├── __init__.py
│   ├── planner.py                # 动作规划器
│   ├── planner_prompts.py        # 提示词模板
│   ├── plan_generator.py         # 计划生成器
│   ├── plan_filter.py            # 计划过滤器
│   └── plan_executor.py          # 计划执行器
│
├── proactive/                     # 主动思考模块
│   ├── __init__.py
│   ├── proactive_thinking_scheduler.py   # 主动思考调度器
│   ├── proactive_thinking_executor.py    # 主动思考执行器
│   └── proactive_thinking_event.py       # 主动思考事件
│
├── tools/                         # 工具模块
│   ├── __init__.py
│   ├── chat_stream_impression_tool.py    # 聊天印象工具
│   └── user_profile_tool.py              # 用户档案工具
│
├── plugin.py                      # 插件注册
├── __init__.py                    # 插件元数据
└── README.md                      # 文档

优势

  • 逻辑清晰:相关功能集中在同一目录
  • 易于维护:模块职责明确,便于定位和修改
  • 可扩展性:新功能可以轻松添加到对应目录
  • 团队协作:多人开发时减少文件冲突

💾 任务2: 修改 Embedding 存储策略

问题分析

原设计:兴趣标签的 embedding 向量2560维度浮点数组直接存储在数据库中

  • 数据库存储过长,可能导致写入失败
  • 每次加载需要反序列化大量数据
  • 数据库体积膨胀

解决方案

新设计Embedding 改为启动时动态生成并缓存在内存中

实现细节

1. 数据库存储(不再包含 embedding

# 保存时
tag_dict = {
    "tag_name": tag.tag_name,
    "weight": tag.weight,
    "expanded": tag.expanded,  # 扩展描述
    "created_at": tag.created_at.isoformat(),
    "updated_at": tag.updated_at.isoformat(),
    "is_active": tag.is_active,
    # embedding 不再存储
}

2. 启动时动态生成

async def _generate_embeddings_for_tags(self, interests: BotPersonalityInterests):
    """为所有兴趣标签生成embedding仅缓存在内存中"""
    for tag in interests.interest_tags:
        if tag.tag_name in self.embedding_cache:
            # 使用内存缓存
            tag.embedding = self.embedding_cache[tag.tag_name]
        else:
            # 动态生成新的embedding
            embedding = await self._get_embedding(tag.tag_name)
            tag.embedding = embedding  # 设置到内存对象
            self.embedding_cache[tag.tag_name] = embedding  # 缓存

3. 加载时处理

tag = BotInterestTag(
    tag_name=tag_data.get("tag_name", ""),
    weight=tag_data.get("weight", 0.5),
    expanded=tag_data.get("expanded"),
    embedding=None,  # 不从数据库加载,改为动态生成
    # ...
)

优势

  • 数据库轻量化:数据库只存储标签名和权重等元数据
  • 避免写入失败:不再因为数据过长导致数据库操作失败
  • 灵活性:可以随时切换 embedding 模型而无需迁移数据
  • 性能:内存缓存访问速度快

权衡

  • ⚠️ 启动时需要生成 embedding首次启动稍慢约10-20秒
  • 后续运行时使用内存缓存,性能与原来相当

🔧 任务3: 修复连续不回复阈值调整问题

问题描述

原实现中,连续不回复调整只提升了分数,但阈值保持不变:

# ❌ 错误的实现
adjusted_score = self._apply_no_reply_boost(total_score)  # 只提升分数
should_reply = adjusted_score >= self.reply_threshold  # 阈值不变

问题:动作阈值(non_reply_action_interest_threshold)没有被调整,导致即使回复阈值满足,动作阈值可能仍然不满足。

解决方案

改为同时降低回复阈值和动作阈值

def _apply_no_reply_threshold_adjustment(self) -> tuple[float, float]:
    """应用阈值调整(包括连续不回复和回复后降低机制)"""
    base_reply_threshold = self.reply_threshold
    base_action_threshold = global_config.affinity_flow.non_reply_action_interest_threshold
    
    total_reduction = 0.0
    
    # 连续不回复的阈值降低
    if self.no_reply_count > 0:
        no_reply_reduction = self.no_reply_count * self.probability_boost_per_no_reply
        total_reduction += no_reply_reduction
    
    # 应用到两个阈值
    adjusted_reply_threshold = max(0.0, base_reply_threshold - total_reduction)
    adjusted_action_threshold = max(0.0, base_action_threshold - total_reduction)
    
    return adjusted_reply_threshold, adjusted_action_threshold

使用

# ✅ 正确的实现
adjusted_reply_threshold, adjusted_action_threshold = self._apply_no_reply_threshold_adjustment()
should_reply = adjusted_score >= adjusted_reply_threshold
should_take_action = adjusted_score >= adjusted_action_threshold

优势

  • 逻辑一致:回复阈值和动作阈值同步调整
  • 避免矛盾:不会出现"满足回复但不满足动作"的情况
  • 更合理连续不回复时bot更容易采取任何行动

⏱️ 任务4: 添加兴趣度计算超时机制

问题描述

兴趣匹配计算调用 embedding API可能因为网络问题或模型响应慢导致

  • 长时间等待(>5秒
  • 整体超时导致强制使用默认分值
  • 丢失了提及分和关系分(因为整个计算被中断)

解决方案

为兴趣匹配计算添加1.5秒超时保护,超时时返回默认分值:

async def _calculate_interest_match_score(self, content: str, keywords: list[str] | None = None) -> float:
    """计算兴趣匹配度(带超时保护)"""
    try:
        # 使用 asyncio.wait_for 添加1.5秒超时
        match_result = await asyncio.wait_for(
            bot_interest_manager.calculate_interest_match(content, keywords or []),
            timeout=1.5
        )
        
        if match_result:
            # 正常计算分数
            final_score = match_result.overall_score * 1.15 * match_result.confidence + match_count_bonus
            return final_score
        else:
            return 0.0
    
    except asyncio.TimeoutError:
        # 超时时返回默认分值 0.5
        logger.warning("⏱️ 兴趣匹配计算超时(>1.5秒)返回默认分值0.5以保留其他分数")
        return 0.5  # 避免丢失提及分和关系分
    
    except Exception as e:
        logger.warning(f"智能兴趣匹配失败: {e}")
        return 0.0

工作流程

正常情况(<1.5秒):
  兴趣匹配分: 0.8 + 关系分: 0.3 + 提及分: 2.5 = 3.6 ✅

超时情况(>1.5秒):
  兴趣匹配分: 0.5(默认)+ 关系分: 0.3 + 提及分: 2.5 = 3.3 ✅
  (保留了关系分和提及分)

强制中断(无超时保护):
  整体计算失败 = 0.0(默认) ❌
  (丢失了所有分数)

优势

  • 防止阻塞不会因为一个API调用卡住整个流程
  • 保留分数:即使兴趣匹配超时,提及分和关系分依然有效
  • 用户体验:响应更快,不会长时间无反应
  • 降级优雅:超时时仍能给出合理的默认值

🔄 任务5: 实现回复后阈值降低机制

需求背景

目标让bot在回复后更容易进行连续对话提升对话的连贯性和自然性。

场景示例

用户: "你好呀"
Bot: "你好!今天过得怎么样?" ← 此时激活连续对话模式

用户: "还不错"
Bot: "那就好~有什么有趣的事情吗?" ← 阈值降低,更容易回复

用户: "没什么"
Bot: "嗯嗯,那要不要聊聊别的?" ← 仍然更容易回复

用户: "..."
(如果一直不回复,降低效果会逐渐衰减)

配置项

bot_config.toml 中添加:

# 回复后连续对话机制参数
enable_post_reply_boost = true  # 是否启用回复后阈值降低机制
post_reply_threshold_reduction = 0.15  # 回复后初始阈值降低值
post_reply_boost_max_count = 3  # 回复后阈值降低的最大持续次数
post_reply_boost_decay_rate = 0.5  # 每次回复后阈值降低衰减率0-1

实现细节

1. 初始化计数器

def __init__(self):
    # 回复后阈值降低机制
    self.enable_post_reply_boost = affinity_config.enable_post_reply_boost
    self.post_reply_boost_remaining = 0  # 剩余的回复后降低次数
    self.post_reply_threshold_reduction = affinity_config.post_reply_threshold_reduction
    self.post_reply_boost_max_count = affinity_config.post_reply_boost_max_count
    self.post_reply_boost_decay_rate = affinity_config.post_reply_boost_decay_rate

2. 阈值调整

def _apply_no_reply_threshold_adjustment(self) -> tuple[float, float]:
    """应用阈值调整"""
    total_reduction = 0.0
    
    # 1. 连续不回复的降低
    if self.no_reply_count > 0:
        no_reply_reduction = self.no_reply_count * self.probability_boost_per_no_reply
        total_reduction += no_reply_reduction
    
    # 2. 回复后的降低(带衰减)
    if self.enable_post_reply_boost and self.post_reply_boost_remaining > 0:
        # 计算衰减因子
        decay_factor = self.post_reply_boost_decay_rate ** (
            self.post_reply_boost_max_count - self.post_reply_boost_remaining
        )
        post_reply_reduction = self.post_reply_threshold_reduction * decay_factor
        total_reduction += post_reply_reduction
    
    # 应用总降低量
    adjusted_reply_threshold = max(0.0, base_reply_threshold - total_reduction)
    adjusted_action_threshold = max(0.0, base_action_threshold - total_reduction)
    
    return adjusted_reply_threshold, adjusted_action_threshold

3. 状态更新

def on_reply_sent(self):
    """当机器人发送回复后调用"""
    if self.enable_post_reply_boost:
        # 重置回复后降低计数器
        self.post_reply_boost_remaining = self.post_reply_boost_max_count
        # 同时重置不回复计数
        self.no_reply_count = 0

def on_message_processed(self, replied: bool):
    """消息处理完成后调用"""
    # 更新不回复计数
    self.update_no_reply_count(replied)
    
    # 如果已回复,激活回复后降低机制
    if replied:
        self.on_reply_sent()
    else:
        # 如果没有回复,减少回复后降低剩余次数
        if self.post_reply_boost_remaining > 0:
            self.post_reply_boost_remaining -= 1

衰减机制说明

衰减公式

decay_factor = decay_rate ^ (max_count - remaining_count)
actual_reduction = base_reduction * decay_factor

示例base_reduction=0.15, decay_rate=0.5, max_count=3

第1次回复后: decay_factor = 0.5^0 = 1.00, reduction = 0.15 * 1.00 = 0.15
第2次回复后: decay_factor = 0.5^1 = 0.50, reduction = 0.15 * 0.50 = 0.075
第3次回复后: decay_factor = 0.5^2 = 0.25, reduction = 0.15 * 0.25 = 0.0375

实际效果

配置示例

  • 回复阈值: 0.7
  • 初始降低值: 0.15
  • 最大次数: 3
  • 衰减率: 0.5

对话流程

初始状态:
  回复阈值: 0.7
  
Bot发送回复 → 激活连续对话模式:
  剩余次数: 3
  
第1条消息:
  阈值降低: 0.15
  实际阈值: 0.7 - 0.15 = 0.55 ✅ 更容易回复
  
第2条消息:
  阈值降低: 0.075 (衰减)
  实际阈值: 0.7 - 0.075 = 0.625
  
第3条消息:
  阈值降低: 0.0375 (继续衰减)
  实际阈值: 0.7 - 0.0375 = 0.6625
  
第4条消息:
  降低结束,恢复正常阈值: 0.7

优势

  • 连贯对话bot回复后更容易继续对话
  • 自然衰减:避免无限连续回复,逐渐恢复正常
  • 可配置:可以根据需求调整降低值、次数和衰减率
  • 灵活控制:可以随时启用/禁用此功能

📊 整体影响

性能优化

  • 内存优化:不再在数据库中存储大量 embedding 数据
  • 响应速度:超时保护避免长时间等待
  • 启动速度:首次启动需要生成 embedding10-20秒后续运行使用缓存

功能增强

  • 阈值调整:修复了回复和动作阈值不一致的问题
  • 连续对话:新增回复后阈值降低机制,提升对话连贯性
  • 容错能力超时保护确保即使API失败也能保留其他分数

代码质量

  • 目录结构:清晰的模块划分,易于维护
  • 可扩展性:新功能可以轻松添加到对应目录
  • 可配置性:关键参数可通过配置文件调整

🔧 使用说明

配置调整

config/bot_config.toml 中调整回复后连续对话参数:

[affinity_flow]
# 回复后连续对话机制
enable_post_reply_boost = true  # 启用/禁用
post_reply_threshold_reduction = 0.15  # 初始降低值建议0.1-0.2
post_reply_boost_max_count = 3  # 持续次数建议2-5
post_reply_boost_decay_rate = 0.5  # 衰减率建议0.3-0.7

调用方式

在 planner 或其他需要的地方调用:

# 计算兴趣值
result = await interest_calculator.execute(message)

# 消息处理完成后更新状态
interest_calculator.on_message_processed(replied=result.should_reply)

🐛 已知问题

暂无


📝 后续优化建议

  1. 监控日志:观察实际使用中的阈值调整效果
  2. A/B测试:对比启用/禁用回复后降低机制的对话质量
  3. 参数调优:根据实际使用情况调整默认配置值
  4. 性能监控:监控 embedding 生成的时间和缓存命中率

👥 贡献者

  • GitHub Copilot - 代码实现和文档编写

📅 更新历史

  • 2025-11-03: 完成所有5个任务的实现
    • 优化插件目录结构
    • 修改 embedding 存储策略
    • 修复连续不回复阈值调整
    • 添加超时保护机制
    • 实现回复后阈值降低