feat(memory): 添加用户画像功能支持

- 新增 `enable_user_profile` 配置项,支持用户画像模式
- 实现 `UserMemory` 类集成,自动提取用户信息
- 更新文档说明用户画像功能及配置要求
This commit is contained in:
渠磊
2026-01-08 14:43:48 +08:00
parent dadf05ab4e
commit de7aedab96
3 changed files with 189 additions and 45 deletions
+67
View File
@@ -17,6 +17,8 @@
## 功能特性 ## 功能特性
- **本地总结**:通过 LLM 在本地进行记忆总结和提取 - **本地总结**:通过 LLM 在本地进行记忆总结和提取
- **用户画像**:通过 `UserMemory` 自动提取用户信息(姓名、职业、兴趣等),持续更新用户画像
- **智能遗忘**:基于艾宾浩斯遗忘曲线,自动"遗忘"过时噪声信息
- **多种存储后端**:支持 OceanBase(推荐,最佳性能)、SeekDB(推荐,AI应用存储一体)、PostgreSQL、SQLite(轻量备选) - **多种存储后端**:支持 OceanBase(推荐,最佳性能)、SeekDB(推荐,AI应用存储一体)、PostgreSQL、SQLite(轻量备选)
- **多种 LLM 支持**:通义千问、智谱(glm-4-flash 免费)、OpenAI 等 - **多种 LLM 支持**:通义千问、智谱(glm-4-flash 免费)、OpenAI 等
- **智能检索**:基于向量搜索的语义检索能力 - **智能检索**:基于向量搜索的语义检索能力
@@ -44,6 +46,8 @@ selected_module:
Memory: Memory:
powermem: powermem:
type: powermem type: powermem
# 是否启用用户画像功能(需要OceanBase)
enable_user_profile: false
# 数据库提供者: oceanbase(推荐,最佳性能), seekdb, postgres, sqlite(轻量备选) # 数据库提供者: oceanbase(推荐,最佳性能), seekdb, postgres, sqlite(轻量备选)
database_provider: sqlite # 资源充足时建议使用 oceanbase 或 seekdb database_provider: sqlite # 资源充足时建议使用 oceanbase 或 seekdb
# LLM提供者: qwen(默认), openai, 等 # LLM提供者: qwen(默认), openai, 等
@@ -62,6 +66,7 @@ Memory:
| 参数 | 说明 | 默认值 | 可选值 | | 参数 | 说明 | 默认值 | 可选值 |
|------|------|--------|--------| |------|------|--------|--------|
| `enable_user_profile` | 启用用户画像模式 | `false` | `true`(需OceanBase), `false` |
| `database_provider` | 存储后端类型 | `sqlite` | `oceanbase`(推荐), `seekdb`, `postgres`, `sqlite`(轻量) | | `database_provider` | 存储后端类型 | `sqlite` | `oceanbase`(推荐), `seekdb`, `postgres`, `sqlite`(轻量) |
| `llm_provider` | LLM 提供商 | `qwen` | `qwen`, `zhipu`(免费), `openai`, 等 | | `llm_provider` | LLM 提供商 | `qwen` | `qwen`, `zhipu`(免费), `openai`, 等 |
| `embedding_provider` | 嵌入模型提供商 | `qwen` | `qwen`, `zhipu`, `openai`, 等 | | `embedding_provider` | 嵌入模型提供商 | `qwen` | `qwen`, `zhipu`, `openai`, 等 |
@@ -72,6 +77,15 @@ Memory:
| `embedding_model` | 嵌入模型名称 | - | 根据提供商选择 | | `embedding_model` | 嵌入模型名称 | - | 根据提供商选择 |
| `embedding_base_url` | 嵌入模型 API 地址(可选) | - | - | | `embedding_base_url` | 嵌入模型 API 地址(可选) | - | - |
### 记忆模式说明
PowerMem 支持两种记忆模式:
| 模式 | 配置 | 功能 | 存储要求 |
|------|------|------|----------|
| **普通记忆** | `enable_user_profile: false` | 对话记忆存储与检索 | 支持所有数据库 |
| **用户画像** | `enable_user_profile: true` | 记忆 + 自动提取用户画像 | 仅支持 OceanBase |
### 使用通义千问(推荐) ### 使用通义千问(推荐)
1. 访问 [阿里云百炼平台](https://bailian.console.aliyun.com/) 注册账号 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 | | 特性 | PowerMem | mem0ai | mem_local_short |
@@ -206,6 +268,8 @@ PowerMem 会自动使用设备 ID`device_id`)作为 `user_id` 进行记忆
| 存储位置 | 本地/云端DB | 云端 | 本地YAML | | 存储位置 | 本地/云端DB | 云端 | 本地YAML |
| 费用 | 取决于LLM和DB | 1000次/月免费 | 完全免费 | | 费用 | 取决于LLM和DB | 1000次/月免费 | 完全免费 |
| 智能检索 | ✅ 向量搜索 | ✅ 向量搜索 | ❌ 全量返回 | | 智能检索 | ✅ 向量搜索 | ✅ 向量搜索 | ❌ 全量返回 |
| 用户画像 | ✅ UserMemory | ❌ | ❌ |
| 智能遗忘 | ✅ 遗忘曲线 | ❌ | ❌ |
| 私有部署 | ✅ 支持 | ❌ 仅云端 | ✅ 支持 | | 私有部署 | ✅ 支持 | ❌ 仅云端 | ✅ 支持 |
| 数据库支持 | OceanBase(推荐)/SeekDB/PostgreSQL/SQLite | - | YAML 文件 | | 数据库支持 | OceanBase(推荐)/SeekDB/PostgreSQL/SQLite | - | YAML 文件 |
@@ -239,6 +303,9 @@ source .venv/bin/activate
# 测试 PowerMem 导入 # 测试 PowerMem 导入
python -c "from powermem import AsyncMemory; print('PowerMem 导入成功')" python -c "from powermem import AsyncMemory; print('PowerMem 导入成功')"
# 测试 UserMemory 导入(用户画像功能)
python -c "from powermem import UserMemory; print('UserMemory 导入成功')"
``` ```
## 更多资源 ## 更多资源
+5
View File
@@ -284,8 +284,13 @@ Memory:
# 官网: https://www.powermem.ai/ # 官网: https://www.powermem.ai/
# 使用示例: https://github.com/oceanbase/powermem/tree/main/examples # 使用示例: https://github.com/oceanbase/powermem/tree/main/examples
type: powermem type: powermem
# 是否启用用户画像功能(需要OceanBase作为存储后端)
# - false: 使用普通记忆模式(AsyncMemory),支持所有数据库
# - true: 使用用户画像模式(UserMemory),自动提取用户信息,仅支持OceanBase
enable_user_profile: false
# 数据库提供者: oceanbase(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选) # 数据库提供者: oceanbase(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选)
# 资源充足时建议使用 oceanbase 或 seekdb # 资源充足时建议使用 oceanbase 或 seekdb
# 注意:用户画像功能(enable_user_profile: true)仅支持oceanbase
database_provider: sqlite database_provider: sqlite
# LLM提供者: qwen(默认), zhipu(免费glm-4-flash), openai, 等 # LLM提供者: qwen(默认), zhipu(免费glm-4-flash), openai, 等
llm_provider: qwen llm_provider: qwen
@@ -7,13 +7,13 @@
PowerMem is an open-source agent memory component from OceanBase PowerMem is an open-source agent memory component from OceanBase
GitHub: https://github.com/oceanbase/powermem GitHub: https://github.com/oceanbase/powermem
Website: https://www.powermem.ai/ Website: https://www.powermem.ai/
@Author: wayyoungboy
""" """
import traceback import traceback
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from ..base import MemoryProviderBase, logger from ..base import MemoryProviderBase, logger
from powermem import AsyncMemory
TAG = __name__ TAG = __name__
@@ -27,19 +27,37 @@ class MemoryProvider(MemoryProviderBase):
Supports multiple storage backends (sqlite, oceanbase, postgres), Supports multiple storage backends (sqlite, oceanbase, postgres),
LLM providers (qwen, openai, etc.) and embedding providers. 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): def __init__(self, config: Dict[str, Any], summary_memory: Optional[str] = None):
super().__init__(config) super().__init__(config)
self.use_powermem = False self.use_powermem = False
self.memory_client = None 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 # Get configuration parameters
database_provider = config.get("database_provider", "sqlite") database_provider = config.get("database_provider", "sqlite")
llm_provider = config.get("llm_provider", "qwen") llm_provider = config.get("llm_provider", "qwen")
embedding_provider = config.get("embedding_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 # Build powermem configuration dict
# PowerMem supports two config styles: # PowerMem supports two config styles:
# 1. powermem style: database, llm, embedding # 1. powermem style: database, llm, embedding
@@ -89,13 +107,21 @@ class MemoryProvider(MemoryProviderBase):
"config": embedder_config "config": embedder_config
} }
# Initialize AsyncMemory client # Initialize memory client based on mode
self.memory_client = AsyncMemory(config=powermem_config) 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 self.use_powermem = True
logger.bind(tag=TAG).info( logger.bind(tag=TAG).info(
f"PowerMem initialized successfully with database={database_provider}, " f"PowerMem initialized successfully: mode={memory_mode}, "
f"llm={llm_provider}, embedding={embedding_provider}" f"database={database_provider}, llm={llm_provider}, embedding={embedding_provider}"
) )
except ImportError as e: except ImportError as e:
@@ -167,6 +193,14 @@ class MemoryProvider(MemoryProviderBase):
logger.bind(tag=TAG).debug("No role_id set, returning empty memory") logger.bind(tag=TAG).debug("No role_id set, returning empty memory")
return "" 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 # Search memories using PowerMem SDK
results = await self.memory_client.search( results = await self.memory_client.search(
query=query, query=query,
@@ -174,54 +208,92 @@ class MemoryProvider(MemoryProviderBase):
limit=30 limit=30
) )
if not results or "results" not in results: if results and "results" in results:
logger.bind(tag=TAG).debug("No memory results found") # Format each memory entry with its update time
return "" 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"])
# Format each memory entry with its update time if timestamp:
memories = [] try:
for entry in results.get("results", []): # Parse and reformat the timestamp (remove milliseconds if present)
# Get timestamp from updated_at or created_at if "." in timestamp:
timestamp = "" dt = timestamp.split(".")[0]
if "updated_at" in entry and entry["updated_at"]: else:
timestamp = str(entry["updated_at"]) dt = timestamp
elif "created_at" in entry and entry["created_at"]: formatted_time = dt.replace("T", " ")
timestamp = str(entry["created_at"]) except Exception:
formatted_time = timestamp
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}"))
else: else:
memories.append(("", memory)) formatted_time = ""
# Sort by timestamp in descending order (newest first) memory = entry.get("memory", "") or entry.get("content", "")
memories.sort(key=lambda x: x[0], reverse=True) if memory:
if formatted_time:
# Store tuple of (timestamp, formatted_string) for sorting
memories.append((timestamp, f"[{formatted_time}] {memory}"))
else:
memories.append(("", memory))
# Extract only the formatted strings # Sort by timestamp in descending order (newest first)
memories_str = "\n".join(f"- {memory[1]}" for memory in memories) memories.sort(key=lambda x: x[0], reverse=True)
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: except Exception as e:
logger.bind(tag=TAG).error(f"Error querying memory: {str(e)}") logger.bind(tag=TAG).error(f"Error querying memory: {str(e)}")
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}") logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
return "" 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 # Register the memory provider instance
powermem = MemoryProvider({}) powermem = MemoryProvider({})