feat(profile):对用户关系和分析系统进行重构,采用结构化数据和异步更新

此提交完全重写了用户关系和分析系统,创建了一个更强大、详细和响应式的框架。旧系统已被弃用,取而代之的是一个集中式的`UserRelationships`模型。

主要变更:

1.  ‌**增强数据库模型(`UserRelationships`):**‌
    - 添加`impression_text`用于长期、叙述式印象。
    - 引入`key_facts`(JSON)存储结构化数据如生日、工作和位置。
    - 添加`relationship_stage`跟踪关系进展(如陌生人、朋友、挚友)。
    - 添加`first_met_time`和`last_impression_update`的时间戳。

2.  ‌**重设计`UserProfileTool`:**‌
    - 工具的用途被限定为仅捕捉重要新信息,防止用于小聊。
    - 更新现在在后台异步处理,确保机器人回复不被延迟。
    - 引入`key_info_type`和`key_info_value`参数供LLM提交结构化事实。

3.  ‌**复杂的印象和情感逻辑:**‌
    - 关系追踪LLM现在分析最近聊天历史生成更丰富、更上下文的印象。
    - 用渐进的`affection_change`(最大±0.03)取代直接情感分数设置,使关系发展更真实。

4.  ‌**数据源整合:**‌
    - `RelationshipFetcher`重构为仅依赖`UserRelationships`表作为唯一数据源。
    - 简化`get_user_relationship` API并移除其缓存,确保分析的实时数据访问。

破坏性变更:`UserProfileTool`已重设计,新增参数(`key_info_type`、`key_info_value`)并改变用途。移除`affection_score`参数。此外,`get_user_relationship`数据库API签名简化为仅接受`user_id`。
This commit is contained in:
tt-P607
2025-12-03 16:53:40 +08:00
parent 39c52490d9
commit 2671a6e7e5
9 changed files with 1233 additions and 202 deletions

View File

