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:
489
docs/用户画像系统重构规划.md
Normal file
489
docs/用户画像系统重构规划.md
Normal 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` | 保留 | 只用于基础信息 |
|
||||
@@ -401,26 +401,21 @@ async def get_usage_statistics(
|
||||
|
||||
|
||||
# ===== UserRelationships 业务API =====
|
||||
@cached(ttl=600, key_prefix="user_relationship") # 缓存10分钟
|
||||
# 注意:这个函数不使用缓存,因为用户画像工具会频繁更新,需要实时读取最新数据
|
||||
async def get_user_relationship(
|
||||
platform: str,
|
||||
user_id: str,
|
||||
target_id: str,
|
||||
**_kwargs, # 兼容旧调用,忽略platform和target_id
|
||||
) -> UserRelationships | None:
|
||||
"""获取用户关系
|
||||
|
||||
Args:
|
||||
platform: 平台
|
||||
user_id: 用户ID
|
||||
target_id: 目标用户ID
|
||||
|
||||
Returns:
|
||||
用户关系实例
|
||||
"""
|
||||
return await _user_relationships_crud.get_by(
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
target_id=target_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -105,8 +105,7 @@ async def check_and_migrate_database(existing_engine=None):
|
||||
column = table.c[column_name]
|
||||
|
||||
# 获取列类型的 SQL 表示
|
||||
# 使用 compile 方法获取正确的类型字符串
|
||||
type_compiler = dialect.type_compiler(dialect)
|
||||
# 直接使用 compile 方法,它会自动选择正确的方言
|
||||
column_type_sql = column.type.compile(dialect=dialect)
|
||||
|
||||
# 构建 ALTER TABLE 语句
|
||||
|
||||
@@ -655,7 +655,16 @@ class UserPermissions(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"
|
||||
|
||||
@@ -663,9 +672,22 @@ class UserRelationships(Base):
|
||||
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_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) # 用户偏好关键词,逗号分隔
|
||||
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)
|
||||
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_relationship_score", "relationship_score"),
|
||||
Index("idx_relationship_updated", "last_updated"),
|
||||
Index("idx_relationship_stage", "relationship_stage"),
|
||||
)
|
||||
|
||||
@@ -352,7 +352,6 @@ class UnifiedMemoryManager:
|
||||
- 如果是纯闲聊("你好"、"哈哈"、表情)→ 现有记忆充足
|
||||
- 如果涉及具体话题(人物、事件、知识)→ 考虑检索长期记忆
|
||||
- 如果用户提到过去的经历或需要回忆 → 需要检索长期记忆
|
||||
3. **倾向于检索**:当不确定时,倾向于设置 `is_sufficient: false`,让系统检索长期记忆以提供更丰富的上下文。
|
||||
|
||||
**输出格式(JSON):**
|
||||
```json
|
||||
|
||||
@@ -101,7 +101,11 @@ class RelationshipFetcher:
|
||||
del self.info_fetched_cache[person_id]
|
||||
|
||||
async def build_relation_info(self, person_id, points_num=5):
|
||||
"""构建详细的人物关系信息,包含从数据库中查询的丰富关系描述"""
|
||||
"""构建详细的人物关系信息
|
||||
|
||||
注意:现在只从 user_relationships 表读取印象和关系数据,
|
||||
person_info 表只用于获取基础信息(用户名、平台等)
|
||||
"""
|
||||
# 初始化log_prefix
|
||||
await self._initialize_log_prefix()
|
||||
|
||||
@@ -109,128 +113,84 @@ class RelationshipFetcher:
|
||||
self._cleanup_expired_cache()
|
||||
|
||||
person_info_manager = get_person_info_manager()
|
||||
|
||||
# 仅从 person_info 获取基础信息(不获取印象相关字段)
|
||||
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")
|
||||
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 = []
|
||||
|
||||
# 1. 基本信息
|
||||
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表获取完整关系信息(新系统)
|
||||
# 从 UserRelationships 表获取完整关系信息(这是唯一的印象数据来源)
|
||||
try:
|
||||
from src.common.database.api.specialized import get_user_relationship
|
||||
|
||||
# 查询用户关系数据
|
||||
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(带缓存)
|
||||
relationship = await get_user_relationship(
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
target_id="bot", # 或者根据实际需要传入目标用户ID
|
||||
)
|
||||
# 使用优化后的API(带缓存)- 只需要user_id
|
||||
relationship = await get_user_relationship(user_id=user_id)
|
||||
|
||||
if relationship:
|
||||
# 将SQLAlchemy对象转换为字典以保持兼容性
|
||||
# 直接使用 __dict__ 访问,避免触发 SQLAlchemy 的描述符和 lazy loading
|
||||
# 方案A已经确保所有字段在缓存前都已预加载,所以 __dict__ 中有完整数据
|
||||
# 将SQLAlchemy对象转换为字典
|
||||
try:
|
||||
rel_data = {
|
||||
"user_aliases": relationship.__dict__.get("user_aliases"),
|
||||
"relationship_text": relationship.__dict__.get("relationship_text"),
|
||||
"impression_text": relationship.__dict__.get("impression_text"),
|
||||
"preference_keywords": relationship.__dict__.get("preference_keywords"),
|
||||
"key_facts": relationship.__dict__.get("key_facts"),
|
||||
"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:
|
||||
logger.warning(f"访问relationship对象属性失败: {attr_error}")
|
||||
rel_data = {}
|
||||
|
||||
# 5.1 用户别名
|
||||
# 1. 用户别名
|
||||
if rel_data.get("user_aliases"):
|
||||
aliases_list = [alias.strip() for alias in rel_data["user_aliases"].split(",") if alias.strip()]
|
||||
if aliases_list:
|
||||
aliases_str = "、".join(aliases_list)
|
||||
relation_parts.append(f"{person_name}的别名有:{aliases_str}")
|
||||
|
||||
# 5.2 关系印象文本(主观认知)
|
||||
if rel_data.get("relationship_text"):
|
||||
relation_parts.append(f"你对{person_name}的整体认知:{rel_data['relationship_text']}")
|
||||
# 2. 关系阶段和好感度
|
||||
if rel_data.get("relationship_score") is not None:
|
||||
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"):
|
||||
keywords_list = [kw.strip() for kw in rel_data["preference_keywords"].split(",") if kw.strip()]
|
||||
if 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 关系亲密程度(好感分数)
|
||||
if rel_data.get("relationship_score") is not None:
|
||||
score_desc = self._get_relationship_score_description(rel_data["relationship_score"])
|
||||
relation_parts.append(f"你们的关系程度:{score_desc}({rel_data['relationship_score']:.2f})")
|
||||
# 6. 关键信息
|
||||
if rel_data.get("key_facts"):
|
||||
try:
|
||||
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:
|
||||
logger.error(f"查询UserRelationships表失败: {e}")
|
||||
@@ -383,6 +343,54 @@ class RelationshipFetcher:
|
||||
else:
|
||||
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):
|
||||
nickname_str = ",".join(global_config.bot.alias_names)
|
||||
name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"
|
||||
|
||||
@@ -5,6 +5,7 @@ AffinityFlow Chatter 工具模块
|
||||
"""
|
||||
|
||||
from .chat_stream_impression_tool import ChatStreamImpressionTool
|
||||
from .user_fact_tool import UserFactTool
|
||||
from .user_profile_tool import UserProfileTool
|
||||
|
||||
__all__ = ["ChatStreamImpressionTool", "UserProfileTool"]
|
||||
__all__ = ["ChatStreamImpressionTool", "UserProfileTool", "UserFactTool"]
|
||||
|
||||
@@ -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
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
采用两阶段设计:
|
||||
1. 工具调用模型(tool_use)负责判断是否需要更新,传入基本信息
|
||||
2. 关系追踪模型(relationship_tracker)负责生成高质量的、有人设特色的印象内容
|
||||
2. 关系追踪模型(relationship_tracker)负责:
|
||||
- 读取最近聊天记录
|
||||
- 生成高质量的、有人设特色的印象内容
|
||||
- 决定好感度变化(联动更新)
|
||||
"""
|
||||
|
||||
import time
|
||||
@@ -11,40 +14,61 @@ from typing import Any
|
||||
|
||||
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.core.models import UserRelationships
|
||||
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
|
||||
|
||||
# 默认好感度分数,用于配置未初始化时的回退
|
||||
DEFAULT_RELATIONSHIP_SCORE = 0.3
|
||||
|
||||
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):
|
||||
"""用户画像更新工具
|
||||
|
||||
两阶段设计:
|
||||
- 第一阶段:tool_use模型判断是否更新,传入简要信息
|
||||
- 第二阶段:relationship_tracker模型生成有人设特色的印象描述
|
||||
- 第二阶段:relationship_tracker模型读取聊天记录,生成印象并决定好感度变化
|
||||
"""
|
||||
|
||||
name = "update_user_profile"
|
||||
description = """当你通过聊天对某个人产生了新的认识或印象时使用此工具。
|
||||
调用时机:当你发现TA透露了新信息、展现了性格特点、表达了兴趣爱好,或你们的互动让你对TA有了新感受时。
|
||||
注意:impression_hint只需要简单描述你观察到的要点,系统会自动用你的人设风格来润色生成最终印象。"""
|
||||
description = """记录你对某个人的重要认识。【不要频繁调用】
|
||||
只在以下情况使用:
|
||||
1. TA首次告诉你【具体的个人信息】:生日日期、职业、所在城市、真实姓名等 → 必填 key_info_type 和 key_info_value
|
||||
2. 你对TA产生了【显著的、值得长期记住的】新印象(不是每次聊天都要记)
|
||||
3. 你们的关系有了【实质性变化】
|
||||
|
||||
【不要调用的情况】:
|
||||
- 普通的日常对话、闲聊
|
||||
- 只是聊得开心但没有实质性新认识
|
||||
- TA只是表达了一下情绪或感受
|
||||
- 你已经记录过类似的印象
|
||||
此工具会在后台异步执行,不会阻塞你的回复。"""
|
||||
parameters = [
|
||||
("target_user_id", ToolParamType.STRING, "目标用户的ID(必须)", True, None),
|
||||
("target_user_name", ToolParamType.STRING, "目标用户的名字/昵称(必须,用于生成印象时称呼)", True, 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),
|
||||
("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
|
||||
history_ttl = 5
|
||||
|
||||
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""执行用户画像更新
|
||||
"""执行用户画像更新(异步后台执行,不阻塞回复)
|
||||
|
||||
Args:
|
||||
function_args: 工具参数
|
||||
@@ -52,6 +76,8 @@ class UserProfileTool(BaseTool):
|
||||
Returns:
|
||||
dict: 执行结果
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
# 提取参数
|
||||
target_user_id = function_args.get("target_user_id")
|
||||
@@ -67,61 +93,33 @@ class UserProfileTool(BaseTool):
|
||||
new_aliases = function_args.get("user_aliases", "")
|
||||
impression_hint = function_args.get("impression_hint", "")
|
||||
new_keywords = function_args.get("preference_keywords", "")
|
||||
new_score = function_args.get("affection_score")
|
||||
|
||||
# 从数据库获取现有用户画像
|
||||
existing_profile = await self._get_user_profile(target_user_id)
|
||||
key_info_type = function_args.get("key_info_type", "")
|
||||
key_info_value = function_args.get("key_info_value", "")
|
||||
|
||||
# 如果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 {
|
||||
"type": "info",
|
||||
"id": target_user_id,
|
||||
"content": "提示:需要提供至少一项更新内容(别名、印象描述、偏好关键词或好感分数)"
|
||||
"content": "提示:需要提供至少一项更新内容(别名、印象描述、偏好关键词或重要信息)"
|
||||
}
|
||||
|
||||
# 🎯 核心:使用relationship_tracker模型生成高质量印象
|
||||
final_impression = existing_profile.get("relationship_text", "")
|
||||
if impression_hint:
|
||||
final_impression = await self._generate_impression_with_personality(
|
||||
# 🎯 异步后台执行,不阻塞回复
|
||||
asyncio.create_task(self._background_update(
|
||||
target_user_id=target_user_id,
|
||||
target_user_name=str(target_user_name) if target_user_name else str(target_user_id),
|
||||
impression_hint=str(impression_hint),
|
||||
existing_impression=str(existing_profile.get("relationship_text", "")),
|
||||
preference_keywords=str(new_keywords or existing_profile.get("preference_keywords", "")),
|
||||
)
|
||||
|
||||
# 构建最终画像
|
||||
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}")
|
||||
new_aliases=new_aliases,
|
||||
impression_hint=impression_hint,
|
||||
new_keywords=new_keywords,
|
||||
key_info_type=key_info_type,
|
||||
key_info_value=key_info_value,
|
||||
))
|
||||
|
||||
# 立即返回,让回复继续
|
||||
return {
|
||||
"type": "user_profile_update",
|
||||
"id": target_user_id,
|
||||
"content": result_text
|
||||
"content": f"正在后台更新对 {target_user_name} 的印象..."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -132,89 +130,374 @@ class UserProfileTool(BaseTool):
|
||||
"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,
|
||||
target_user_name: str,
|
||||
impression_hint: str,
|
||||
existing_impression: str,
|
||||
preference_keywords: str,
|
||||
) -> str:
|
||||
"""使用relationship_tracker模型生成有人设特色的印象描述
|
||||
chat_history: str,
|
||||
current_score: float,
|
||||
) -> dict[str, Any]:
|
||||
"""使用relationship_tracker模型生成印象并决定好感度变化
|
||||
|
||||
Args:
|
||||
target_user_name: 目标用户的名字
|
||||
impression_hint: 工具调用模型传入的简要观察
|
||||
existing_impression: 现有的印象描述
|
||||
preference_keywords: 用户的兴趣偏好
|
||||
chat_history: 最近的聊天记录
|
||||
current_score: 当前好感度分数
|
||||
|
||||
Returns:
|
||||
str: 生成的印象描述
|
||||
dict: {"impression": str, "affection_change": float}
|
||||
"""
|
||||
try:
|
||||
import orjson
|
||||
from json_repair import repair_json
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
|
||||
# 获取人设信息
|
||||
bot_name = global_config.bot.nickname
|
||||
personality_core = global_config.personality.personality_core
|
||||
personality_side = global_config.personality.personality_side
|
||||
# 获取人设信息(添加空值保护)
|
||||
bot_name = global_config.bot.nickname if global_config and global_config.bot else "Bot"
|
||||
personality_core = global_config.personality.personality_core if global_config and global_config.personality else ""
|
||||
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_side}
|
||||
|
||||
## 任务
|
||||
根据下面的观察要点,用你自己的语气和视角,写一段对"{target_user_name}"的印象描述。
|
||||
## 你的说话风格
|
||||
{reply_style}
|
||||
|
||||
## 观察到的要点
|
||||
{impression_hint}
|
||||
## 你之前对{target_user_name}的印象
|
||||
{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 "暂未了解"}
|
||||
|
||||
## 之前对TA的印象(如果有)
|
||||
{existing_impression if existing_impression else "这是第一次记录对TA的印象"}
|
||||
## 当前好感度
|
||||
{current_score:.2f} (范围0-1,0.3=普通认识,0.5=朋友,0.7=好友,0.9=挚友)
|
||||
|
||||
## 写作要求
|
||||
1. 用第一人称"我"来写,就像在写日记或者跟朋友聊天时描述一个人
|
||||
2. 用"{target_user_name}"或"TA"来称呼对方,不要用"该用户"、"此人"
|
||||
3. 写出你真实的、主观的感受,可以带情绪和直觉判断
|
||||
4. 如果有之前的印象,可以结合新观察进行补充或修正
|
||||
5. 长度控制在50-150字,自然流畅
|
||||
## 任务
|
||||
1. 根据聊天记录判断{target_user_name}的性别(男用"他",女用"她",无法判断用名字)
|
||||
2. {"写下你对这个人的第一印象" if is_first_impression else "在原有印象基础上,融入新的观察"}
|
||||
3. 决定好感度是否需要变化(大多数情况不需要)
|
||||
|
||||
请直接输出印象描述,不要加任何前缀或解释:"""
|
||||
## 印象写作要求
|
||||
- 用第一人称"我"来写
|
||||
- 根据判断的性别使用"他/她",或者直接用"{target_user_name}"
|
||||
- {"第一印象可以短一些,50-150字,写下初步感受" if is_first_impression else "在原有印象基础上补充新认识,150-300字"}
|
||||
- {"不要假装很熟,你们才刚认识" if is_first_impression else "体现出你们相处时间的积累"}
|
||||
- 写出这个人的特点、性格、给你的感觉
|
||||
|
||||
## 好感度变化规则(极度严格!)
|
||||
- 范围:-0.03 到 +0.03,**但默认是0,90%以上的对话好感度应该不变**
|
||||
- 好感度是长期积累的结果,不是短短几句话就能改变的
|
||||
- **绝对不变(=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(
|
||||
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(
|
||||
prompt=prompt,
|
||||
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)
|
||||
affection_change = max(-0.03, min(0.03, affection_change))
|
||||
|
||||
# 如果印象为空或太短,回退到hint
|
||||
if not impression or len(impression) < 10:
|
||||
logger.warning(f"印象生成结果过短,使用原始hint: {impression_hint}")
|
||||
return impression_hint
|
||||
logger.warning(f"印象生成结果过短,使用原始hint")
|
||||
impression = impression_hint or existing_impression
|
||||
|
||||
logger.info(f"成功生成有人设特色的印象描述,长度: {len(impression)}")
|
||||
return 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:
|
||||
logger.error(f"生成印象描述失败,回退到原始hint: {e}")
|
||||
# 失败时回退到工具调用模型传入的hint
|
||||
return impression_hint
|
||||
logger.error(f"生成印象和好感度失败: {e}")
|
||||
# 失败时回退
|
||||
return {
|
||||
"impression": impression_hint or existing_impression,
|
||||
"affection_change": 0.0
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if profile:
|
||||
# 优先使用新字段 impression_text,如果没有则用旧字段 relationship_text
|
||||
impression = profile.impression_text or profile.relationship_text or ""
|
||||
return {
|
||||
"user_name": profile.user_name or user_id,
|
||||
"user_aliases": profile.user_aliases or "",
|
||||
"relationship_text": profile.relationship_text or "",
|
||||
"relationship_text": impression, # 兼容旧代码
|
||||
"impression_text": impression,
|
||||
"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:
|
||||
# 用户不存在,返回默认值
|
||||
@@ -245,8 +534,12 @@ class UserProfileTool(BaseTool):
|
||||
"user_name": user_id,
|
||||
"user_aliases": "",
|
||||
"relationship_text": "",
|
||||
"impression_text": "",
|
||||
"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:
|
||||
logger.error(f"获取用户画像失败: {e}")
|
||||
@@ -254,8 +547,12 @@ class UserProfileTool(BaseTool):
|
||||
"user_name": user_id,
|
||||
"user_aliases": "",
|
||||
"relationship_text": "",
|
||||
"impression_text": "",
|
||||
"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)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
# 根据好感度自动计算关系阶段
|
||||
score = profile.get("relationship_score", 0.3)
|
||||
stage = self._calculate_relationship_stage(score)
|
||||
|
||||
if existing:
|
||||
# 更新现有记录
|
||||
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.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
|
||||
# 如果是首次认识,记录时间
|
||||
if not existing.first_met_time:
|
||||
existing.first_met_time = current_time
|
||||
else:
|
||||
# 创建新记录
|
||||
impression = profile.get("relationship_text", "")
|
||||
new_profile = UserRelationships(
|
||||
user_id=user_id,
|
||||
user_name=user_id,
|
||||
user_aliases=profile.get("user_aliases", ""),
|
||||
relationship_text=profile.get("relationship_text", ""),
|
||||
relationship_text=impression,
|
||||
impression_text=impression,
|
||||
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
|
||||
)
|
||||
session.add(new_profile)
|
||||
|
||||
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:
|
||||
logger.error(f"更新用户画像到数据库失败: {e}")
|
||||
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" # 陌生人
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user