Skip to content

记忆系统架构 Architecture

长期记忆系统让 AI 跨对话记住用户信息,提供更个性化的体验。

📚 相关文档

组件地图(与源码一致)

组件文件职责
MemoryServicesrc/services/memory/MemoryService.jsstructured_memories 表 CRUD、分类、搜索、合并;/api/memory 路由的数据层
MemoryExtractorsrc/services/memory/MemoryExtractor.js从对话自动提取结构化记忆
MemorySummarizersrc/services/memory/MemorySummarizer.jsAI 总结(memorySummarizer 单例;输出 [分类] 内容 行)
MemoryTypessrc/services/memory/MemoryTypes.js分类/子类型常量与中文标签映射
MemoryManagersrc/services/storage/MemoryManager.js轮询分析memory.pollInterval,默认 5 分钟)与游标管理、群聊上下文采集;memoryManager 单例
llmHelpersrc/services/memory/llmHelper.js旁路 LLM 调用(经 LlmDelegate
migrationsrc/services/memory/migration.js旧格式迁移

注意:/api/memory 直接使用 memoryService;后台轮询整理走 memoryManager, 两者共享 structured_memories 表但入口不同。

架构概览

MemoryManager 轮询与游标

MemoryManager.pollAndSummarize()startPolling() 周期触发 (memory.enabled 为 true 时启动):

  • 周期memory.pollInterval(分钟,默认 5);单目标最小间隔 memory.minPollInterval(分钟,默认 30)。
  • 双轨游标_resolvePollCursor(pollKey) 取「内存 Map(lastPollTime)」与 「kv_store 持久化值(键前缀 memory:poll:last:)」的较大者;进程重启后内存游标 丢失,由 KV 游标兜底,碰撞窗口内的对话会重新处理一遍,避免漏分析。
  • 目标标识(pollKey):私聊为 userId;共享群为 group:<gid>:user:<uid>
  • 共享群归属:只信任落库消息的 sender.user_id(旧记录无 sender 跳过), 按发送者去重,避免把整个群的对话都记到同一用户头上。
  • 限幅lastPollTime / lastSummarizeTime Map 上限 5000 条、过期 7 天 (_capTimeMap);单次轮询处理上限 100 个目标。
  • 群聊上下文采集由 startGroupContextCollection() 独立驱动 (memory.groupContext.enabledcollectInterval 默认 10 分钟)。

核心组件

MemoryService

统一管理所有记忆操作的服务类。

javascript
import { memoryService } from './services/memory/MemoryService.js'

// 保存记忆
await memoryService.saveMemory({
  userId: '123456',
  groupId: '789',           // 可选
  category: 'profile',      // 分类
  subType: 'name',          // 子类型
  content: '用户叫小明',
  confidence: 0.9,          // 可信度 0-1
  source: 'auto'            // 来源
})

// 查询记忆
const memories = await memoryService.getMemories('123456', {
  category: 'profile',
  limit: 10
})

// 搜索记忆
const results = await memoryService.searchMemories('123456', '喜欢')

MemoryExtractor

从对话中自动提取用户信息。

javascript
import { memoryExtractor } from './services/memory/MemoryExtractor.js'

// 设置 LLM 客户端
memoryExtractor.setLLMClient(llmClient)

// 提取记忆
const extracted = await memoryExtractor.extract('123456', messages)
// 返回: [{ category, subType, content, confidence }, ...]

MemorySummarizer

定期生成对话摘要。

javascript
import { memorySummarizer } from './services/memory/MemorySummarizer.js'

// 生成群聊摘要
const summary = await memorySummarizer.summarizeGroupChat(groupId, messages)

记忆分类

系统使用结构化分类管理记忆:

分类标识说明子类型
基本信息profile用户个人信息name, age, gender, location, occupation, education, contact
偏好习惯preference喜好和习惯like, dislike, hobby, habit, food, style
重要事件event日期和计划birthday, anniversary, plan, milestone, schedule
人际关系relation社交关系family, friend, colleague, partner, pet
话题兴趣topic讨论话题interest, discussed, knowledge
自定义custom扩展类型-

分类定义

javascript
import { 
  MemoryCategory,
  ProfileSubType,
  PreferenceSubType,
  getCategoryLabel,
  getSubTypeLabel
} from './services/memory/MemoryTypes.js'

// 使用分类
const memory = {
  category: MemoryCategory.PROFILE,
  subType: ProfileSubType.NAME,
  content: '用户叫小明'
}

// 获取中文标签
getCategoryLabel('profile')  // '基本信息'
getSubTypeLabel('name')      // '姓名'

数据存储

数据库表结构

实际建表语句(src/services/storage/DatabaseService.js):

sql
CREATE TABLE IF NOT EXISTS structured_memories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id TEXT NOT NULL,
    group_id TEXT,
    category TEXT NOT NULL,
    sub_type TEXT,
    content TEXT NOT NULL,
    confidence REAL DEFAULT 0.8,
    source TEXT DEFAULT 'auto',
    metadata TEXT,
    created_at INTEGER DEFAULT (strftime('%s','now') * 1000),
    updated_at INTEGER DEFAULT (strftime('%s','now') * 1000),
    expires_at INTEGER,
    is_active INTEGER DEFAULT 1
);