@@ -0,0 +1,489 @@
# 用户画像系统重构规划 v2
> **决策:统一使用 `user_relationships` 表,废弃 `person_info` 表的印象相关功能**
>
> 旧表数据保留但不再调用,作为历史存档。
---
## 一、设计理念:像人一样记住别人
### 1.1 人类是怎么记住一个人的?
当我们认识一个人,我们的记忆不是一堆标签,而是:
1. **第一印象** - "这个人看起来挺随和的"
2. **具体片段** - "上次他帮我修电脑,折腾了一晚上"
3. **情感色彩** - "和他聊天很舒服,不会有压力"
4. **关键信息** - "他生日是11月23日喜欢吃辣"
5. **关系定位** - "算是比较聊得来的朋友"
6. **印象演变** - "一开始觉得他很高冷,后来发现其实很话痨"
### 1.2 当前系统的问题
```
❌ 现状:
"该用户性格开朗喜欢编程、游戏、音乐关系分数0.65"
✅ 期望:
"柒柒是个挺有意思的人,说话很有逻辑但偶尔会冒出一些冷笑话。
和他聊天挺舒服的,他不会问让人尴尬的问题。他在游戏公司做后端,
经常加班但从不抱怨。他说以后想开一家咖啡店,感觉是认真的。
生日是11月23日。"
```
### 1.3 与记忆系统的分工
| 系统 | 负责内容 | 特点 |
|------|---------|------|
| **上下文** | 当前对话内容 | 即时、完整 |
| **短期记忆** | 近期聊天片段 | 几天内、可检索 |
| **长期记忆** | 重要事件、知识 | 持久、语义检索 |
| **用户印象(本系统)** | 对人的认知 | 长期、主观、人格化 |
**本系统只关注**
- ✅ 对这个人的**长期印象**(性格、相处感受)
-**重要的具体信息**(生日、职业、理想等)
-**关系状态**(熟悉程度、好感度)
**不需要记录**
- ❌ 最近聊了什么(短期记忆负责)
- ❌ 具体对话内容(上下文和长期记忆负责)
- ❌ 临时性的事件(记忆系统负责)
### 1.4 新系统的设计目标
| 维度 | 要求 |
|------|------|
| **长期** | 只记稳定的、长期的认知,不记临时的 |
| **精炼** | 200-500字的印象 + 关键信息列表 |
| **主观** | 是"我"眼中的TA不是客观档案 |
| **实时** | 通过工具调用实时更新 |
---
## 二、数据结构重设计
### 2.1 扩展 `user_relationships` 表
```sql
-- 保留现有字段
id, user_id, user_name, user_aliases, relationship_score, last_updated, created_at
-- relationship_text 重命名为 impression_text
-- preference_keywords 废弃
-- 新增字段
ALTER TABLE user_relationships ADD COLUMN impression_text TEXT; -- 长期印象
ALTER TABLE user_relationships ADD COLUMN key_facts TEXT; -- 关键信息JSON
ALTER TABLE user_relationships ADD COLUMN relationship_stage VARCHAR(50); -- 关系阶段
ALTER TABLE user_relationships ADD COLUMN first_met_time FLOAT; -- 认识时间
ALTER TABLE user_relationships ADD COLUMN last_impression_update FLOAT; -- 上次更新印象时间
```
### 2.2 字段说明
#### `impression_text` - 长期印象(核心)
**精炼的、稳定的**对这个人的认知,不是流水账:
```
示例200-400字
"柒柒是个典型的理科生,说话很有逻辑,但偶尔会冒出一些冷笑话。
和他聊天挺舒服的,不会有压力,他也不会问让人尴尬的问题。
他在游戏公司做后端开发,虽然经常加班但从不抱怨,感觉是个挺拼的人。
他对技术很感兴趣经常问我一些AI相关的问题虽然有些问题挺刁钻
但能感觉到他是真的好奇。
他说以后想开一家自己的咖啡店,深夜聊天时说的,感觉不是随便说说的。
总的来说他给我的感觉是'表面随意但内心认真'的那种人。"
```
**更新时机**
- 对TA的认知发生明显变化时
- 关系有明显进展时
#### `key_facts` - 关键信息(长期记住)
存储**长期稳定、一定要记住**的重要信息(生日、职业这种不会变的):
**信息类型**
| 类型 | 说明 | 示例 |
|------|------|------|
| `birthday` | 生日 | "11月23日" |
| `job` | 职业/工作 | "程序员" |
| `location` | 所在地/老家 | "上海" |
| `dream` | 理想/目标 | "想开咖啡店" |
| `family` | 家庭 | "有个妹妹" |
| `pet` | 宠物 | "养了只橘猫" |
| `other` | 其他 | - |
**存储格式**
```json
[
{"type": "birthday", "value": "11月23日"},
{"type": "job", "value": "游戏公司后端程序员"},
{"type": "dream", "value": "想开咖啡店"},
{"type": "pet", "value": "养了只橘猫叫橘子"}
]
```
#### `relationship_stage` - 关系阶段
| 阶段 | 描述 | 对应分数范围 |
|------|------|-------------|
| stranger | 陌生人 | 0.0-0.2 |
| acquaintance | 初识 | 0.2-0.4 |
| familiar | 熟人 | 0.4-0.6 |
| friend | 朋友 | 0.6-0.75 |
| close_friend | 好友 | 0.75-0.9 |
| bestie | 挚友 | 0.9-1.0 |
---
## 三、工具调用设计(实时更新)
沿用之前的**两阶段设计**
1. `tool_use` 模型决定是否更新
2. `relationship_tracker` 模型生成高质量内容
### 3.1 工具1`update_user_impression` - 更新印象
```python
name = "update_user_impression"
description = """当你对某人有了新的、长期的认识时使用。
注意:只记录稳定的、长期的印象,临时的事情不用记。
适用场景:
- 发现了TA的性格特点
- 对TA有了新的认知
- 觉得和TA的关系有变化"""
parameters = [
("user_id", STRING, "用户ID", True),
("user_name", STRING, "怎么称呼TA", True),
("impression_update", STRING, "你对TA的新认识简要描述即可系统会用你的语气润色", True),
]
```
### 3.2 工具2`remember_user_info` - 记住重要信息
```python
name = "remember_user_info"
description = """当你知道了关于某人的长期重要信息时使用。
比如:生日、职业、理想、家庭、宠物等不会经常变的信息。"""
parameters = [
("user_id", STRING, "用户ID", True),
("user_name", STRING, "怎么称呼TA", True),
("info_type", STRING, "信息类型birthday/job/location/dream/family/pet/other", True),
("info_value", STRING, "具体内容", True),
]
```
### 3.3 更新流程
```
┌─────────────────────────────────────────────────────────────┐
│ tool_use 模型判断:需要更新印象/记住信息吗? │
└─────────────────────────────────────────────────────────────┘
│ 是
┌─────────────────────────────────────────────────────────────┐
│ 调用工具,传入简要信息 │
│ - impression_update: "他是个很细心的人" │
│ - 或 info_type: "birthday", info_value: "11月23日" │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ relationship_tracker 模型生成完整内容 │
│ - 融合现有印象 + 新信息 │
│ - 用人设语气润色 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 写入 user_relationships 表 │
└─────────────────────────────────────────────────────────────┘
```
---
## 四、读取和展示
### 4.1 构建关系信息时的呈现
修改 `RelationshipFetcher.build_relation_info()`
```python
async def build_relation_info(self, person_id, points_num=5):
"""构建关于某人的关系信息(新版)"""
person_info_manager = get_person_info_manager()
person_name = await person_info_manager.get_value(person_id, "person_name")
# 从 user_relationships 表读取所有数据
relationship = await get_user_relationship(platform, user_id, "bot")
if not relationship:
return f"你完全不认识{person_name},这是你们第一次交流。"
relation_parts = []
# 1. 基本信息:认识时间
if relationship.first_met_time:
from datetime import datetime
know_time = datetime.fromtimestamp(relationship.first_met_time).strftime("%Y年%m月")
relation_parts.append(f"你从{know_time}开始认识{person_name},已经聊过很多次了")
# 2. 别名
if relationship.user_aliases:
aliases_str = relationship.user_aliases.replace(",", "、")
relation_parts.append(f"{person_name}的别名:{aliases_str}")
# 3. 关系程度(保留好感度!)
if relationship.relationship_score is not None:
score = relationship.relationship_score
stage_desc = self._get_relationship_stage_description(relationship.relationship_stage or score)
relation_parts.append(f"你和{person_name}的关系:{stage_desc}(好感度{score:.2f}")
# 4. 核心:长期印象(新字段)
if relationship.impression_text:
relation_parts.append(f"\n你对{person_name}的印象:\n{relationship.impression_text}")
# 5. 喜好和兴趣(保留!)
if relationship.preference_keywords:
keywords_str = relationship.preference_keywords.replace(",", "、")
relation_parts.append(f"\n{person_name}的喜好和兴趣:{keywords_str}")
# 6. 关键信息(新字段)
if relationship.key_facts:
facts = json.loads(relationship.key_facts)
if facts:
facts_lines = [self._format_fact(f) for f in facts]
relation_parts.append(f"\n你记住的关于{person_name}的重要信息:\n" + "\n".join(facts_lines))
# 构建最终输出
if relation_parts:
return f"关于{person_name},你知道以下信息:\n" + "\n".join([f"• {p}" if not p.startswith("\n") else p for p in relation_parts])
else:
return f"你完全不认识{person_name},这是你们第一次交流。"
def _format_fact(self, fact: dict) -> str:
"""格式化单条关键信息"""
type_names = {
"birthday": "生日",
"job": "工作",
"location": "所在地",
"dream": "理想",
"family": "家庭",
"pet": "宠物",
"other": "其他"
}
type_name = type_names.get(fact.get("type", "other"), "其他")
return f"• {type_name}{fact.get('value', '')}"
```
### 4.2 展示效果示例
**完整的提示词插入内容**
```
关于言柒,你知道以下信息:
• 你从2024年6月开始认识言柒已经聊过很多次了
• 言柒的别名:柒柒、小柒
• 你和言柒的关系很亲近的好友好感度0.82
你对言柒的印象:
柒柒是个典型的理科生,说话很有逻辑,但偶尔会冒出一些冷笑话让人忍俊不禁。
和他聊天挺舒服的,不会有压力,他也不会问让人尴尬的问题。
他在游戏公司做后端开发,虽然经常加班但从不抱怨,感觉是个挺拼的人。
他对技术很感兴趣经常问我一些AI相关的问题虽然有些问题挺刁钻
但能感觉到他是真的好奇而不是在刁难我。
他说以后想开一家自己的咖啡店,深夜聊天时说的,感觉不是随便说说的。
总的来说他给我的感觉是"表面随意但内心认真"的那种人。
言柒的喜好和兴趣群内梗文化、AI技术、编程、游戏
你记住的关于言柒的重要信息:
• 生日11月23日
• 工作:游戏公司后端程序员
• 理想:想开咖啡店
• 宠物:养了只橘猫叫橘子
```
---
## 五、迁移计划
### 5.1 数据库变更
```sql
-- 1. 扩展 user_relationships 表
ALTER TABLE user_relationships ADD COLUMN impression_text TEXT; -- 长期印象
ALTER TABLE user_relationships ADD COLUMN key_facts TEXT; -- 关键信息JSON
ALTER TABLE user_relationships ADD COLUMN relationship_stage VARCHAR(50) DEFAULT 'stranger';
ALTER TABLE user_relationships ADD COLUMN first_met_time FLOAT;
ALTER TABLE user_relationships ADD COLUMN last_impression_update FLOAT;
-- 2. 保留的字段
-- relationship_score 保留!很多地方在用
-- preference_keywords 保留!喜好是印象的一部分
-- relationship_text 可作为 impression_text 的初始值迁移
-- 3. person_info 表不删除,但停止写入印象相关字段
```
### 5.2 代码变更清单
| 文件 | 变更内容 |
|------|---------|
| `models.py` | 扩展 UserRelationships 模型 |
| `user_profile_tool.py` | 重构为新的印象更新逻辑 |
| `relationship_fetcher.py` | 修改读取逻辑,只从 user_relationships 读 |
| `relationship_manager.py` | 废弃或精简,只保留必要的统计功能 |
| 新增 `user_fact_tool.py` | 记录具体信息的工具 |
### 5.3 废弃的功能
以下功能将**停止使用**(代码保留但不调用):
- `RelationshipManager.update_person_impression()` - 停止写入 person_info
- `RelationshipManager._update_impression()` - 停止使用
- `person_info` 表的以下字段停止更新:
- `impression`
- `short_impression`
- `points`
- `forgotten_points`
- `attitude`
---
## 六、实施步骤
### Phase 1数据库扩展 ✅ 已完成
- [x] 修改 `models.py`,添加新字段
- `impression_text`: 长期印象
- `key_facts`: 关键信息JSON
- `relationship_stage`: 关系阶段
- `first_met_time`: 认识时间
- `last_impression_update`: 上次更新时间
### Phase 2核心工具重构 ✅ 已完成
- [x] 重构 `UserProfileTool`
- [x] 新增 `_get_recent_chat_history()` 读取聊天记录
- [x] 新增 `_generate_impression_with_affection()` 联动生成印象+好感度
- [x] 新增 `_calculate_relationship_stage()` 自动计算关系阶段
- [x] 创建 `UserFactTool` (remember_user_info 工具)
- [x] 编写印象生成提示词(包含好感度变化规则)
### Phase 3读取层适配 ✅ 已完成
- [x] 修改 `RelationshipFetcher.build_relation_info()`
- [x] 读取新字段 `impression_text`, `key_facts`, `relationship_stage`
- [x] 新增辅助方法 `_get_stage_description()`, `_format_key_facts()`
- [x] 优先使用新字段,兼容旧字段
### Phase 4清理与优化待后续
- [ ] 标记废弃的代码
- [ ] 添加迁移说明
- [ ] 性能测试
---
## 七、新旧系统提示词输出对比
### 7.1 旧系统输出(当前)
```
关于言柒,你知道以下信息:
• 用户言柒在QQ平台的昵称是柒柒
• 你从2024年06月21日开始认识言柒
• 你们已经交流过2357次
• 最近一次交流是在08月21日
• 你对言柒的态度是比较有好感
• 你对ta的总体印象一个挺有趣的理科男
• 更详细的了解:这位用户是群里的'真相洞察者'与'梗文化解构师'善于在群聊中发现有趣的点并进行吐槽说话直接但不会让人觉得刻薄。对技术尤其是AI领域比较感兴趣经常会问一些技术相关的问题。
• 你记得关于言柒的一些事情:
- 他说最近工作很累经常加班2024-08-20
- 他对AI很感兴趣想学习2024-08-15
- 他通过了新公司的面试很开心2024-08-10
• 言柒的别名有:柒柒、小柒
• 你对言柒的整体认知:这位用户是群里的'真相洞察者'与'梗文化解构师'...
• 言柒的偏好和兴趣群内梗文化、真相揭露、二次元文化、AI技术、编程
• 你们的关系程度非常亲密的好友0.82
```
**问题**
- 数据来自两个表,有重复("总体印象"和"整体认知"说的差不多)
- 格式像数据库报表,不像人的认知
- points 是碎片化的事件记录,不是对人的理解
---
### 7.2 新系统输出(改造后)
```
关于言柒,你知道以下信息:
• 你从2024年6月开始认识言柒已经聊过很多次了
• 言柒的别名:柒柒、小柒
• 你和言柒的关系很亲近的好友好感度0.82
你对言柒的印象:
柒柒是个典型的理科生,说话很有逻辑,但偶尔会冒出一些冷笑话让人忍俊不禁。
和他聊天挺舒服的,不会有压力,他也不会问让人尴尬的问题。
他在游戏公司做后端开发,虽然经常加班但从不抱怨,感觉是个挺拼的人。
他对技术很感兴趣经常问我一些AI相关的问题虽然有些问题挺刁钻
但能感觉到他是真的好奇而不是在刁难我。
他说以后想开一家自己的咖啡店,深夜聊天时说的,感觉不是随便说说的。
总的来说他给我的感觉是"表面随意但内心认真"的那种人。
言柒的喜好和兴趣群内梗文化、AI技术、编程、游戏
你记住的关于言柒的重要信息:
• 生日11月23日
• 工作:游戏公司后端程序员
• 理想:想开咖啡店
• 宠物:养了只橘猫叫橘子
```
**改进点**
- 只从 `user_relationships` 一个表读取,不再重复
- **长期印象**impression_text是完整的叙事像真的在描述一个认识的人
- **好感度**relationship_score保留给AI参考
- **喜好**preference_keywords保留是印象的一部分
- **关键信息**key_facts是长期稳定的事实不是临时事件
- 临时事件("最近加班很累")不在这里,交给记忆系统
---
### 7.3 数据来源对照
| 输出内容 | 旧系统来源 | 新系统来源 |
|---------|-----------|-----------|
| 认识时间 | person_info.know_since | user_relationships.first_met_time |
| 别名 | person_info.nickname + user_relationships.user_aliases | user_relationships.user_aliases |
| 好感度 | person_info.attitude | **保留** user_relationships.relationship_score |
| 印象 | person_info.impression + user_relationships.relationship_text | **合并到** user_relationships.impression_text |
| 喜好 | user_relationships.preference_keywords | **保留** user_relationships.preference_keywords |
| 记忆点 | person_info.points | 移除(交给记忆系统) |
| 关键信息 | 无 | **新增** user_relationships.key_facts |
---
## 附录:相关文件
| 文件 | 状态 | 说明 |
|------|------|------|
| `src/common/database/core/models.py` | 需修改 | 扩展 UserRelationships |
| `src/plugins/.../user_profile_tool.py` | 需重构 | 新的印象更新逻辑 |
| `src/person_info/relationship_fetcher.py` | 需修改 | 适配新数据结构 |
| `src/person_info/relationship_manager.py` | 废弃 | 印象功能停用 |
| `src/person_info/person_info.py` | 保留 | 只用于基础信息 |

