RAG记忆增强系统
版本:v1.0
日期:2025-12-04
目标:为LLM构建具备时间感知和上下文理解的持久化记忆模块
1. 项目概述
1.1 核心目标
构建一个基于向量检索(RAG)的对话记忆系统,使LLM能够:
- 记住多轮对话历史
- 基于语义相似性检索相关记忆
- 保持对话上下文的时空连贯性
- 理解时间敏感信息("明天"、"刚才"等)
1.2 关键场景
- QQ机器人:区分私聊和群聊,记住每个用户的历史对话
- 智能客服:记住用户偏好和历史问题
- 个人助理:跟踪任务进度和时间安排
- 群聊助手:理解群组上下文,避免跨群记忆污染
2. 功能需求
2.1 核心记忆存储
| 功能点 | 详细描述 | 优先级 |
|---|---|---|
| 对话持久化 | 存储用户/助手的每轮对话,包含角色、内容、时间戳 | P0 |
| 向量生成 | 调用外部Embedding API(SiliconFlow)将文本转为向量 | P0 |
| 会话隔离 | 基于user_id和group_id区分私聊/群聊记忆数据 | P0 |
| QQ元数据 | 存储QQ号、群号、昵称、消息ID等元数据 | P0 |
| 批量写入 | 支持批量插入历史对话,初始化时性能优化 | P1 |
技术参数:
- Embedding模型:
Qwen/Qwen3-Embedding-0.6B(1024维向量) - API端点:
https://api.siliconflow.cn/v1/embeddings - 单条向量大小:约4KB
2.2 智能检索
| 功能点 | 详细描述 | 优先级 |
|---|---|---|
| 语义搜索 | 基于余弦相似度检索Top-N相关对话 | P0 |
| 上下文窗口 | 每个锚点对话自动扩展前后各X条,保持对话连贯性 | P0 |
| 时间衰减 | 7天以上的记忆自动降低权重或过滤 | P1 |
| 混合过滤 | 支持关键词+向量混合检索(如只搜"用户"角色) | P2 |
检索逻辑:
- 根据
user_id和group_id确定查询范围 - 将查询文本向量化(调用Embedding API)
- 在向量数据库中查找Top-N相似度锚点
- 私聊:只检索
user_id=X AND group_id IS NULL的对话 - 群聊:只检索
user_id=X AND group_id=Y的对话
- 私聊:只检索
- 为每个锚点扩展前后
window_size条对话(保持上下文连贯) - 去重后按
created_at时间排序返回
2.3 时间感知增强
| 功能点 | 详细描述 | 优先级 |
|---|---|---|
| 绝对时间记录 | 精确到秒的创建时间(YYYY-MM-DD HH:MM:SS) | P0 |
| 相对时间计算 | 动态生成"3小时前"、"2天前"等人类可读格式 | P0 |
| 时间提示注入 | 在prompt中注入当前时间,帮助AI理解"明天"等词 | P0 |
| 时区支持 | 支持配置时区(默认Asia/Shanghai) | P1 |
时间格式化示例:
[2024-01-16 15:00:00] (1小时前) user: 我的订单号是多少?
2.4 Prompt构建规范
- 记忆格式:
[绝对时间] (相对时间) role: content - 长度控制:记忆总token数不超过1000(可配置)
- System Prompt:必须包含当前时间,并明确告知AI:
- 时间久远的信息可能已过时
- 涉及时间敏感词请基于当前时间理解
- 指代词需结合时间戳判断
- 对话间隔超过1小时可主动提供上下文回顾
3. 非功能需求
3.1 性能指标
- 向量检索:单条查询<100ms(10万条数据量)
- API调用:Embedding接口RT<500ms
- 写入吞吐:>100条/秒(批量模式)
- 内存占用:单连接<50MB
3.2 成本约束
- Embedding成本:每百万token < 20元
- 存储成本:单条记忆(含向量)< 5KB
- 缓存策略:重复文本命中LRU缓存,减少API调用
3.3 可扩展性
- 数据量:支持平滑扩展到100万条对话
- 分布式:未来可迁移到Milvus/Pinecone
- 模型切换:支持更换Embedding模型(如bge-m3)
4. 技术架构
4.1 组件图
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 用户输入 │────▶│ TemporalMemory│────▶│ SiliconFlow│
└─────────────┘ │ 核心类 │ │ Embedding API│
└──────┬───────┘ └─────────────┘
│
┌─────────────┐ │ 向量检索
│ LLM API │◀───────────┼─────────────┐
└─────────────┘ │ │
▼ ▼
┌─────────────────────────┐
│ PostgreSQL + pgvector │
│ (向量+时间混合存储) │
└─────────────────────────┘
4.2 核心类设计
class TemporalMemory:
def __init__(self, db_config: Dict, embedding_config: Dict):
"""初始化记忆模块"""
def _get_embedding(self, text: str) -> List[float]:
"""调用SiliconFlow Embedding API"""
def add_dialogue(
self,
user_id: int,
role: str,
content: str,
group_id: Optional[int] = None,
sender_name: Optional[str] = None,
message_id: Optional[int] = None
) -> None:
"""存储单条对话(自动生成向量)"""
def get_contextual_memory(
self,
user_id: int,
query: str,
group_id: Optional[int] = None,
top_n: int = 3,
window_size: int = 2
) -> List[Dict]:
"""检索语义相关的记忆(锚点+上下文窗口)"""
def chat_with_temporal_memory(
self,
user_id: int,
user_input: str,
group_id: Optional[int] = None,
llm_callback: Callable = None,
max_memory_tokens: int = 1000
) -> str:
"""完整业务流程:检索记忆 -> 构建Prompt -> 调用LLM"""
@staticmethod
def generate_session_key(user_id: int, group_id: Optional[int] = None) -> str:
"""生成会话标识(私聊/群聊)"""
5. 数据模型
5.1 表结构(PostgreSQL)
CREATE TABLE dialogues (
id SERIAL PRIMARY KEY, -- 主键(时间序)
user_id BIGINT NOT NULL, -- QQ号(用户ID)
group_id BIGINT, -- 群号(NULL表示私聊)
chat_type TEXT CHECK (chat_type IN ('private', 'group')), -- 聊天类型
role TEXT CHECK (role IN ('user', 'assistant')), -- 角色
content TEXT, -- 对话内容
sender_name TEXT, -- 发送者昵称(可选)
message_id BIGINT, -- QQ消息ID(可选,用于追溯)
embedding VECTOR(1024), -- 向量(Qwen3-Embedding-0.6B)
token_count INTEGER, -- Token数(用于裁剪)
created_at TIMESTAMP DEFAULT NOW(), -- 创建时间
created_date DATE GENERATED ALWAYS AS (created_at::date) STORED -- 日期分区
);
-- 索引
-- 1. 向量检索索引(按群聊/私聊分组)
CREATE INDEX idx_group_embedding ON dialogues
USING ivfflat (embedding vector_cosine_ops)
WHERE group_id IS NOT NULL
WITH (lists=100);
CREATE INDEX idx_private_embedding ON dialogues
USING ivfflat (embedding vector_cosine_ops)
WHERE group_id IS NULL
WITH (lists=100);
-- 2. 时间序列查询索引(群聊场景)
CREATE INDEX idx_group_time ON dialogues (group_id, user_id, id DESC)
WHERE group_id IS NOT NULL;
-- 3. 时间序列查询索引(私聊场景)
CREATE INDEX idx_private_time ON dialogues (user_id, id DESC)
WHERE group_id IS NULL;
-- 4. 复合查询索引(用于上下文窗口扩展)
CREATE INDEX idx_chat_context ON dialogues (chat_type, user_id, group_id, created_at DESC);
5.2 会话标识(Session Key)规则
- 私聊:
user_id= QQ号- 示例:
123456789表示与用户123456789的私聊
- 示例:
- 群聊:
group_id:user_id- 示例:
987654321:123456789表示群987654321中用户123456789的对话
- 示例:
5.3 数据示例
-- 私聊示例
INSERT INTO dialogues (user_id, group_id, chat_type, role, content, sender_name, embedding)
VALUES (123456789, NULL, 'private', 'user', '今天天气怎么样', '小明', [...]);
-- 群聊示例
INSERT INTO dialogues (user_id, group_id, chat_type, role, content, sender_name, message_id, embedding)
VALUES (123456789, 987654321, 'group', 'user', '大家好', '小明', 1001, [...]);
-- AI回复(群聊)
INSERT INTO dialogues (user_id, group_id, chat_type, role, content, sender_name, embedding)
VALUES (123456789, 987654321, 'group', 'assistant', '你好!', '小诗', [...]);
5.4 向量维度说明
- 模型:
Qwen/Qwen3-Embedding-0.6B - 维度:1024维
- 精度:float32(4字节/维度)
- 单条向量大小:4KB
5.5 索引设计说明
- 为什么分离群聊/私聊索引?
- 群聊和私聊的查询模式不同,分离索引可以提高检索效率
- 私聊记忆只检索与特定用户的对话
- 群聊记忆需要区分群组和用户,避免跨群污染
- IVFFLAT参数:
lists=100适用于10万条数据,建议按数据量/1000调整
6. API接口定义
6.1 Embedding接口(外部)
POST https://api.siliconflow.cn/v1/embeddings
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Body:
{
"model": "Qwen/Qwen3-Embedding-0.6B",
"input": "文本内容"
}
Response:
{
"data": [{"embedding": [0.123, -0.456, ...]}],
"usage": {"total_tokens": 15}
}
6.2 记忆模块内部接口
# 存储对话(支持QQ机器人场景)
add_dialogue(
user_id: int, # QQ号
role: str, # 'user' 或 'assistant'
content: str, # 消息内容
group_id: Optional[int] = None, # 群号(None表示私聊)
sender_name: Optional[str] = None, # 发送者昵称
message_id: Optional[int] = None # QQ消息ID
) -> None
# 检索记忆(返回带时间戳的对话列表)
get_contextual_memory(
user_id: int, # QQ号
query: str, # 查询文本
group_id: Optional[int] = None, # 群号(None表示私聊)
top_n: int = 3, # 向量检索锚点数量
window_size: int = 2 # 上下文窗口大小
) -> List[Dict]
# 完整聊天流程
chat_with_temporal_memory(
user_id: int, # QQ号
user_input: str, # 用户输入
group_id: Optional[int] = None, # 群号
llm_callback: Callable, # LLM回调函数
max_memory_tokens: int = 1000 # 记忆最大token数
) -> str
# 生成会话标识(工具方法)
def generate_session_key(user_id: int, group_id: Optional[int] = None) -> str:
"""
私聊:返回 "{user_id}"
群聊:返回 "{group_id}:{user_id}"
"""
return f"{group_id}:{user_id}" if group_id else str(user_id)
7. 记忆评估系统(Memory Evaluation)
7.1 设计目标
传统RAG系统会将所有对话无差别地存入长期记忆,导致:
- 存储浪费:日常寒暄、无意义对话占用大量空间
- 检索噪音:低价值记忆干扰有价值内容的检索
- 成本上升:无效对话的向量化和存储增加成本
记忆评估系统通过LLM智能评估对话价值,只保存有意义的对话到长期记忆。
7.2 核心功能
| 功能点 | 描述 | 优先级 |
|---|---|---|
| 对话价值评估 | 使用LLM评估用户-AI完整对话回合的记忆价值 | P0 |
| 智能保留策略 | 根据评分设置不同过期时间(1天/1周/1个月/永久) | P0 |
| 无缝集成 | 自动集成到chat流程,无需手动调用 | P0 |
| 降级保护 | 评估失败时自动降级为全量保存策略 | P1 |
7.3 评分标准(0-100分)
| 分数区间 | 保留时长 | 典型场景 | 示例对话 |
|---|---|---|---|
| 91-100 | 永久保留 | 核心知识、解决方案、关键信息 | 用户:"如何配置SSL?" AI:"需要生成证书,配置nginx..." |
| 71-90 | 保留1个月 | 具体需求、技术细节、明确结论 | 用户:"Docker端口被占用" AI:"运行lsof -i找到进程..." |
| 41-70 | 保留1周 | 一般交流、偏好确认、信息补充 | 用户:"推荐编辑器" AI:"VS Code比较好用" |
| 21-40 | 保留1天 | 礼貌问答、简单确认、无实质信息 | 用户:"谢谢" AI:"不客气" |
| 0-20 | 不保存 | 完全无关、测试对话、无意义内容 | 用户:"测试" AI:"你好" |
7.4 评估流程
┌──────────────┐
│ 用户发送消息 │
└──────┬───────┘
│
▼
┌──────────────────┐
│ 存入短期记忆 │
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 检索长期记忆 │
│ 构建Prompt │
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 请求LLM获取回复 │
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 存入短期记忆 │
└──────┬───────────┘
│
▼
┌────────────────────────┐
│ 记忆评估器评估对话价值 │
│ (user+assistant完整回合)│
└──────┬─────────────────┘
│
▼
评分 > 20?
│
┌───┴───┐
是│ │否
│ │
▼ ▼
┌──────┐ ┌────────┐
│存入长│ │跳过存储│
│期记忆│ │ │
│带过期│ └────────┘
│时间 │
└──────┘
7.5 技术实现
7.5.1 评估器初始化
// 在ChatService初始化时自动创建
let memory_evaluator = if config.memory.rag.enabled
&& config.memory.rag.memory_evaluation.enabled {
match MemoryEvaluator::new(config.memory.rag.memory_evaluation.clone()) {
Ok(evaluator) => {
kovi::log::info!("✅ 记忆评估系统已启用");
Some(Arc::new(evaluator))
}
Err(e) => {
kovi::log::error!("❌ 记忆评估器初始化失败: {}", e);
None
}
}
} else {
None
};
7.5.2 评估接口
// 评估单轮对话价值,返回评分
async fn evaluate(
&self,
user_message: &str,
assistant_message: &str
) -> Result<i32>
// 评估并决定保留时长,返回(评分, 保留时长, 过期时间)
async fn evaluate_and_decide(
&self,
user_message: &str,
assistant_message: &str,
) -> Result<(i32, RetentionDuration, Option<DateTime<Utc>>)>
7.5.3 保留时长枚举
pub enum RetentionDuration {
None, // 不保存(立即过期)
OneDay, // 保留1天
OneWeek, // 保留1周
OneMonth, // 保留1个月
Forever, // 永久保留(expires_at = NULL)
}
7.5.4 数据库存储
-- dialogues表新增字段
ALTER TABLE dialogues ADD COLUMN score INTEGER; -- 评估分数
ALTER TABLE dialogues ADD COLUMN expires_at TIMESTAMP; -- 过期时间
-- 插入时带评分和过期时间
INSERT INTO dialogues (..., score, expires_at)
VALUES (..., 85, NOW() + INTERVAL '30 days');
-- 清理过期记忆
DELETE FROM dialogues WHERE expires_at IS NOT NULL AND expires_at < NOW();
7.6 配置参数
{
"memory": {
"rag": {
"enabled": true,
"memory_evaluation": {
"enabled": true, // 是否启用记忆评估
"model": "Qwen/Qwen3-VL-8B-Instruct", // 评估模型
"url": "https://api.siliconflow.cn/v1",
"apikey": "sk-xxx",
"prompt": "你是对话重要性评估专家..." // 评估提示词
}
}
}
}
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
enabled | boolean | 是否启用记忆评估 | true |
model | string | 评估模型(建议轻量级模型) | Qwen3-VL-8B |
url | string | LLM API地址 | - |
apikey | string | API密钥 | - |
prompt | string | 评估提示词(定义评分标准) | 见默认配置 |
7.7 评估提示词模板
你是对话重要性评估专家。请评估以下**完整对话回合**的记忆价值。
**评分标准(0-100分):**
- **100分**:核心知识问答、解决方案、关键信息确认
- **80-90分**:具体需求对话、技术细节讨论、明确结论
- **50-70分**:一般性交流、偏好确认、信息补充
- **20-40分**:礼貌性问答、简单确认、无实质信息
- **0-10分**:完全无关、测试性对话、无意义内容
**评估要点:**
1. 用户问题是否包含可复用的信息?
2. AI回答是否提供了有价值的解决方案或知识?
3. 整个对话是否有长期参考价值?
4. 如果删除这段对话,是否会影响后续理解?
**输出要求:**
- 只输出一个整数,0-100之间
- 不要任何解释、标点符号、空格或换行
7.8 使用示例
7.8.1 高价值对话(永久保存)
用户:"我的Docker容器启动时报'port already in use'错误怎么办?"
AI:"这个错误表示端口被占用。你可以运行lsof -i:端口号找到占用进程..."
评估结果:
- 评分:90分
- 保留时长:1个月
- 过期时间:2024-02-16 16:00:00
- 日志:📊 记忆评估:90 分 -> 保留 1个月
7.8.2 低价值对话(不保存)
用户:"你好"
AI:"你好!有什么可以帮您?"
评估结果:
- 评分:5分
- 保留时长:不保存
- 日志:📊 记忆评估:5 分 -> 不保存到长期记忆
7.8.3 中等价值对话(保留1周)
用户:"推荐一本Python书"
AI:"《流畅的Python》很适合进阶学习"
评估结果:
- 评分:60分
- 保留时长:1周
- 过期时间:2024-01-23 16:00:00
- 日志:📊 记忆评估:60 分 -> 保留 1周
7.9 性能与成本
7.9.1 性能指标
- 评估延迟:每轮对话增加100-500ms(取决于评估模型)
- 并发处理:支持异步评估,不阻塞主流程
- 失败降级:评估失败自动降级为全量保存
7.9.2 成本优化
- 模型选择:使用轻量级模型(如Qwen3-VL-8B)降低成本
- 批量评估:未来可支持批量评估,减少API调用
- 缓存策略:相似对话可复用评估结果(待实现)
7.9.3 成本估算
假设:
- 评估模型:Qwen3-VL-8B($0.5/百万token)
- 单轮对话:平均100 tokens
- 每天1000轮对话
成本计算:
每轮评估成本 = 100 tokens × $0.5 / 1,000,000 = $0.00005
每天成本 = 1000 × $0.00005 = $0.05
每月成本 = $0.05 × 30 = $1.5
节省效果:
- 过滤掉约30%低价值对话,减少Embedding和存储成本
- 提升检索质量,减少无效记忆干扰
7.10 最佳实践
7.10.1 评分标准调优
- 初期:使用默认prompt,观察评分分布
- 调整:根据业务场景修改评分标准(如客服场景可降低阈值)
- 监控:记录评分分布,定期审查边界案例
7.10.2 降级策略
// 评估失败时的处理
match evaluator.evaluate_and_decide(user_input, &response).await {
Ok((score, duration, expires_at)) => {
if duration != RetentionDuration::None {
// 保存到长期记忆
rag.add_dialogue_with_evaluation(..., Some(score), expires_at).await
}
}
Err(e) => {
kovi::log::warn!("⚠️ 记忆评估失败: {},使用默认策略保存", e);
// 降级:全量保存(不带评分)
rag.add_dialogue(...).await
}
}
7.10.3 过期记忆清理
// 定期清理过期记忆(建议每天执行)
pub async fn cleanup_expired_memories(&self) -> Result<u64> {
self.database.execute(
"DELETE FROM dialogues WHERE expires_at IS NOT NULL AND expires_at < NOW()",
&[]
).await
}
7.10.4 监控指标
- 评估成功率:评估API调用成功率(目标>99%)
- 评分分布:各分数段占比(观察是否合理)
- 保存率:实际保存到长期记忆的对话比例(建议60-80%)
- 清理量:每日清理的过期记忆数量
7.11 数据表扩展
-- dialogues表完整结构(含评估字段)
CREATE TABLE dialogues (
id SERIAL PRIMARY KEY,
message_uuid TEXT UNIQUE NOT NULL, -- 消息唯一ID
user_id BIGINT NOT NULL,
group_id BIGINT,
chat_type TEXT CHECK (chat_type IN ('private', 'group')),
role TEXT CHECK (role IN ('user', 'assistant')),
content TEXT,
sender_name TEXT,
qq_message_id BIGINT,
embedding VECTOR(1024),
token_count INTEGER,
score INTEGER, -- 评估分数(0-100)
expires_at TIMESTAMP, -- 过期时间(NULL表示永久)
created_at TIMESTAMP DEFAULT NOW(),
created_date DATE GENERATED ALWAYS AS (created_at::date) STORED
);
-- 过期时间索引(用于清理)
CREATE INDEX idx_expires_at ON dialogues (expires_at)
WHERE expires_at IS NOT NULL;
-- 评分索引(用于分析)
CREATE INDEX idx_score ON dialogues (score)
WHERE score IS NOT NULL;
8. 关键参数配置
| 参数 | 说明 | 推荐值 | 可调范围 |
|---|---|---|---|
top_n | 向量检索锚点数量 | 3 | 0-10 |
window_size | 每锚点上下文宽度 | 2 | 0-10 |
max_memory_tokens | 记忆总token限制 | 1000 | 0-128k |
lists (IVFFLAT) | 索引分区数 | 数据量/1000 | 100-1000 |
memory_evaluation.enabled | 是否启用记忆评估 | true | true/false |
9. 使用示例(QQ机器人场景)
9.1 私聊场景
# 初始化记忆模块
memory = TemporalMemory(db_config, embedding_config)
# 用户发送消息(QQ号: 123456789)
user_id = 123456789
user_input = "我昨天问的订单号是多少?"
# 存储用户消息
memory.add_dialogue(
user_id=user_id,
role="user",
content=user_input,
sender_name="小明"
)
# 检索相关记忆
memories = memory.get_contextual_memory(
user_id=user_id,
query=user_input,
top_n=3,
window_size=2
)
# 调用LLM生成回复
response = llm_callback(memories, user_input)
# 存储AI回复
memory.add_dialogue(
user_id=user_id,
role="assistant",
content=response,
sender_name="小诗"
)
9.2 群聊场景
# 群聊消息(群号: 987654321, 用户QQ: 123456789)
group_id = 987654321
user_id = 123456789
user_input = "记得我上次分享的那个链接吗?"
# 存储群聊消息
memory.add_dialogue(
user_id=user_id,
group_id=group_id,
role="user",
content=user_input,
sender_name="小明",
message_id=1001 # QQ消息ID
)
# 检索群聊记忆(只检索该群该用户的对话)
memories = memory.get_contextual_memory(
user_id=user_id,
group_id=group_id,
query=user_input,
top_n=3,
window_size=2
)
# 生成并存储回复
response = llm_callback(memories, user_input)
memory.add_dialogue(
user_id=user_id,
group_id=group_id,
role="assistant",
content=response,
sender_name="小诗"
)
9.3 会话标识示例
# 私聊会话
session_key = TemporalMemory.generate_session_key(123456789)
# 结果: "123456789"
# 群聊会话
session_key = TemporalMemory.generate_session_key(123456789, 987654321)
# 结果: "987654321:123456789"
9.4 时间感知Prompt示例
检索到的记忆会被格式化为带时间戳的形式:
当前时间:2024-01-16 16:00:00
--- 相关记忆 ---
[2024-01-16 10:30:15] (5小时前) user(小明): 我的订单号是ORD123456
[2024-01-16 10:30:20] (5小时前) assistant(小诗): 好的,我已经记住了您的订单号ORD123456
[2024-01-15 14:20:00] (1天前) user(小明): 我想查询订单状态