RAG记忆增强系统

版本:v1.0
日期:2025-12-04
目标:为LLM构建具备时间感知和上下文理解的持久化记忆模块


1. 项目概述

1.1 核心目标

构建一个基于向量检索(RAG)的对话记忆系统,使LLM能够:

  • 记住多轮对话历史
  • 基于语义相似性检索相关记忆
  • 保持对话上下文的时空连贯性
  • 理解时间敏感信息("明天"、"刚才"等)

1.2 关键场景

  • QQ机器人:区分私聊和群聊,记住每个用户的历史对话
  • 智能客服:记住用户偏好和历史问题
  • 个人助理:跟踪任务进度和时间安排
  • 群聊助手:理解群组上下文,避免跨群记忆污染

2. 功能需求

2.1 核心记忆存储

功能点详细描述优先级
对话持久化存储用户/助手的每轮对话,包含角色、内容、时间戳P0
向量生成调用外部Embedding API(SiliconFlow)将文本转为向量P0
会话隔离基于user_idgroup_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

检索逻辑

  1. 根据 user_idgroup_id 确定查询范围
  2. 将查询文本向量化(调用Embedding API)
  3. 在向量数据库中查找Top-N相似度锚点
    • 私聊:只检索 user_id=X AND group_id IS NULL 的对话
    • 群聊:只检索 user_id=X AND group_id=Y 的对话
  4. 为每个锚点扩展前后window_size条对话(保持上下文连贯)
  5. 去重后按 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. 时间久远的信息可能已过时
    2. 涉及时间敏感词请基于当前时间理解
    3. 指代词需结合时间戳判断
    4. 对话间隔超过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": "你是对话重要性评估专家..."  // 评估提示词
      }
    }
  }
}
参数类型说明默认值
enabledboolean是否启用记忆评估true
modelstring评估模型(建议轻量级模型)Qwen3-VL-8B
urlstringLLM API地址-
apikeystringAPI密钥-
promptstring评估提示词(定义评分标准)见默认配置

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向量检索锚点数量30-10
window_size每锚点上下文宽度20-10
max_memory_tokens记忆总token限制10000-128k
lists (IVFFLAT)索引分区数数据量/1000100-1000
memory_evaluation.enabled是否启用记忆评估truetrue/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(小明): 我想查询订单状态