View File

@@ -401,26 +401,21 @@ async def get_usage_statistics(
# ===== UserRelationships 业务API ===== # ===== UserRelationships 业务API =====
@cached(ttl=600, key_prefix="user_relationship") # 缓存10分钟 # 注意:这个函数不使用缓存,因为用户画像工具会频繁更新,需要实时读取最新数据
async def get_user_relationship( async def get_user_relationship(
platform: str,
user_id: str, user_id: str,
target_id: str, **_kwargs, # 兼容旧调用忽略platform和target_id
) -> UserRelationships | None: ) -> UserRelationships | None:
"""获取用户关系 """获取用户关系
Args: Args:
platform: 平台
user_id: 用户ID user_id: 用户ID
target_id: 目标用户ID
Returns: Returns:
用户关系实例 用户关系实例
""" """
return await _user_relationships_crud.get_by( return await _user_relationships_crud.get_by(
platform=platform,
user_id=user_id, user_id=user_id,
target_id=target_id,
) )

View File

@@ -105,8 +105,7 @@ async def check_and_migrate_database(existing_engine=None):
column = table.c[column_name] column = table.c[column_name]
# 获取列类型的 SQL 表示 # 获取列类型的 SQL 表示
# 使用 compile 方法获取正确的类型字符串 # 直接使用 compile 方法,它会自动选择正确的方言
type_compiler = dialect.type_compiler(dialect)
column_type_sql = column.type.compile(dialect=dialect) column_type_sql = column.type.compile(dialect=dialect)
# 构建 ALTER TABLE 语句 # 构建 ALTER TABLE 语句

View File

@@ -655,7 +655,16 @@ class UserPermissions(Base):
class UserRelationships(Base): class UserRelationships(Base):
"""用户关系模型 - 存储用户与bot的关系数据""" """用户关系模型 - 存储用户与bot的关系数据
核心字段:
- relationship_text: 当前印象描述(用于兼容旧系统,逐步迁移到 impression_text
- impression_text: 长期印象(新字段,自然叙事风格)
- preference_keywords: 用户偏好关键词
- relationship_score: 好感度分数(0-1)
- key_facts: 关键信息JSON生日、职业、理想等
- relationship_stage: 关系阶段stranger/acquaintance/friend/close_friend/bestie
"""
__tablename__ = "user_relationships" __tablename__ = "user_relationships"
@@ -663,9 +672,22 @@ class UserRelationships(Base):
user_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, unique=True, index=True) user_id: Mapped[str] = mapped_column(get_string_field(100), nullable=False, unique=True, index=True)
user_name: Mapped[str | None] = mapped_column(get_string_field(100), nullable=True) user_name: Mapped[str | None] = mapped_column(get_string_field(100), nullable=True)
user_aliases: Mapped[str | None] = mapped_column(Text, nullable=True) # 用户别名,逗号分隔 user_aliases: Mapped[str | None] = mapped_column(Text, nullable=True) # 用户别名,逗号分隔
relationship_text: Mapped[str | None] = mapped_column(Text, nullable=True)
# 印象相关(新旧兼容)
relationship_text: Mapped[str | None] = mapped_column(Text, nullable=True) # 旧字段,保持兼容
impression_text: Mapped[str | None] = mapped_column(Text, nullable=True) # 新字段:长期印象(自然叙事)
# 用户信息
preference_keywords: Mapped[str | None] = mapped_column(Text, nullable=True) # 用户偏好关键词,逗号分隔 preference_keywords: Mapped[str | None] = mapped_column(Text, nullable=True) # 用户偏好关键词,逗号分隔
relationship_score: Mapped[float] = mapped_column(Float, nullable=False, default=0.3) # 关系分数(0-1) key_facts: Mapped[str | None] = mapped_column(Text, nullable=True) # 关键信息JSON生日、职业等
# 关系状态
relationship_score: Mapped[float] = mapped_column(Float, nullable=False, default=0.3) # 好感度(0-1)
relationship_stage: Mapped[str | None] = mapped_column(get_string_field(50), nullable=True, default="stranger") # 关系阶段
# 时间记录
first_met_time: Mapped[float | None] = mapped_column(Float, nullable=True) # 首次认识时间戳
last_impression_update: Mapped[float | None] = mapped_column(Float, nullable=True) # 上次更新印象时间
last_updated: Mapped[float] = mapped_column(Float, nullable=False, default=time.time) last_updated: Mapped[float] = mapped_column(Float, nullable=False, default=time.time)
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False) created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, nullable=False)
@@ -673,4 +695,5 @@ class UserRelationships(Base):
Index("idx_user_relationship_id", "user_id"), Index("idx_user_relationship_id", "user_id"),
Index("idx_relationship_score", "relationship_score"), Index("idx_relationship_score", "relationship_score"),
Index("idx_relationship_updated", "last_updated"), Index("idx_relationship_updated", "last_updated"),
Index("idx_relationship_stage", "relationship_stage"),
) )

View File

@@ -352,7 +352,6 @@ class UnifiedMemoryManager:
- 如果是纯闲聊("你好""哈哈"、表情)→ 现有记忆充足 - 如果是纯闲聊("你好""哈哈"、表情)→ 现有记忆充足
- 如果涉及具体话题(人物、事件、知识)→ 考虑检索长期记忆 - 如果涉及具体话题(人物、事件、知识)→ 考虑检索长期记忆
- 如果用户提到过去的经历或需要回忆 → 需要检索长期记忆 - 如果用户提到过去的经历或需要回忆 → 需要检索长期记忆
3. **倾向于检索**:当不确定时,倾向于设置 `is_sufficient: false`,让系统检索长期记忆以提供更丰富的上下文。
**输出格式JSON** **输出格式JSON**
```json ```json

View File

