Appearance
场景 3: 个性化对话助手
模块:Store(长期记忆)优先级:🟡 P2(中高)业务价值:跨会话记忆,提升用户粘性和体验
一、业务背景
1.1 当前痛点
当前项目每次会话都是独立的,无法记住用户的偏好:
具体痛点:
- 用户每次都要重新设置偏好(语言、风格等)
- AI 无法记住用户的专业背景、兴趣爱好
- 跨会话上下文丢失,体验不连贯
1.2 期望效果
二、记忆设计
2.1 记忆类型
2.2 数据结构
Store Namespace 结构:
├── (memories, user_123, preferences) # 用户偏好
│ ├── language: {"value": "中文", "confidence": 0.9}
│ ├── style: {"value": "简洁", "confidence": 0.8}
│ └── tone: {"value": "专业", "confidence": 0.7}
│
├── (memories, user_123, facts) # 用户事实
│ ├── fact_001: {"text": "用户是一名程序员", "source": "对话中提到"}
│ ├── fact_002: {"text": "用户喜欢玩游戏", "source": "明确说明"}
│ └── fact_003: {"text": "用户住在上海", "source": "对话推断"}
│
└── (memories, user_123, sessions) # 会话摘要
├── session_001: {"summary": "讨论了 Python 异步编程", "date": "2024-01-15"}
└── session_002: {"summary": "帮助调试了数据库问题", "date": "2024-01-20"}三、代码实现
3.1 记忆管理服务
创建文件: services/memory_store.py
python
"""用户记忆管理服务
基于 LangGraph Store 实现跨会话长期记忆。
"""
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
import logging
import uuid
import os
logger = logging.getLogger(__name__)
@dataclass
class MemoryItem:
"""记忆项"""
key: str
text: str
source: str # explicit, inferred, conversation
confidence: float
created_at: str
class MemoryService:
"""用户记忆管理服务"""
def __init__(self, store=None):
"""
初始化记忆服务
Args:
store: LangGraph Store 实例(可选,用于测试)
"""
self._store = store
self._memory_cache: Dict[str, Dict] = {} # 简单缓存
@property
def store(self):
"""获取 Store 实例"""
if self._store is None:
# 使用内存 Store(开发用)
from langgraph.store.memory import InMemoryStore
self._store = InMemoryStore()
return self._store
def _get_namespace(self, user_id: str, category: str) -> tuple:
"""获取命名空间"""
return ("memories", user_id, category)
# ============ 偏好管理 ============
def set_preference(
self,
user_id: str,
preference_type: str,
value: str,
confidence: float = 1.0
) -> None:
"""
设置用户偏好
Args:
user_id: 用户 ID
preference_type: 偏好类型(language, style, tone)
value: 偏好值
confidence: 置信度(0-1)
"""
namespace = self._get_namespace(user_id, "preferences")
self.store.put(
namespace,
preference_type,
{"value": value, "confidence": confidence}
)
# 清除缓存
self._memory_cache.pop(user_id, None)
logger.info(f"已存储用户偏好: {user_id} -> {preference_type} = {value}")
def get_preference(self, user_id: str, preference_type: str) -> Optional[str]:
"""获取用户偏好"""
namespace = self._get_namespace(user_id, "preferences")
try:
item = self.store.get(namespace, preference_type)
return item.value.get("value") if item else None
except Exception:
return None
def get_all_preferences(self, user_id: str) -> Dict[str, str]:
"""获取用户所有偏好"""
namespace = self._get_namespace(user_id, "preferences")
preferences = {}
try:
items = self.store.search(namespace, query="", limit=10)
for item in items:
preferences[item.key] = item.value.get("value")
except Exception as e:
logger.warning(f"获取偏好失败: {e}")
return preferences
# ============ 事实管理 ============
def store_fact(
self,
user_id: str,
text: str,
source: str = "explicit",
confidence: float = 0.8
) -> str:
"""
存储用户事实
Args:
user_id: 用户 ID
text: 事实内容
source: 来源(explicit=用户明确说明, inferred=推断, conversation=对话中提到)
confidence: 置信度
Returns:
事实 ID
"""
namespace = self._get_namespace(user_id, "facts")
fact_id = str(uuid.uuid4())[:8]
self.store.put(
namespace,
fact_id,
{
"text": text,
"source": source,
"confidence": confidence,
"created_at": datetime.datetime.now().isoformat()
}
)
# 清除缓存
self._memory_cache.pop(user_id, None)
logger.info(f"已存储用户事实: {user_id} -> {text[:50]}...")
return fact_id
def search_memories(
self,
user_id: str,
query: str,
limit: int = 5
) -> List[MemoryItem]:
"""
搜索相关记忆
Args:
user_id: 用户 ID
query: 搜索查询
limit: 返回数量
Returns:
相关记忆列表
"""
namespace = self._get_namespace(user_id, "facts")
memories = []
try:
items = self.store.search(namespace, query=query, limit=limit)
for item in items:
memories.append(MemoryItem(
key=item.key,
text=item.value.get("text", ""),
source=item.value.get("source", "unknown"),
confidence=item.value.get("confidence", 0.5),
created_at=item.value.get("created_at", "")
))
except Exception as e:
logger.warning(f"搜索记忆失败: {e}")
return memories
# ============ 记忆上下文构建 ============
def build_memory_context(
self,
user_id: str,
current_query: str
) -> str:
"""
构建记忆上下文
用于在系统提示中添加用户相关信息。
Args:
user_id: 用户 ID
current_query: 当前用户查询(用于搜索相关记忆)
Returns:
格式化的记忆上下文字符串
"""
# 检查缓存
cache_key = f"{user_id}:{hash(current_query)}"
if cache_key in self._memory_cache:
return self._memory_cache[cache_key]
context_parts = []
# 获取偏好
preferences = self.get_all_preferences(user_id)
if preferences:
pref_str = ", ".join(f"{k}: {v}" for k, v in preferences.items())
context_parts.append(f"用户偏好: {pref_str}")
# 搜索相关事实
relevant_facts = self.search_memories(user_id, current_query, limit=3)
if relevant_facts:
facts_str = "\n".join(f"- {f.text}" for f in relevant_facts)
context_parts.append(f"关于用户的信息:\n{facts_str}")
result = "\n\n".join(context_parts)
# 缓存结果
self._memory_cache[cache_key] = result
return result
# ============ 智能提取 ============
def extract_and_store_from_message(
self,
user_id: str,
user_message: str
) -> None:
"""
从用户消息中智能提取并存储信息
自动检测用户声明偏好和事实。
"""
message_lower = user_message.lower()
# 检测偏好声明
preference_patterns = {
"language": [
("我喜欢用中文", "中文"),
("请用中文", "中文"),
("用中文回复", "中文"),
("Speak English", "英文"),
("用英文", "英文"),
],
"style": [
("简洁一点", "简洁"),
("简单说", "简洁"),
("简短回答", "简洁"),
("详细一点", "详细"),
("详细解释", "详细"),
("展开说", "详细"),
],
"tone": [
("专业一点", "专业"),
("正式一点", "正式"),
("轻松一点", "轻松"),
("幽默一点", "幽默"),
]
}
for pref_type, patterns in preference_patterns.items():
for pattern, value in patterns:
if pattern.lower() in message_lower:
self.set_preference(user_id, pref_type, value)
logger.info(f"自动检测到偏好: {pref_type} = {value}")
return # 一次只处理一个
# 检测事实声明
fact_keywords = [
"我是", "我叫", "我住在", "我在", "我喜欢",
"我的工作是", "我是做", "我从事", "我的专业是"
]
for keyword in fact_keywords:
if keyword in user_message:
# 提取包含关键词的句子
import re
sentence = re.search(
rf"[^.。]*{re.escape(keyword)}[^.。]*",
user_message
)
if sentence:
self.store_fact(
user_id,
sentence.group(),
source="explicit"
)
return
# 全局实例
_memory_service: Optional[MemoryService] = None
def get_memory_service() -> MemoryService:
"""获取记忆服务单例"""
global _memory_service
if _memory_service is None:
_memory_service = MemoryService()
return _memory_service3.2 集成到聊天服务
修改 services/langgraph_chat.py:
python
# 在 call_model 节点中添加记忆支持
def call_model(state: MessagesState, user_id: Optional[str] = None):
# 构建记忆上下文
memory_context = ""
if user_id:
memory_service = get_memory_service()
query = str(state["messages"][-1].content) if state["messages"] else ""
memory_context = memory_service.build_memory_context(user_id, query)
# 尝试提取新记忆
last_user_msg = next(
(m for m in reversed(state["messages"]) if isinstance(m, HumanMessage)),
None
)
if last_user_msg:
memory_service.extract_and_store_from_message(user_id, last_user_msg.content)
# 构建系统提示
if memory_context:
system_prompt = f"""{base_system_prompt}
## 关于用户的信息
{memory_context}
请根据用户的偏好和背景提供个性化的回答。"""
else:
system_prompt = base_system_prompt
# ... 继续原有逻辑四、前端集成
4.1 偏好设置页面
javascript
// static/js/preferences.js
class UserPreferences {
constructor() {
this.preferences = {};
this.facts = [];
this.init();
}
async init() {
await this.loadPreferences();
await this.loadFacts();
this.render();
}
async loadPreferences() {
try {
const response = await fetch('/api/memory/preferences');
const data = await response.json();
if (data.success) {
this.preferences = data.preferences;
}
} catch (error) {
console.error('加载偏好失败:', error);
}
}
async loadFacts() {
try {
const response = await fetch('/api/memory/facts?limit=10');
const data = await response.json();
if (data.success) {
this.facts = data.facts;
}
} catch (error) {
console.error('加载事实失败:', error);
}
}
async setPreference(type, value) {
try {
const response = await fetch(`/api/memory/preferences/${type}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({value})
});
const data = await response.json();
if (data.success) {
this.preferences[type] = value;
this.showToast(`已保存偏好: ${this.getPreferenceLabel(type, value)}`);
}
} catch (error) {
console.error('保存偏好失败:', error);
}
}
async addFact(text) {
try {
const response = await fetch('/api/memory/facts', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text, source: 'manual'})
});
const data = await response.json();
if (data.success) {
this.facts.unshift(data.fact);
this.renderFacts();
this.showToast('已添加记忆');
}
} catch (error) {
console.error('添加事实失败:', error);
}
}
render() {
const container = document.getElementById('preferences-container');
if (!container) return;
container.innerHTML = `
<div class="preferences-section">
<h3>🎨 对话偏好</h3>
<div class="preference-item">
<label>回复语言</label>
<select id="pref-language" onchange="userPrefs.setPreference('language', this.value)">
<option value="">自动检测</option>
<option value="中文" ${this.preferences.language === '中文' ? 'selected' : ''}>中文</option>
<option value="英文" ${this.preferences.language === '英文' ? 'selected' : ''}>英文</option>
<option value="日文" ${this.preferences.language === '日文' ? 'selected' : ''}>日文</option>
</select>
</div>
<div class="preference-item">
<label>回复风格</label>
<select id="pref-style" onchange="userPrefs.setPreference('style', this.value)">
<option value="">默认</option>
<option value="简洁" ${this.preferences.style === '简洁' ? 'selected' : ''}>简洁</option>
<option value="详细" ${this.preferences.style === '详细' ? 'selected' : ''}>详细</option>
</select>
</div>
<div class="preference-item">
<label>语气风格</label>
<select id="pref-tone" onchange="userPrefs.setPreference('tone', this.value)">
<option value="">默认</option>
<option value="专业" ${this.preferences.tone === '专业' ? 'selected' : ''}>专业</option>
<option value="轻松" ${this.preferences.tone === '轻松' ? 'selected' : ''}>轻松</option>
<option value="幽默" ${this.preferences.tone === '幽默' ? 'selected' : ''}>幽默</option>
</select>
</div>
</div>
<div class="facts-section">
<h3>🧠 关于我</h3>
<p class="hint">AI 会记住这些信息,提供个性化回答</p>
<div class="add-fact-form">
<input type="text" id="new-fact-input" placeholder="添加一条关于你的信息...">
<button onclick="userPrefs.addFact(document.getElementById('new-fact-input').value)">添加</button>
</div>
<div id="facts-list"></div>
</div>
`;
this.renderFacts();
}
renderFacts() {
const container = document.getElementById('facts-list');
if (!container) return;
container.innerHTML = this.facts.map(fact => `
<div class="fact-item" data-id="${fact.key}">
<span class="fact-text">${fact.text}</span>
<span class="fact-source">${this.getSourceLabel(fact.source)}</span>
<button class="delete-fact" onclick="userPrefs.deleteFact('${fact.key}')">×</button>
</div>
`).join('');
}
getSourceLabel(source) {
const labels = {
'explicit': '用户说明',
'inferred': 'AI 推断',
'manual': '手动添加'
};
return labels[source] || source;
}
getPreferenceLabel(type, value) {
const labels = {
'language': `语言: ${value}`,
'style': `风格: ${value}`,
'tone': `语气: ${value}`
};
return labels[type] || `${type}: ${value}`;
}
showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast success';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
}
const userPrefs = new UserPreferences();五、用户交互流程
六、预期收益
6.1 用户体验提升
| 场景 | 改进前 | 改进后 |
|---|---|---|
| 语言偏好 | 每次都要设置 | 自动应用 |
| 回复风格 | 需要反复强调 | 自动适配 |
| 专业背景 | 每次对话需要说明 | 自动识别 |
| 个性化推荐 | 通用推荐 | 基于兴趣推荐 |
6.2 业务价值
七、实施计划
| 步骤 | 任务 | 预估时间 |
|---|---|---|
| 1 | 创建 services/memory_store.py | 3h |
| 2 | 集成到 services/langgraph_chat.py | 2h |
| 3 | 添加 API 端点 | 1h |
| 4 | 前端偏好设置页面 | 3h |
| 5 | 测试和优化 | 1h |
| 总计 | 10h (1.5天) |