CREATE INDEX IF NOT EXISTS idx_struct_mem_user ON structured_memories(user_id);
CREATE INDEX IF NOT EXISTS idx_struct_mem_group ON structured_memories(group_id);
CREATE INDEX IF NOT EXISTS idx_struct_mem_category ON structured_memories(category);
CREATE INDEX IF NOT EXISTS idx_struct_mem_user_group ON structured_memories(user_id, group_id);
CREATE INDEX IF NOT EXISTS idx_struct_mem_active ON structured_memories(is_active);

(旧版记忆表 memories 仍存在,MemoryService 在初始化时会做一次迁移合并。)

记忆对象结构

typescript
interface Memory {
  id: number
  userId: string
  groupId?: string
  category: string      // profile | preference | event | relation | topic | custom
  subType?: string      // 子类型
  content: string       // 记忆内容
  confidence: number    // 可信度 0-1
  source: string        // auto | manual | import | summary | migration
  metadata?: object     // 额外元数据
  createdAt: number     // 创建时间戳
  updatedAt: number     // 更新时间戳
  expiresAt?: number    // 过期时间戳
  isActive: number      // 软删除标记(1 活跃)
}

提取流程

提取 Prompt

系统使用专门的 Prompt 指导 LLM 提取记忆:

你是一个记忆提取助手负责从对话中提取用户的关键信息

任务分析对话内容提取用户个人信息并分类

输出格式每行一条记忆格式:[分类:子类型] 内容

示例输出
[profile:name] 用户叫小明
[profile:age] 25
[preference:like] 喜欢打游戏
[event:birthday] 生日是3月15日

去重机制

保存记忆时自动检测相似内容:

javascript
// MemoryService.saveMemory() 内部逻辑
const existing = this.findSimilarMemory(userId, category, content, groupId)
if (existing) {
  // 更新现有记忆,取更高可信度
  return this.updateMemory(existing.id, {
    content,
    confidence: Math.max(existing.confidence, confidence),
    updatedAt: now
  })
}
// 插入新记忆

记忆检索

基础查询

javascript
// 按分类查询
const profiles = await memoryService.getMemories(userId, {
  category: 'profile'
})

// 按子类型查询
const likes = await memoryService.getMemories(userId, {
  category: 'preference',
  subType: 'like'
})

// 分页查询
const memories = await memoryService.getMemories(userId, {
  limit: 20,
  offset: 0
})

搜索

javascript
// 关键词搜索
const results = await memoryService.searchMemories(userId, '游戏')

// 带分类过滤
const hobbies = await memoryService.searchMemories(userId, '游戏', {
  category: 'preference'
})

注入对话

记忆通过 System Prompt 注入 AI 对话:

javascript
// 构建记忆上下文
const memories = await memoryService.getMemories(userId, { limit: 20 })
const memoryText = memories.map(m => `- ${m.content}`).join('\n')

const systemPrompt = `
你正在与用户对话。以下是关于该用户的记忆:

${memoryText}

请根据这些信息提供个性化回复。
`

群聊上下文

群聊记忆收集系统:

配置

yaml
memory:
  groupContext:
    enabled: true
    collectInterval: 10       # 收集间隔(分钟)
    maxMessagesPerCollect: 50 # 每次最大消息数
    analyzeThreshold: 20      # 触发分析的消息数
    extractUserInfo: true     # 提取用户信息
    extractTopics: true       # 提取话题
    extractRelations: true    # 提取关系

迁移支持

从旧格式迁移记忆:

javascript
import { migrateMemories } from './services/memory/migration.js'

// 迁移用户记忆
await migrateMemories(userId)

API 接口

REST API(实际端点,见记忆接口

接口方法说明
/api/memory/usersGET有记忆的用户列表(userId/count/lastUpdate/categories)
/api/memory/statsGET全局统计 { total, users, byCategory }
/api/memory/categoriesGET分类定义
/api/memory/user/:userIdGET用户记忆(format=tree/list)
/api/memory/user/:userIdPOST添加记忆(content 必填)
/api/memory/searchPOST搜索记忆
/api/memory/:idPUT更新单条(数字主键)
/api/memory/:idDELETE删除单条(hard 参数)
/api/memory/user/:userIdDELETE清空用户记忆
/api/memory/user/:userId/summarizePOSTAI 总结
/api/memory/user/:userId/cleanupPOST低质量清理
/api/memory/batchPOST批量保存
/api/memory/merge/:userIdPOST合并记忆
/api/memory/group/:groupIdGET群组记忆

示例请求

bash
# 获取用户记忆(树状结构)
curl http://localhost:3000/api/memory/user/123456?format=tree

# 添加记忆
curl -X POST http://localhost:3000/api/memory/user/123456 \
  -H "Content-Type: application/json" \
  -d '{
    "category": "preference",
    "subType": "like",
    "content": "喜欢编程"
  }'

路由遮蔽(重要)

记忆接口:纯数字路径(QQ 号)与 /user/:userId 的语义不同,数字路径始终按「主键查单条」处理。

下一步

基于 MIT 许可发布