@@ -101,7 +101,11 @@ class RelationshipFetcher:
del self.info_fetched_cache[person_id] del self.info_fetched_cache[person_id]
async def build_relation_info(self, person_id, points_num=5): async def build_relation_info(self, person_id, points_num=5):
"""构建详细的人物关系信息,包含从数据库中查询的丰富关系描述""" """构建详细的人物关系信息
注意:现在只从 user_relationships 表读取印象和关系数据,
person_info 表只用于获取基础信息(用户名、平台等)
"""
# 初始化log_prefix # 初始化log_prefix
await self._initialize_log_prefix() await self._initialize_log_prefix()
@@ -109,128 +113,84 @@ class RelationshipFetcher:
self._cleanup_expired_cache() self._cleanup_expired_cache()
person_info_manager = get_person_info_manager() person_info_manager = get_person_info_manager()
# 仅从 person_info 获取基础信息(不获取印象相关字段)
person_name = await person_info_manager.get_value(person_id, "person_name") person_name = await person_info_manager.get_value(person_id, "person_name")
short_impression = await person_info_manager.get_value(person_id, "short_impression")
full_impression = await person_info_manager.get_value(person_id, "impression")
attitude = await person_info_manager.get_value(person_id, "attitude") or 50
nickname_str = await person_info_manager.get_value(person_id, "nickname")
platform = await person_info_manager.get_value(person_id, "platform") platform = await person_info_manager.get_value(person_id, "platform")
know_times = await person_info_manager.get_value(person_id, "know_times") or 0
know_since = await person_info_manager.get_value(person_id, "know_since")
last_know = await person_info_manager.get_value(person_id, "last_know")
# 获取用户特征点
current_points = await person_info_manager.get_value(person_id, "points") or []
forgotten_points = await person_info_manager.get_value(person_id, "forgotten_points") or []
# 确保 points 是列表类型(可能从数据库返回字符串)
if not isinstance(current_points, list):
current_points = []
if not isinstance(forgotten_points, list):
forgotten_points = []
# 按时间排序并选择最有代表性的特征点
all_points = current_points + forgotten_points
if all_points:
# 按权重和时效性综合排序
all_points.sort(
key=lambda x: (float(x[1]) if len(x) > 1 else 0, float(x[2]) if len(x) > 2 else 0), reverse=True
)
selected_points = all_points[:points_num]
points_text = "\n".join([f"- {point[0]}{point[2]}" for point in selected_points if len(point) > 2])
else:
points_text = ""
# 构建详细的关系描述 # 构建详细的关系描述
relation_parts = [] relation_parts = []
# 1. 基本信息 # 从 UserRelationships 表获取完整关系信息(这是唯一的印象数据来源)
if nickname_str and person_name != nickname_str:
relation_parts.append(f"用户{person_name}{platform}平台的昵称是{nickname_str}")
# 2. 认识时间和频率
if know_since:
from datetime import datetime
know_time = datetime.fromtimestamp(know_since).strftime("%Y年%m月%d")
relation_parts.append(f"你从{know_time}开始认识{person_name}")
if know_times > 0:
relation_parts.append(f"你们已经交流过{int(know_times)}")
if last_know:
from datetime import datetime
last_time = datetime.fromtimestamp(last_know).strftime("%m月%d")
relation_parts.append(f"最近一次交流是在{last_time}")
# 3. 态度和印象
attitude_desc = self._get_attitude_description(attitude)
relation_parts.append(f"你对{person_name}的态度是{attitude_desc}")
if short_impression:
relation_parts.append(f"你对ta的总体印象{short_impression}")
if full_impression:
relation_parts.append(f"更详细的了解:{full_impression}")
# 4. 特征点和记忆
if points_text:
relation_parts.append(f"你记得关于{person_name}的一些事情:\n{points_text}")
# 5. 从UserRelationships表获取完整关系信息新系统
try: try:
from src.common.database.api.specialized import get_user_relationship from src.common.database.api.specialized import get_user_relationship
# 查询用户关系数据 # 查询用户关系数据
user_id = str(await person_info_manager.get_value(person_id, "user_id")) user_id = str(await person_info_manager.get_value(person_id, "user_id"))
platform = str(await person_info_manager.get_value(person_id, "platform"))
# 使用优化后的API带缓存 # 使用优化后的API带缓存- 只需要user_id
relationship = await get_user_relationship( relationship = await get_user_relationship(user_id=user_id)
platform=platform,
user_id=user_id,
target_id="bot", # 或者根据实际需要传入目标用户ID
)
if relationship: if relationship:
# 将SQLAlchemy对象转换为字典以保持兼容性 # 将SQLAlchemy对象转换为字典
# 直接使用 __dict__ 访问,避免触发 SQLAlchemy 的描述符和 lazy loading
# 方案A已经确保所有字段在缓存前都已预加载所以 __dict__ 中有完整数据
try: try:
rel_data = { rel_data = {
"user_aliases": relationship.__dict__.get("user_aliases"), "user_aliases": relationship.__dict__.get("user_aliases"),
"relationship_text": relationship.__dict__.get("relationship_text"), "relationship_text": relationship.__dict__.get("relationship_text"),
"impression_text": relationship.__dict__.get("impression_text"),
"preference_keywords": relationship.__dict__.get("preference_keywords"), "preference_keywords": relationship.__dict__.get("preference_keywords"),
"key_facts": relationship.__dict__.get("key_facts"),
"relationship_score": relationship.__dict__.get("relationship_score"), "relationship_score": relationship.__dict__.get("relationship_score"),
"relationship_stage": relationship.__dict__.get("relationship_stage"),
"first_met_time": relationship.__dict__.get("first_met_time"),
} }
except Exception as attr_error: except Exception as attr_error:
logger.warning(f"访问relationship对象属性失败: {attr_error}") logger.warning(f"访问relationship对象属性失败: {attr_error}")
rel_data = {} rel_data = {}
# 5.1 用户别名 # 1. 用户别名
if rel_data.get("user_aliases"): if rel_data.get("user_aliases"):
aliases_list = [alias.strip() for alias in rel_data["user_aliases"].split(",") if alias.strip()] aliases_list = [alias.strip() for alias in rel_data["user_aliases"].split(",") if alias.strip()]
if aliases_list: if aliases_list:
aliases_str = "".join(aliases_list) aliases_str = "".join(aliases_list)
relation_parts.append(f"{person_name}的别名有:{aliases_str}") relation_parts.append(f"{person_name}的别名有:{aliases_str}")
# 5.2 关系印象文本(主观认知) # 2. 关系阶段和好感度
if rel_data.get("relationship_text"): if rel_data.get("relationship_score") is not None:
relation_parts.append(f"你对{person_name}的整体认知:{rel_data['relationship_text']}") score = rel_data["relationship_score"]
stage = rel_data.get("relationship_stage") or self._get_stage_from_score(score)
stage_desc = self._get_stage_description(stage)
relation_parts.append(f"你和{person_name}的关系:{stage_desc}(好感度{score:.2f}")
# 5.3 用户偏好关键词 # 3. 认识时间
if rel_data.get("first_met_time"):
from datetime import datetime
first_met = datetime.fromtimestamp(rel_data["first_met_time"]).strftime("%Y年%m月")
relation_parts.append(f"你们从{first_met}开始认识")
# 4. 长期印象(优先使用新字段 impression_text回退到 relationship_text
impression = rel_data.get("impression_text") or rel_data.get("relationship_text")
if impression:
relation_parts.append(f"\n你对{person_name}的印象:\n{impression}")
# 5. 用户偏好关键词
if rel_data.get("preference_keywords"): if rel_data.get("preference_keywords"):
keywords_list = [kw.strip() for kw in rel_data["preference_keywords"].split(",") if kw.strip()] keywords_list = [kw.strip() for kw in rel_data["preference_keywords"].split(",") if kw.strip()]
if keywords_list: if keywords_list:
keywords_str = "".join(keywords_list) keywords_str = "".join(keywords_list)
relation_parts.append(f"{person_name}的偏好和兴趣:{keywords_str}") relation_parts.append(f"\n{person_name}的偏好和兴趣:{keywords_str}")
# 5.4 关系亲密程度(好感分数) # 6. 关键信息
if rel_data.get("relationship_score") is not None: if rel_data.get("key_facts"):
score_desc = self._get_relationship_score_description(rel_data["relationship_score"]) try:
relation_parts.append(f"你们的关系程度:{score_desc}{rel_data['relationship_score']:.2f}") import orjson
facts = orjson.loads(rel_data["key_facts"])
if facts and isinstance(facts, list):
facts_lines = self._format_key_facts(facts, person_name)
if facts_lines:
relation_parts.append(f"\n你记住的关于{person_name}的重要信息:\n{facts_lines}")
except Exception:
pass
except Exception as e: except Exception as e:
logger.error(f"查询UserRelationships表失败: {e}") logger.error(f"查询UserRelationships表失败: {e}")
@@ -383,6 +343,54 @@ class RelationshipFetcher:
else: else:
return "陌生人" return "陌生人"
def _get_stage_from_score(self, score: float) -> str:
"""根据好感度分数返回关系阶段"""
if score >= 0.9:
return "bestie"
elif score >= 0.75:
return "close_friend"
elif score >= 0.6:
return "friend"
elif score >= 0.4:
return "familiar"
elif score >= 0.2:
return "acquaintance"
else:
return "stranger"
def _get_stage_description(self, stage: str) -> str:
"""根据关系阶段返回描述性文字"""
stage_map = {
"stranger": "陌生人",
"acquaintance": "初识",
"familiar": "熟人",
"friend": "朋友",
"close_friend": "好友",
"bestie": "挚友",
}
return stage_map.get(stage, "未知关系")
def _format_key_facts(self, facts: list, person_name: str) -> str:
"""格式化关键信息列表"""
type_names = {
"birthday": "生日",
"job": "工作",
"location": "所在地",
"dream": "理想",
"family": "家庭",
"pet": "宠物",
"other": "其他"
}
lines = []
for fact in facts:
if isinstance(fact, dict):
fact_type = fact.get("type", "other")
value = fact.get("value", "")
type_name = type_names.get(fact_type, "其他")
if value:
lines.append(f"{type_name}{value}")
return "\n".join(lines)
async def _build_fetch_query(self, person_id, target_message, chat_history): async def _build_fetch_query(self, person_id, target_message, chat_history):
nickname_str = ",".join(global_config.bot.alias_names) nickname_str = ",".join(global_config.bot.alias_names)
name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。" name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"

