mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-21 22:53:56 +08:00
feat(memory): 添加用户画像功能支持
- 新增 `enable_user_profile` 配置项,支持用户画像模式 - 实现 `UserMemory` 类集成,自动提取用户信息 - 更新文档说明用户画像功能及配置要求
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
## 功能特性
|
||||
|
||||
- **本地总结**:通过 LLM 在本地进行记忆总结和提取
|
||||
- **用户画像**:通过 `UserMemory` 自动提取用户信息(姓名、职业、兴趣等),持续更新用户画像
|
||||
- **智能遗忘**:基于艾宾浩斯遗忘曲线,自动"遗忘"过时噪声信息
|
||||
- **多种存储后端**:支持 OceanBase(推荐,最佳性能)、SeekDB(推荐,AI应用存储一体)、PostgreSQL、SQLite(轻量备选)
|
||||
- **多种 LLM 支持**:通义千问、智谱(glm-4-flash 免费)、OpenAI 等
|
||||
- **智能检索**:基于向量搜索的语义检索能力
|
||||
@@ -44,6 +46,8 @@ selected_module:
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
# 是否启用用户画像功能(需要OceanBase)
|
||||
enable_user_profile: false
|
||||
# 数据库提供者: oceanbase(推荐,最佳性能), seekdb, postgres, sqlite(轻量备选)
|
||||
database_provider: sqlite # 资源充足时建议使用 oceanbase 或 seekdb
|
||||
# LLM提供者: qwen(默认), openai, 等
|
||||
@@ -62,6 +66,7 @@ Memory:
|
||||
|
||||
| 参数 | 说明 | 默认值 | 可选值 |
|
||||
|------|------|--------|--------|
|
||||
| `enable_user_profile` | 启用用户画像模式 | `false` | `true`(需OceanBase), `false` |
|
||||
| `database_provider` | 存储后端类型 | `sqlite` | `oceanbase`(推荐), `seekdb`, `postgres`, `sqlite`(轻量) |
|
||||
| `llm_provider` | LLM 提供商 | `qwen` | `qwen`, `zhipu`(免费), `openai`, 等 |
|
||||
| `embedding_provider` | 嵌入模型提供商 | `qwen` | `qwen`, `zhipu`, `openai`, 等 |
|
||||
@@ -72,6 +77,15 @@ Memory:
|
||||
| `embedding_model` | 嵌入模型名称 | - | 根据提供商选择 |
|
||||
| `embedding_base_url` | 嵌入模型 API 地址(可选) | - | - |
|
||||
|
||||
### 记忆模式说明
|
||||
|
||||
PowerMem 支持两种记忆模式:
|
||||
|
||||
| 模式 | 配置 | 功能 | 存储要求 |
|
||||
|------|------|------|----------|
|
||||
| **普通记忆** | `enable_user_profile: false` | 对话记忆存储与检索 | 支持所有数据库 |
|
||||
| **用户画像** | `enable_user_profile: true` | 记忆 + 自动提取用户画像 | 仅支持 OceanBase |
|
||||
|
||||
### 使用通义千问(推荐)
|
||||
|
||||
1. 访问 [阿里云百炼平台](https://bailian.console.aliyun.com/) 注册账号
|
||||
@@ -198,6 +212,54 @@ PowerMem 会自动使用设备 ID(`device_id`)作为 `user_id` 进行记忆
|
||||
- 不同设备之间的记忆完全隔离
|
||||
- 同一设备的多次对话可以共享记忆上下文
|
||||
|
||||
## 用户画像(UserMemory)
|
||||
|
||||
PowerMem 提供 `UserMemory` 类,可自动从对话中提取用户画像信息。
|
||||
|
||||
### 启用用户画像
|
||||
|
||||
在配置中设置 `enable_user_profile: true` 即可启用:
|
||||
|
||||
```yaml
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
enable_user_profile: true # 启用用户画像
|
||||
database_provider: oceanbase # 必须使用 OceanBase
|
||||
llm_provider: qwen
|
||||
embedding_provider: qwen
|
||||
llm_api_key: sk-xxxxxxxxxxxxxxxx
|
||||
llm_model: qwen-plus
|
||||
embedding_api_key: sk-xxxxxxxxxxxxxxxx
|
||||
embedding_model: text-embedding-v3
|
||||
# OceanBase 数据库连接配置
|
||||
vector_store:
|
||||
provider: oceanbase
|
||||
config:
|
||||
host: 127.0.0.1
|
||||
port: 2881
|
||||
user: root@test
|
||||
password: your_password
|
||||
database: powermem
|
||||
```
|
||||
|
||||
### 用户画像能力
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **信息提取** | 自动从对话中提取姓名、年龄、职业、兴趣等 |
|
||||
| **持续更新** | 随着对话进行,不断完善用户画像 |
|
||||
| **画像检索** | 将用户画像与记忆搜索结合,提升检索相关性 |
|
||||
| **智能遗忘** | 基于艾宾浩斯遗忘曲线,淡化过时信息 |
|
||||
|
||||
### 工作原理
|
||||
|
||||
启用用户画像后,小智在查询记忆时会自动返回:
|
||||
1. **用户画像**:用户的基本信息、兴趣爱好等
|
||||
2. **相关记忆**:与当前对话相关的历史记忆
|
||||
|
||||
> ⚠️ **注意**:`UserMemory` 功能需要 OceanBase 作为存储后端,其他数据库暂不支持。
|
||||
|
||||
## 与其他记忆组件的对比
|
||||
|
||||
| 特性 | PowerMem | mem0ai | mem_local_short |
|
||||
@@ -206,6 +268,8 @@ PowerMem 会自动使用设备 ID(`device_id`)作为 `user_id` 进行记忆
|
||||
| 存储位置 | 本地/云端DB | 云端 | 本地YAML |
|
||||
| 费用 | 取决于LLM和DB | 1000次/月免费 | 完全免费 |
|
||||
| 智能检索 | ✅ 向量搜索 | ✅ 向量搜索 | ❌ 全量返回 |
|
||||
| 用户画像 | ✅ UserMemory | ❌ | ❌ |
|
||||
| 智能遗忘 | ✅ 遗忘曲线 | ❌ | ❌ |
|
||||
| 私有部署 | ✅ 支持 | ❌ 仅云端 | ✅ 支持 |
|
||||
| 数据库支持 | OceanBase(推荐)/SeekDB/PostgreSQL/SQLite | - | YAML 文件 |
|
||||
|
||||
@@ -239,6 +303,9 @@ source .venv/bin/activate
|
||||
|
||||
# 测试 PowerMem 导入
|
||||
python -c "from powermem import AsyncMemory; print('PowerMem 导入成功')"
|
||||
|
||||
# 测试 UserMemory 导入(用户画像功能)
|
||||
python -c "from powermem import UserMemory; print('UserMemory 导入成功')"
|
||||
```
|
||||
|
||||
## 更多资源
|
||||
|
||||
@@ -284,8 +284,13 @@ Memory:
|
||||
# 官网: https://www.powermem.ai/
|
||||
# 使用示例: https://github.com/oceanbase/powermem/tree/main/examples
|
||||
type: powermem
|
||||
# 是否启用用户画像功能(需要OceanBase作为存储后端)
|
||||
# - false: 使用普通记忆模式(AsyncMemory),支持所有数据库
|
||||
# - true: 使用用户画像模式(UserMemory),自动提取用户信息,仅支持OceanBase
|
||||
enable_user_profile: false
|
||||
# 数据库提供者: oceanbase(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选)
|
||||
# 资源充足时建议使用 oceanbase 或 seekdb
|
||||
# 注意:用户画像功能(enable_user_profile: true)仅支持oceanbase
|
||||
database_provider: sqlite
|
||||
# LLM提供者: qwen(默认), zhipu(免费glm-4-flash), openai, 等
|
||||
llm_provider: qwen
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
PowerMem is an open-source agent memory component from OceanBase
|
||||
GitHub: https://github.com/oceanbase/powermem
|
||||
Website: https://www.powermem.ai/
|
||||
@Author: wayyoungboy
|
||||
"""
|
||||
|
||||
import traceback
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from ..base import MemoryProviderBase, logger
|
||||
from powermem import AsyncMemory
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -27,19 +27,37 @@ class MemoryProvider(MemoryProviderBase):
|
||||
|
||||
Supports multiple storage backends (sqlite, oceanbase, postgres),
|
||||
LLM providers (qwen, openai, etc.) and embedding providers.
|
||||
|
||||
Config options:
|
||||
- enable_user_profile: bool - Enable UserMemory for user profiling (requires OceanBase)
|
||||
- database_provider: str - Storage backend (sqlite, oceanbase, postgres)
|
||||
- llm_provider: str - LLM provider (qwen, openai, etc.)
|
||||
- embedding_provider: str - Embedding provider (qwen, openai, etc.)
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any], summary_memory: Optional[str] = None):
|
||||
super().__init__(config)
|
||||
self.use_powermem = False
|
||||
self.memory_client = None
|
||||
self.enable_user_profile = False
|
||||
|
||||
try:
|
||||
try:
|
||||
# Check if user profile mode is enabled
|
||||
self.enable_user_profile = config.get("enable_user_profile", False)
|
||||
|
||||
# Get configuration parameters
|
||||
database_provider = config.get("database_provider", "sqlite")
|
||||
llm_provider = config.get("llm_provider", "qwen")
|
||||
embedding_provider = config.get("embedding_provider", "qwen")
|
||||
|
||||
# UserMemory requires OceanBase
|
||||
if self.enable_user_profile and database_provider not in ["oceanbase"]:
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"UserMemory requires OceanBase as storage backend, but got {database_provider}. "
|
||||
"Falling back to AsyncMemory mode."
|
||||
)
|
||||
self.enable_user_profile = False
|
||||
|
||||
# Build powermem configuration dict
|
||||
# PowerMem supports two config styles:
|
||||
# 1. powermem style: database, llm, embedding
|
||||
@@ -89,13 +107,21 @@ class MemoryProvider(MemoryProviderBase):
|
||||
"config": embedder_config
|
||||
}
|
||||
|
||||
# Initialize AsyncMemory client
|
||||
self.memory_client = AsyncMemory(config=powermem_config)
|
||||
# Initialize memory client based on mode
|
||||
if self.enable_user_profile:
|
||||
from powermem import UserMemory
|
||||
self.memory_client = UserMemory(config=powermem_config)
|
||||
memory_mode = "UserMemory (用户画像模式)"
|
||||
else:
|
||||
from powermem import AsyncMemory
|
||||
self.memory_client = AsyncMemory(config=powermem_config)
|
||||
memory_mode = "AsyncMemory (普通记忆模式)"
|
||||
|
||||
self.use_powermem = True
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
f"PowerMem initialized successfully with database={database_provider}, "
|
||||
f"llm={llm_provider}, embedding={embedding_provider}"
|
||||
f"PowerMem initialized successfully: mode={memory_mode}, "
|
||||
f"database={database_provider}, llm={llm_provider}, embedding={embedding_provider}"
|
||||
)
|
||||
|
||||
except ImportError as e:
|
||||
@@ -167,6 +193,14 @@ class MemoryProvider(MemoryProviderBase):
|
||||
logger.bind(tag=TAG).debug("No role_id set, returning empty memory")
|
||||
return ""
|
||||
|
||||
result_parts = []
|
||||
|
||||
# If user profile mode is enabled, include user profile in results
|
||||
if self.enable_user_profile:
|
||||
profile = await self.get_user_profile()
|
||||
if profile:
|
||||
result_parts.append(f"【用户画像】\n{profile}")
|
||||
|
||||
# Search memories using PowerMem SDK
|
||||
results = await self.memory_client.search(
|
||||
query=query,
|
||||
@@ -174,54 +208,92 @@ class MemoryProvider(MemoryProviderBase):
|
||||
limit=30
|
||||
)
|
||||
|
||||
if not results or "results" not in results:
|
||||
logger.bind(tag=TAG).debug("No memory results found")
|
||||
return ""
|
||||
|
||||
# Format each memory entry with its update time
|
||||
memories = []
|
||||
for entry in results.get("results", []):
|
||||
# Get timestamp from updated_at or created_at
|
||||
timestamp = ""
|
||||
if "updated_at" in entry and entry["updated_at"]:
|
||||
timestamp = str(entry["updated_at"])
|
||||
elif "created_at" in entry and entry["created_at"]:
|
||||
timestamp = str(entry["created_at"])
|
||||
|
||||
if timestamp:
|
||||
try:
|
||||
# Parse and reformat the timestamp (remove milliseconds if present)
|
||||
if "." in timestamp:
|
||||
dt = timestamp.split(".")[0]
|
||||
else:
|
||||
dt = timestamp
|
||||
formatted_time = dt.replace("T", " ")
|
||||
except Exception:
|
||||
formatted_time = timestamp
|
||||
else:
|
||||
formatted_time = ""
|
||||
|
||||
memory = entry.get("memory", "") or entry.get("content", "")
|
||||
if memory:
|
||||
if formatted_time:
|
||||
# Store tuple of (timestamp, formatted_string) for sorting
|
||||
memories.append((timestamp, f"[{formatted_time}] {memory}"))
|
||||
if results and "results" in results:
|
||||
# Format each memory entry with its update time
|
||||
memories = []
|
||||
for entry in results.get("results", []):
|
||||
# Get timestamp from updated_at or created_at
|
||||
timestamp = ""
|
||||
if "updated_at" in entry and entry["updated_at"]:
|
||||
timestamp = str(entry["updated_at"])
|
||||
elif "created_at" in entry and entry["created_at"]:
|
||||
timestamp = str(entry["created_at"])
|
||||
|
||||
if timestamp:
|
||||
try:
|
||||
# Parse and reformat the timestamp (remove milliseconds if present)
|
||||
if "." in timestamp:
|
||||
dt = timestamp.split(".")[0]
|
||||
else:
|
||||
dt = timestamp
|
||||
formatted_time = dt.replace("T", " ")
|
||||
except Exception:
|
||||
formatted_time = timestamp
|
||||
else:
|
||||
memories.append(("", memory))
|
||||
formatted_time = ""
|
||||
|
||||
memory = entry.get("memory", "") or entry.get("content", "")
|
||||
if memory:
|
||||
if formatted_time:
|
||||
# Store tuple of (timestamp, formatted_string) for sorting
|
||||
memories.append((timestamp, f"[{formatted_time}] {memory}"))
|
||||
else:
|
||||
memories.append(("", memory))
|
||||
|
||||
# Sort by timestamp in descending order (newest first)
|
||||
memories.sort(key=lambda x: x[0], reverse=True)
|
||||
# Sort by timestamp in descending order (newest first)
|
||||
memories.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
# Extract only the formatted strings
|
||||
memories_str = "\n".join(f"- {memory[1]}" for memory in memories)
|
||||
logger.bind(tag=TAG).debug(f"Query results: {memories_str}")
|
||||
return memories_str
|
||||
# Extract only the formatted strings
|
||||
if memories:
|
||||
memories_str = "\n".join(f"- {memory[1]}" for memory in memories)
|
||||
result_parts.append(f"【相关记忆】\n{memories_str}")
|
||||
|
||||
final_result = "\n\n".join(result_parts)
|
||||
logger.bind(tag=TAG).debug(f"Query results: {final_result}")
|
||||
return final_result
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error querying memory: {str(e)}")
|
||||
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
|
||||
return ""
|
||||
|
||||
async def get_user_profile(self) -> str:
|
||||
"""
|
||||
Get user profile from PowerMem (only available in UserMemory mode).
|
||||
|
||||
Returns:
|
||||
Formatted user profile string or empty string if not available
|
||||
"""
|
||||
if not self.use_powermem or self.memory_client is None:
|
||||
return ""
|
||||
|
||||
if not self.enable_user_profile:
|
||||
logger.bind(tag=TAG).debug("User profile mode is not enabled")
|
||||
return ""
|
||||
|
||||
try:
|
||||
if not getattr(self, "role_id", None):
|
||||
return ""
|
||||
|
||||
# Get user profile using UserMemory SDK
|
||||
profile = await self.memory_client.get_profile(user_id=self.role_id)
|
||||
|
||||
if not profile:
|
||||
return ""
|
||||
|
||||
# Format profile as readable string
|
||||
profile_parts = []
|
||||
for key, value in profile.items():
|
||||
if value:
|
||||
profile_parts.append(f"- {key}: {value}")
|
||||
|
||||
return "\n".join(profile_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error getting user profile: {str(e)}")
|
||||
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
|
||||
return ""
|
||||
|
||||
|
||||
# Register the memory provider instance
|
||||
powermem = MemoryProvider({})
|
||||
|
||||
Reference in New Issue
Block a user