View File

@@ -5,6 +5,7 @@ AffinityFlow Chatter 工具模块
""" """
from .chat_stream_impression_tool import ChatStreamImpressionTool from .chat_stream_impression_tool import ChatStreamImpressionTool
from .user_fact_tool import UserFactTool
from .user_profile_tool import UserProfileTool from .user_profile_tool import UserProfileTool
__all__ = ["ChatStreamImpressionTool", "UserProfileTool"] __all__ = ["ChatStreamImpressionTool", "UserProfileTool", "UserFactTool"]

View File

@@ -0,0 +1,170 @@
"""
用户关键信息记录工具
用于记录用户的长期重要信息,如生日、职业、理想、宠物等。
这些信息会存储在 user_relationships 表的 key_facts 字段中。
"""
import time
from typing import Any
import orjson
from sqlalchemy import select
from src.common.database.compatibility import get_db_session
from src.common.database.core.models import UserRelationships
from src.common.logger import get_logger
from src.plugin_system import BaseTool, ToolParamType
logger = get_logger("user_fact_tool")
class UserFactTool(BaseTool):
"""用户关键信息记录工具
用于记录生日、职业、理想、宠物等长期重要信息。
注意:一般情况下使用 update_user_profile 工具即可同时记录印象和关键信息。
此工具仅在需要单独补充记录信息时使用。
"""
name = "remember_user_info"
description = """【备用工具】单独记录用户的重要个人信息。
注意:大多数情况请直接使用 update_user_profile 工具(它可以同时更新印象和记录关键信息)。
仅当你只想补充记录一条信息、不需要更新印象时才使用此工具。"""
parameters = [
("target_user_id", ToolParamType.STRING, "目标用户的ID必须", True, None),
("target_user_name", ToolParamType.STRING, "目标用户的名字/昵称(必须)", True, None),
("info_type", ToolParamType.STRING, "信息类型birthday生日/job职业/location所在地/dream理想/family家庭/pet宠物/other其他", True, None),
("info_value", ToolParamType.STRING, "具体内容,如'11月23日''程序员''想开咖啡店'", True, None),
]
available_for_llm = True
history_ttl = 5
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
"""执行关键信息记录
Args:
function_args: 工具参数
Returns:
dict: 执行结果
"""
try:
# 提取参数
target_user_id = function_args.get("target_user_id")
target_user_name = function_args.get("target_user_name", target_user_id)
info_type = function_args.get("info_type", "other")
info_value = function_args.get("info_value", "")
if not target_user_id:
return {
"type": "error",
"id": "remember_user_info",
"content": "错误必须提供目标用户ID"
}
if not info_value:
return {
"type": "error",
"id": "remember_user_info",
"content": "错误:必须提供要记录的信息内容"
}
# 验证 info_type
valid_types = ["birthday", "job", "location", "dream", "family", "pet", "other"]
if info_type not in valid_types:
info_type = "other"
# 更新数据库
await self._add_key_fact(target_user_id, info_type, info_value)
# 生成友好的类型名称
type_names = {
"birthday": "生日",
"job": "职业",
"location": "所在地",
"dream": "理想",
"family": "家庭",
"pet": "宠物",
"other": "其他信息"
}
type_name = type_names.get(info_type, "信息")
result_text = f"已记住 {target_user_name}{type_name}{info_value}"
logger.info(f"记录用户关键信息: {target_user_id}, {info_type}={info_value}")
return {
"type": "user_fact_recorded",
"id": target_user_id,
"content": result_text
}
except Exception as e:
logger.error(f"记录用户关键信息失败: {e}")
return {
"type": "error",
"id": function_args.get("target_user_id", "unknown"),
"content": f"记录失败: {e!s}"
}
async def _add_key_fact(self, user_id: str, info_type: str, info_value: str):
"""添加或更新关键信息
Args:
user_id: 用户ID
info_type: 信息类型
info_value: 信息内容
"""
try:
current_time = time.time()
async with get_db_session() as session:
stmt = select(UserRelationships).where(UserRelationships.user_id == user_id)
result = await session.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
# 解析现有的 key_facts
try:
facts = orjson.loads(existing.key_facts) if existing.key_facts else []
except Exception:
facts = []
if not isinstance(facts, list):
facts = []
# 查找是否已有相同类型的信息
found = False
for i, fact in enumerate(facts):
if isinstance(fact, dict) and fact.get("type") == info_type:
# 更新现有记录
facts[i] = {"type": info_type, "value": info_value}
found = True
break
if not found:
# 添加新记录
facts.append({"type": info_type, "value": info_value})
# 更新数据库
existing.key_facts = orjson.dumps(facts).decode("utf-8")
existing.last_updated = current_time
else:
# 创建新用户记录
facts = [{"type": info_type, "value": info_value}]
new_profile = UserRelationships(
user_id=user_id,
user_name=user_id,
key_facts=orjson.dumps(facts).decode("utf-8"),
first_met_time=current_time,
last_updated=current_time
)
session.add(new_profile)
await session.commit()
logger.info(f"关键信息已保存: {user_id}, {info_type}={info_value}")
except Exception as e:
logger.error(f"保存关键信息失败: {e}")
raise

View File

@@ -3,7 +3,10 @@
采用两阶段设计: 采用两阶段设计:
1. 工具调用模型(tool_use)负责判断是否需要更新,传入基本信息 1. 工具调用模型(tool_use)负责判断是否需要更新,传入基本信息
2. 关系追踪模型(relationship_tracker)负责生成高质量的、有人设特色的印象内容 2. 关系追踪模型(relationship_tracker)负责
- 读取最近聊天记录
- 生成高质量的、有人设特色的印象内容
- 决定好感度变化(联动更新)
""" """
import time import time
@@ -11,40 +14,61 @@ from typing import Any
from sqlalchemy import select from sqlalchemy import select
from src.chat.utils.chat_message_builder import build_readable_messages
from src.common.database.compatibility import get_db_session from src.common.database.compatibility import get_db_session
from src.common.database.core.models import UserRelationships from src.common.database.core.models import UserRelationships
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config, model_config from src.config.config import global_config, model_config # type: ignore[attr-defined]
from src.plugin_system import BaseTool, ToolParamType from src.plugin_system import BaseTool, ToolParamType
# 默认好感度分数,用于配置未初始化时的回退
DEFAULT_RELATIONSHIP_SCORE = 0.3
logger = get_logger("user_profile_tool") logger = get_logger("user_profile_tool")
def _get_base_relationship_score() -> float:
"""安全获取基础好感度分数"""
if global_config and global_config.affinity_flow:
return global_config.affinity_flow.base_relationship_score
return DEFAULT_RELATIONSHIP_SCORE
class UserProfileTool(BaseTool): class UserProfileTool(BaseTool):
"""用户画像更新工具 """用户画像更新工具
两阶段设计: 两阶段设计:
- 第一阶段tool_use模型判断是否更新传入简要信息 - 第一阶段tool_use模型判断是否更新传入简要信息
- 第二阶段relationship_tracker模型生成有人设特色的印象描述 - 第二阶段relationship_tracker模型读取聊天记录,生成印象并决定好感度变化
""" """
name = "update_user_profile" name = "update_user_profile"
description = """当你通过聊天对某个人产生了新的认识或印象时使用此工具。 description = """记录你对某个人的重要认识。【不要频繁调用】
调用时机当你发现TA透露了新信息、展现了性格特点、表达了兴趣爱好或你们的互动让你对TA有了新感受时。 只在以下情况使用:
注意impression_hint只需要简单描述你观察到的要点系统会自动用你的人设风格来润色生成最终印象。""" 1. TA首次告诉你【具体的个人信息】生日日期、职业、所在城市、真实姓名等 → 必填 key_info_type 和 key_info_value
2. 你对TA产生了【显著的、值得长期记住的】新印象不是每次聊天都要记
3. 你们的关系有了【实质性变化】
【不要调用的情况】:
- 普通的日常对话、闲聊
- 只是聊得开心但没有实质性新认识
- TA只是表达了一下情绪或感受
- 你已经记录过类似的印象
此工具会在后台异步执行,不会阻塞你的回复。"""
parameters = [ parameters = [
("target_user_id", ToolParamType.STRING, "目标用户的ID必须", True, None), ("target_user_id", ToolParamType.STRING, "目标用户的ID必须", True, None),
("target_user_name", ToolParamType.STRING, "目标用户的名字/昵称(必须,用于生成印象时称呼)", True, None), ("target_user_name", ToolParamType.STRING, "目标用户的名字/昵称(必须,用于生成印象时称呼)", True, None),
("user_aliases", ToolParamType.STRING, "TA的其他昵称或别名多个用逗号分隔可选", False, None), ("user_aliases", ToolParamType.STRING, "TA的其他昵称或别名多个用逗号分隔可选", False, None),
("impression_hint", ToolParamType.STRING, "【简要描述】你观察到的关于TA的要点'很健谈,喜欢聊游戏,有点害羞'。系统会用你的人设风格润色(可选)", False, None), ("impression_hint", ToolParamType.STRING, "【简要描述】你观察到的关于TA的要点'很健谈,喜欢聊游戏,有点害羞'(可选,只填显著的新认识", False, None),
("preference_keywords", ToolParamType.STRING, "TA的兴趣爱好关键词'编程,游戏,音乐',用逗号分隔(可选)", False, None), ("preference_keywords", ToolParamType.STRING, "TA的兴趣爱好关键词'编程,游戏,音乐',用逗号分隔(可选)", False, None),
("affection_score", ToolParamType.FLOAT, "你对TA的好感度(0.0-1.0)。0.3=普通认识0.5=还不错的朋友0.7=很喜欢0.9=非常亲密。打分要保守(可选)", False, None), ("key_info_type", ToolParamType.STRING, "【重要信息类型】birthday(生日日期)/job(职业)/location(城市)/dream(人生理想)/family(家庭成员)/pet(宠物名字)。只有TA告诉你这些【具体事实】时才填不要用other", False, None),
("key_info_value", ToolParamType.STRING, "【重要信息内容】如'11月23日''程序员''北京'。必须是具体的事实信息!", False, None),
] ]
available_for_llm = True available_for_llm = True
history_ttl = 5 history_ttl = 5
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
"""执行用户画像更新 """执行用户画像更新(异步后台执行,不阻塞回复)
Args: Args:
function_args: 工具参数 function_args: 工具参数
@@ -52,6 +76,8 @@ class UserProfileTool(BaseTool):
Returns: Returns:
dict: 执行结果 dict: 执行结果
""" """
import asyncio
try: try:
# 提取参数 # 提取参数
target_user_id = function_args.get("target_user_id") target_user_id = function_args.get("target_user_id")
@@ -67,61 +93,33 @@ class UserProfileTool(BaseTool):
new_aliases = function_args.get("user_aliases", "") new_aliases = function_args.get("user_aliases", "")
impression_hint = function_args.get("impression_hint", "") impression_hint = function_args.get("impression_hint", "")
new_keywords = function_args.get("preference_keywords", "") new_keywords = function_args.get("preference_keywords", "")
new_score = function_args.get("affection_score") key_info_type = function_args.get("key_info_type", "")
key_info_value = function_args.get("key_info_value", "")
# 从数据库获取现有用户画像
existing_profile = await self._get_user_profile(target_user_id)
# 如果LLM没有传入任何有效参数返回提示 # 如果LLM没有传入任何有效参数返回提示
if not any([new_aliases, impression_hint, new_keywords, new_score is not None]): if not any([new_aliases, impression_hint, new_keywords, key_info_value]):
return { return {
"type": "info", "type": "info",
"id": target_user_id, "id": target_user_id,
"content": "提示:需要提供至少一项更新内容(别名、印象描述、偏好关键词或好感分数" "content": "提示:需要提供至少一项更新内容(别名、印象描述、偏好关键词或重要信息"
} }
# 🎯 核心使用relationship_tracker模型生成高质量印象 # 🎯 异步后台执行,不阻塞回复
final_impression = existing_profile.get("relationship_text", "") asyncio.create_task(self._background_update(
if impression_hint: target_user_id=target_user_id,
final_impression = await self._generate_impression_with_personality( target_user_name=str(target_user_name) if target_user_name else str(target_user_id),
target_user_name=str(target_user_name) if target_user_name else str(target_user_id), new_aliases=new_aliases,
impression_hint=str(impression_hint), impression_hint=impression_hint,
existing_impression=str(existing_profile.get("relationship_text", "")), new_keywords=new_keywords,
preference_keywords=str(new_keywords or existing_profile.get("preference_keywords", "")), key_info_type=key_info_type,
) key_info_value=key_info_value,
))
# 构建最终画像
final_profile = {
"user_aliases": new_aliases if new_aliases else existing_profile.get("user_aliases", ""),
"relationship_text": final_impression,
"preference_keywords": new_keywords if new_keywords else existing_profile.get("preference_keywords", ""),
"relationship_score": new_score if new_score is not None else existing_profile.get("relationship_score", global_config.affinity_flow.base_relationship_score),
}
# 确保分数在有效范围内
final_profile["relationship_score"] = max(0.0, min(1.0, float(final_profile["relationship_score"])))
# 更新数据库
await self._update_user_profile_in_db(target_user_id, final_profile)
# 构建返回信息
updates = []
if final_profile.get("user_aliases"):
updates.append(f"别名: {final_profile['user_aliases']}")
if final_profile.get("relationship_text"):
updates.append(f"印象: {final_profile['relationship_text'][:80]}...")
if final_profile.get("preference_keywords"):
updates.append(f"偏好: {final_profile['preference_keywords']}")
if final_profile.get("relationship_score") is not None:
updates.append(f"好感分: {final_profile['relationship_score']:.2f}")
result_text = f"已更新用户 {target_user_name} 的画像:\n" + "\n".join(updates)
logger.info(f"用户画像更新成功: {target_user_id}")
# 立即返回,让回复继续
return { return {
"type": "user_profile_update", "type": "user_profile_update",
"id": target_user_id, "id": target_user_id,
"content": result_text "content": f"正在后台更新对 {target_user_name} 的印象..."
} }
except Exception as e: except Exception as e:
@@ -132,89 +130,374 @@ class UserProfileTool(BaseTool):
"content": f"用户画像更新失败: {e!s}" "content": f"用户画像更新失败: {e!s}"
} }
async def _generate_impression_with_personality( async def _background_update(
self,
target_user_id: str,
target_user_name: str,
new_aliases: str,
impression_hint: str,
new_keywords: str,
key_info_type: str = "",
key_info_value: str = "",
):
"""后台执行用户画像更新"""
try:
# 从数据库获取现有用户画像
existing_profile = await self._get_user_profile(target_user_id)
# 🎯 如果有关键信息,先保存(生日、职业等重要信息)
if key_info_value:
await self._add_key_fact(target_user_id, key_info_type or "other", key_info_value)
logger.info(f"[后台] 已记录关键信息: {target_user_id}, {key_info_type}={key_info_value}")
# 获取最近的聊天记录
chat_history_text = await self._get_recent_chat_history(target_user_id)
# 🎯 核心使用relationship_tracker模型生成印象并决定好感度变化
final_impression = existing_profile.get("relationship_text", "")
affection_change = 0.0 # 好感度变化量
if impression_hint or chat_history_text:
impression_result = await self._generate_impression_with_affection(
target_user_name=target_user_name,
impression_hint=impression_hint,
existing_impression=str(existing_profile.get("relationship_text", "")),
preference_keywords=str(new_keywords or existing_profile.get("preference_keywords", "")),
chat_history=chat_history_text,
current_score=float(existing_profile.get("relationship_score", _get_base_relationship_score())),
)
final_impression = impression_result.get("impression", final_impression)
affection_change = impression_result.get("affection_change", 0.0)
# 计算新的好感度
old_score = float(existing_profile.get("relationship_score", _get_base_relationship_score()))
new_score = old_score + affection_change
new_score = max(0.0, min(1.0, new_score)) # 确保在0-1范围内
# 构建最终画像
final_profile = {
"user_aliases": new_aliases if new_aliases else existing_profile.get("user_aliases", ""),
"relationship_text": final_impression,
"preference_keywords": new_keywords if new_keywords else existing_profile.get("preference_keywords", ""),
"relationship_score": new_score,
}
# 更新数据库
await self._update_user_profile_in_db(target_user_id, final_profile)
logger.info(f"[后台] 用户画像更新成功: {target_user_id}, 好感度变化: {affection_change:+.2f}")
except Exception as e:
logger.error(f"[后台] 用户画像更新失败: {e}")
async def _add_key_fact(self, user_id: str, info_type: str, info_value: str):
"""添加或更新关键信息(生日、职业等)
Args:
user_id: 用户ID
info_type: 信息类型birthday/job/location/dream/family/pet/other
info_value: 信息内容
"""
import orjson
try:
# 验证 info_type
valid_types = ["birthday", "job", "location", "dream", "family", "pet", "other"]
if info_type not in valid_types:
info_type = "other"
current_time = time.time()
async with get_db_session() as session:
stmt = select(UserRelationships).where(UserRelationships.user_id == user_id)
result = await session.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
# 解析现有的 key_facts
try:
facts = orjson.loads(existing.key_facts) if existing.key_facts else []
except Exception:
facts = []
if not isinstance(facts, list):
facts = []
# 查找是否已有相同类型的信息
found = False
for i, fact in enumerate(facts):
if isinstance(fact, dict) and fact.get("type") == info_type:
# 更新现有记录
facts[i] = {"type": info_type, "value": info_value}
found = True
break
if not found:
# 添加新记录
facts.append({"type": info_type, "value": info_value})
# 更新数据库
existing.key_facts = orjson.dumps(facts).decode("utf-8")
existing.last_updated = current_time
else:
# 创建新用户记录
facts = [{"type": info_type, "value": info_value}]
new_profile = UserRelationships(
user_id=user_id,
user_name=user_id,
key_facts=orjson.dumps(facts).decode("utf-8"),
first_met_time=current_time,
last_updated=current_time
)
session.add(new_profile)
await session.commit()
# 清除缓存,确保下次查询获取最新数据
try:
from src.common.database.optimization.cache_manager import get_cache
cache = await get_cache()
cache_key = f"user_relationships:filter:[('user_id', '{user_id}')]"
await cache.delete(cache_key)
logger.debug(f"已清除用户关系缓存: {user_id}")
except Exception as cache_err:
logger.warning(f"清除缓存失败(不影响数据保存): {cache_err}")
logger.info(f"关键信息已保存: {user_id}, {info_type}={info_value}")
except Exception as e:
logger.error(f"保存关键信息失败: {e}")
# 不抛出异常,因为这是后台任务
async def _get_recent_chat_history(self, target_user_id: str, max_messages: int = 40) -> str:
"""获取最近的聊天记录
Args:
target_user_id: 目标用户ID
max_messages: 最大消息数量
Returns:
str: 格式化的聊天记录文本
"""
try:
# 从 chat_stream 获取上下文
if not self.chat_stream:
logger.warning("chat_stream 未初始化,无法获取聊天记录")
return ""
context = getattr(self.chat_stream, "context", None)
if not context:
logger.warning("chat_stream.context 不存在,无法获取聊天记录")
return ""
# 获取最近的消息 - 使用正确的方法名 get_messages
messages = context.get_messages(limit=max_messages, include_unread=True)
if not messages:
return ""
# 将 DatabaseMessages 对象转换为字典列表
messages_dict = []
for msg in messages:
try:
if hasattr(msg, 'to_dict'):
messages_dict.append(msg.to_dict())
elif hasattr(msg, '__dict__'):
# 手动构建字典
msg_dict = {
"time": getattr(msg, "time", 0),
"processed_plain_text": getattr(msg, "processed_plain_text", ""),
"display_message": getattr(msg, "display_message", ""),
}
# 处理 user_info
user_info = getattr(msg, "user_info", None)
if user_info:
msg_dict["user_info"] = {
"user_id": getattr(user_info, "user_id", ""),
"user_nickname": getattr(user_info, "user_nickname", ""),
}
# 处理 chat_info
chat_info = getattr(msg, "chat_info", None)
if chat_info:
msg_dict["chat_info"] = {
"platform": getattr(chat_info, "platform", ""),
}
messages_dict.append(msg_dict)
except Exception as e:
logger.warning(f"转换消息失败: {e}")
continue
if not messages_dict:
return ""
# 构建可读的消息文本
readable_messages = await build_readable_messages(
messages=messages_dict,
replace_bot_name=True,
timestamp_mode="normal_no_YMD",
truncate=True
)
return readable_messages or ""
except Exception as e:
logger.error(f"获取聊天记录失败: {e}")
return ""
async def _generate_impression_with_affection(
self, self,
target_user_name: str, target_user_name: str,
impression_hint: str, impression_hint: str,
existing_impression: str, existing_impression: str,
preference_keywords: str, preference_keywords: str,
) -> str: chat_history: str,
"""使用relationship_tracker模型生成有人设特色的印象描述 current_score: float,
) -> dict[str, Any]:
"""使用relationship_tracker模型生成印象并决定好感度变化
Args: Args:
target_user_name: 目标用户的名字 target_user_name: 目标用户的名字
impression_hint: 工具调用模型传入的简要观察 impression_hint: 工具调用模型传入的简要观察
existing_impression: 现有的印象描述 existing_impression: 现有的印象描述
preference_keywords: 用户的兴趣偏好 preference_keywords: 用户的兴趣偏好
chat_history: 最近的聊天记录
current_score: 当前好感度分数
Returns: Returns:
str: 生成的印象描述 dict: {"impression": str, "affection_change": float}
""" """
try: try:
import orjson
from json_repair import repair_json
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
# 获取人设信息 # 获取人设信息(添加空值保护)
bot_name = global_config.bot.nickname bot_name = global_config.bot.nickname if global_config and global_config.bot else "Bot"
personality_core = global_config.personality.personality_core personality_core = global_config.personality.personality_core if global_config and global_config.personality else ""
personality_side = global_config.personality.personality_side personality_side = global_config.personality.personality_side if global_config and global_config.personality else ""
reply_style = global_config.personality.reply_style if global_config and global_config.personality else ""
# 构建提示词 # 构建提示词
prompt = f"""你是{bot_name},现在要记录你对一个人的印象。 # 根据是否有旧印象决定任务类型
is_first_impression = not existing_impression or len(existing_impression) < 20
## 你的人设 prompt = f"""你是{bot_name},现在要记录你对"{target_user_name}"的印象。
## 你的核心人格
{personality_core} {personality_core}
## 你的性格特点 ## 你的性格侧面
{personality_side} {personality_side}
## 任务 ## 你的说话风格
根据下面的观察要点,用你自己的语气和视角,写一段对"{target_user_name}"的印象描述。 {reply_style}
## 观察到的要点 ## 你之前对{target_user_name}的印象
{impression_hint} {existing_impression if existing_impression else "这是你第一次记录对TA的印象"}
## TA的兴趣爱好 ## 最近的聊天记录
{chat_history if chat_history else "(无聊天记录)"}
## 这次观察到的新要点
{impression_hint if impression_hint else "(无特别观察)"}
## {target_user_name}的兴趣爱好
{preference_keywords if preference_keywords else "暂未了解"} {preference_keywords if preference_keywords else "暂未了解"}
## 之前对TA的印象如果有 ## 当前好感度
{existing_impression if existing_impression else "这是第一次记录对TA的印象"} {current_score:.2f} (范围0-10.3=普通认识0.5=朋友0.7=好友0.9=挚友)
## 写作要求 ## 任务
1. 用第一人称""来写,就像在写日记或者跟朋友聊天时描述一个人 1. 根据聊天记录判断{target_user_name}的性别(男"",女用"",无法判断用名字)
2. "{target_user_name}""TA"来称呼对方,不要用"该用户""此人" 2. {"写下你对这个人的第一印象" if is_first_impression else "在原有印象基础上,融入新的观察"}
3. 写出你真实的、主观的感受,可以带情绪和直觉判断 3. 决定好感度是否需要变化(大多数情况不需要)
4. 如果有之前的印象,可以结合新观察进行补充或修正
5. 长度控制在50-150字自然流畅
请直接输出印象描述,不要加任何前缀或解释:""" ## 印象写作要求
- 用第一人称""来写
- 根据判断的性别使用"他/她",或者直接用"{target_user_name}"
- {"第一印象可以短一些50-150字写下初步感受" if is_first_impression else "在原有印象基础上补充新认识150-300字"}
- {"不要假装很熟,你们才刚认识" if is_first_impression else "体现出你们相处时间的积累"}
- 写出这个人的特点、性格、给你的感觉
## 好感度变化规则(极度严格!)
- 范围:-0.03 到 +0.03**但默认是090%以上的对话好感度应该不变**
- 好感度是长期积累的结果,不是短短几句话就能改变的
- **绝对不变(=0)的情况**
- 普通聊天、日常问候、闲聊
- 正常的交流,即使聊得很开心
- 分享日常、讨论话题
- 简单的互相关心
- **可能微涨(+0.01)的情况**(很少见):
- 对方真正信任你,分享了很私密的心事或秘密
- 在你困难时主动帮助
- **可能涨(+0.02~0.03)的情况**(非常罕见):
- 真正触动内心的深度交流
- 长期相处后的重要情感突破
- 记住:聊得好≠好感度增加,好感是需要长时间培养的
请严格按照以下JSON格式输出
{{
"gender": "male/female/unknown",
"impression": "你对{target_user_name}的印象...",
"affection_change": 0,
"change_reason": "无变化/变化原因"
}}"""
# 使用relationship_tracker模型添加空值保护
if not model_config or not model_config.model_task_config:
raise ValueError("model_config 未初始化")
# 使用relationship_tracker模型
llm = LLMRequest( llm = LLMRequest(
model_set=model_config.model_task_config.relationship_tracker, model_set=model_config.model_task_config.relationship_tracker,
request_type="user_profile.impression_generator" request_type="user_profile.impression_and_affection"
) )
response, _ = await llm.generate_response_async( response, _ = await llm.generate_response_async(
prompt=prompt, prompt=prompt,
temperature=0.7, temperature=0.7,
max_tokens=300, max_tokens=600,
) )
# 清理响应 # 解析响应
impression = response.strip() response = response.strip()
try:
result = orjson.loads(repair_json(response))
impression = result.get("impression", "")
affection_change = float(result.get("affection_change", 0))
change_reason = result.get("change_reason", "")
detected_gender = result.get("gender", "unknown")
# 如果响应为空或太短回退到原始hint # 限制好感度变化范围(严格:-0.03 到 +0.03
if not impression or len(impression) < 10: affection_change = max(-0.03, min(0.03, affection_change))
logger.warning(f"印象生成结果过短使用原始hint: {impression_hint}")
return impression_hint
logger.info(f"成功生成有人设特色的印象描述,长度: {len(impression)}") # 如果印象为空或太短回退到hint
return impression if not impression or len(impression) < 10:
logger.warning(f"印象生成结果过短使用原始hint")
impression = impression_hint or existing_impression
logger.info(f"印象更新: 用户性别判断={detected_gender}, 好感度变化={affection_change:+.3f}")
if change_reason:
logger.info(f"好感度变化原因: {change_reason}")
return {
"impression": impression,
"affection_change": affection_change
}
except Exception as parse_error:
logger.warning(f"解析JSON失败: {parse_error},尝试提取文本")
# 如果JSON解析失败尝试直接使用响应作为印象
return {
"impression": response if len(response) > 10 else (impression_hint or existing_impression),
"affection_change": 0.0
}
except Exception as e: except Exception as e:
logger.error(f"生成印象描述失败回退到原始hint: {e}") logger.error(f"生成印象和好感度失败: {e}")
# 失败时回退到工具调用模型传入的hint # 失败时回退
return impression_hint return {
"impression": impression_hint or existing_impression,
"affection_change": 0.0
}
async def _get_user_profile(self, user_id: str) -> dict[str, Any]: async def _get_user_profile(self, user_id: str) -> dict[str, Any]:
"""从数据库获取用户现有画像 """从数据库获取用户现有画像
@@ -232,12 +515,18 @@ class UserProfileTool(BaseTool):
profile = result.scalar_one_or_none() profile = result.scalar_one_or_none()
if profile: if profile:
# 优先使用新字段 impression_text如果没有则用旧字段 relationship_text
impression = profile.impression_text or profile.relationship_text or ""
return { return {
"user_name": profile.user_name or user_id, "user_name": profile.user_name or user_id,
"user_aliases": profile.user_aliases or "", "user_aliases": profile.user_aliases or "",
"relationship_text": profile.relationship_text or "", "relationship_text": impression, # 兼容旧代码
"impression_text": impression,
"preference_keywords": profile.preference_keywords or "", "preference_keywords": profile.preference_keywords or "",
"relationship_score": float(profile.relationship_score) if profile.relationship_score is not None else global_config.affinity_flow.base_relationship_score, "key_facts": profile.key_facts or "[]",
"relationship_score": float(profile.relationship_score) if profile.relationship_score is not None else _get_base_relationship_score(),
"relationship_stage": profile.relationship_stage or "stranger",
"first_met_time": profile.first_met_time,
} }
else: else:
# 用户不存在,返回默认值 # 用户不存在,返回默认值
@@ -245,8 +534,12 @@ class UserProfileTool(BaseTool):
"user_name": user_id, "user_name": user_id,
"user_aliases": "", "user_aliases": "",
"relationship_text": "", "relationship_text": "",
"impression_text": "",
"preference_keywords": "", "preference_keywords": "",
"relationship_score": global_config.affinity_flow.base_relationship_score, "key_facts": "[]",
"relationship_score": _get_base_relationship_score(),
"relationship_stage": "stranger",
"first_met_time": None,
} }
except Exception as e: except Exception as e:
logger.error(f"获取用户画像失败: {e}") logger.error(f"获取用户画像失败: {e}")
@@ -254,8 +547,12 @@ class UserProfileTool(BaseTool):
"user_name": user_id, "user_name": user_id,
"user_aliases": "", "user_aliases": "",
"relationship_text": "", "relationship_text": "",
"impression_text": "",
"preference_keywords": "", "preference_keywords": "",
"relationship_score": global_config.affinity_flow.base_relationship_score, "key_facts": "[]",
"relationship_score": _get_base_relationship_score(),
"relationship_stage": "stranger",
"first_met_time": None,
} }
@@ -275,31 +572,81 @@ class UserProfileTool(BaseTool):
result = await session.execute(stmt) result = await session.execute(stmt)
existing = result.scalar_one_or_none() existing = result.scalar_one_or_none()
# 根据好感度自动计算关系阶段
score = profile.get("relationship_score", 0.3)
stage = self._calculate_relationship_stage(score)
if existing: if existing:
# 更新现有记录 # 更新现有记录
existing.user_aliases = profile.get("user_aliases", "") existing.user_aliases = profile.get("user_aliases", "")
existing.relationship_text = profile.get("relationship_text", "") # 同时更新新旧两个印象字段,保持兼容
impression = profile.get("relationship_text", "")
existing.relationship_text = impression
existing.impression_text = impression
existing.preference_keywords = profile.get("preference_keywords", "") existing.preference_keywords = profile.get("preference_keywords", "")
existing.relationship_score = profile.get("relationship_score", global_config.affinity_flow.base_relationship_score) existing.relationship_score = score
existing.relationship_stage = stage
existing.last_impression_update = current_time
existing.last_updated = current_time existing.last_updated = current_time
# 如果是首次认识,记录时间
if not existing.first_met_time:
existing.first_met_time = current_time
else: else:
# 创建新记录 # 创建新记录
impression = profile.get("relationship_text", "")
new_profile = UserRelationships( new_profile = UserRelationships(
user_id=user_id, user_id=user_id,
user_name=user_id, user_name=user_id,
user_aliases=profile.get("user_aliases", ""), user_aliases=profile.get("user_aliases", ""),
relationship_text=profile.get("relationship_text", ""), relationship_text=impression,
impression_text=impression,
preference_keywords=profile.get("preference_keywords", ""), preference_keywords=profile.get("preference_keywords", ""),
relationship_score=profile.get("relationship_score", global_config.affinity_flow.base_relationship_score), relationship_score=score,
relationship_stage=stage,
first_met_time=current_time,
last_impression_update=current_time,
last_updated=current_time last_updated=current_time
) )
session.add(new_profile) session.add(new_profile)
await session.commit() await session.commit()
logger.info(f"用户画像已更新到数据库: {user_id}")
# 清除缓存,确保下次查询获取最新数据
try:
from src.common.database.optimization.cache_manager import get_cache
cache = await get_cache()
cache_key = f"user_relationships:filter:[('user_id', '{user_id}')]"
await cache.delete(cache_key)
logger.debug(f"已清除用户关系缓存: {user_id}")
except Exception as cache_err:
logger.warning(f"清除缓存失败(不影响数据保存): {cache_err}")
logger.info(f"用户画像已更新到数据库: {user_id}, 阶段: {stage}")
except Exception as e: except Exception as e:
logger.error(f"更新用户画像到数据库失败: {e}") logger.error(f"更新用户画像到数据库失败: {e}")
raise raise
def _calculate_relationship_stage(self, score: float) -> str:
"""根据好感度分数计算关系阶段
Args:
score: 好感度分数(0-1)
Returns:
str: 关系阶段
"""
if score >= 0.9:
return "bestie" # 挚友
elif score >= 0.75:
return "close_friend" # 好友
elif score >= 0.6:
return "friend" # 朋友
elif score >= 0.4:
return "familiar" # 熟人
elif score >= 0.2:
return "acquaintance" # 初识
else:
return "stranger" # 陌生人