向量记忆系统
向量记忆系统
学习目标
- 理解 Cherry Studio 如何通过 SQLite + LibSQL 实现本地持久化记忆存储
- 掌握主进程 MemoryService 与渲染进程的委托模式(IPC 通信)
- 了解嵌入向量生成、语义相似度搜索与用户隔离的具体实现
- 能够基于现有架构设计自己的记忆管理方案
前置知识
本章专注于 Cherry Studio 的具体工程实现,以下内容已在通用知识教程中详细讲解,不再重复:
项目实践
整体架构:双进程委托模式
Cherry Studio 基于 Electron 构建,记忆系统采用经典的主进程承载 + 渲染进程委托架构。渲染进程不直接操作数据库,而是通过 IPC(进程间通信)将记忆操作委托给主进程。
为什么选择委托模式?
- SQLite 数据库文件位于主进程可访问的路径(
DATA_PATH/Memory/memories.db),渲染进程无权直接读写 - 向量嵌入生成需要调用外部 API(OpenAI 兼容接口),密钥应保存在主进程
- IPC 通道天然提供了一层隔离,渲染进程的代码注入不会影响数据库操作
核心模块与文件布局
| 文件路径 | 职责 | 大小 |
|---|---|---|
src/main/services/memory/MemoryService.ts | 主进程记忆服务,协调数据库操作与向量嵌入 | 26KB+ |
src/main/services/memory/queries.ts | MemoryQueries 数据库操作层,封装所有 SQL 语句 | 核心依赖 |
src/renderer/src/services/MemoryService.ts | 渲染进程代理,通过 IPC 转发请求 | 8KB+ |
src/renderer/src/store/memory.ts | Redux memory slice,存储记忆配置 | 状态管理 |
src/main/knowledge/embedjs/embeddings/Embeddings.ts | 嵌入向量生成模块 | 向量计算 |
数据库初始化与路径迁移
Cherry Studio 使用 @libsql/client(v0.14.0)作为 SQLite 驱动。LibSQL 是 SQLite 的一个分支,在保持兼容性的同时扩展了远程同步能力。
关键细节:
- 数据库路径:
DATA_PATH/Memory/memories.db,而非旧版的userData/memories.db migrateMemoryDb方法负责检测旧路径数据库并迁移到新位置,保证升级平滑过渡- 建表逻辑在
MemoryQueries中完成,使用CREATE TABLE IF NOT EXISTS确保幂等
嵌入向量配置
Cherry Studio 的记忆系统不绑定特定的嵌入模型提供商,而是通过可配置的方式支持多种后端。
统一维度约束:
UNIFIED_DIMENSION = 1536— 系统内部统一使用的嵌入向量维度- 所有存储到数据库的向量必须为 1536 维,不同模型的输出在此维度上对齐
- 如果配置的模型输出维度不为 1536,需要进行截断或填充处理
记忆类型定义
系统定义了三种核心类型:
| 类型 | 用途 | 关键字段 |
|---|---|---|
MemoryItem | 单条记忆记录 | id, content, embedding, userId, agentId, tags, timestamp |
MemoryHistoryItem | 记忆操作审计日志 | id, action, memoryId, userId, timestamp |
MemoryConfig | 用户记忆系统配置 | userId, agentId, enabled, embeddingModel, similarityThreshold |
记忆操作流程
相似度阈值: SIMILARITY_THRESHOLD = 0.85
- 语义搜索时,只有余弦相似度 >= 0.85 的结果才会被返回
- 这个阈值是一个经验值,在保证召回精度的同时避免返回过多弱相关结果
用户与 Agent 隔离
Cherry Studio 支持多用户和多 Agent 场景,记忆系统通过 userId 和 agentId 实现数据隔离。
隔离层级:
userId级别:不同用户的记忆完全隔离,互不可见agentId级别:同一用户下不同 Agent(如”翻译助手”、“代码审查”)拥有各自独立的记忆池- 数据库查询层面强制附加过滤条件,不存在绕过隔离的路径
渲染进程操作流程
渲染进程的 MemoryService 是一个薄代理层,负责将 UI 层的调用转换为 IPC 消息。
配置读取流程:
- 渲染进程通过
selectMemoryConfig从 Redux store 获取当前记忆配置 - 配置包含模型名称、提供商、API 密钥等敏感信息
- 这些配置会随 IPC 请求传递给主进程,由主进程实际调用嵌入 API
知识库参数转换
getKnowledgeBaseParams 方法负责将 MemoryConfig 转换为知识库模块可用的参数格式,打通记忆系统与知识库之间的数据流。这使得记忆中的内容可以被知识库检索模块复用,实现记忆与知识的联动。
问题与规避
1. 数据库路径迁移风险
旧版本数据库位于 userData/memories.db,新版改为 DATA_PATH/Memory/memories.db。迁移过程中的风险:
- 文件冲突:如果新路径下已存在同名文件,迁移可能覆盖数据
- 权限问题:新路径的目录权限可能与旧路径不同
规避策略:migrateMemoryDb 应在启动早期执行,且仅在旧路径存在、新路径不存在时触发迁移。
2. 嵌入向量维度不一致
系统规定 UNIFIED_DIMENSION = 1536,但不同嵌入模型的输出维度可能不同:
- OpenAI
text-embedding-3-small:默认 1536 维(可通过dimensions参数调整) - 其他提供商的模型可能输出 768、1024 等不同维度
规避策略:
- 配置层提供
dimensions字段,确保与模型实际输出匹配 - 存储前进行维度校验,不一致时截断或零填充至 1536 维
- 在
MemoryConfig中记录使用的模型与维度,便于追溯
3. 相似度阈值调优
SIMILARITY_THRESHOLD = 0.85 是固定阈值,但不同场景可能需要不同的精度:
- 高精确场景(如代码补全记忆):阈值应更高(0.90+)
- 高召回场景(如知识推荐):阈值可适当降低(0.75+)
规避策略:MemoryConfig 中的 similarityThreshold 字段允许用户自定义阈值,而非硬编码。
4. 大批量写入性能
嵌入向量生成通常是网络 I/O 操作,批量写入时如果逐条处理会导致性能瓶颈。
规避策略:
MemoryConfig提供batchSize字段控制批量大小Embeddings模块支持批量生成嵌入向量,减少 API 调用次数- 主进程可采用异步队列缓冲写入请求,避免阻塞 UI
5. IPC 通信序列化开销
渲染进程与主进程之间的 IPC 通信需要序列化/反序列化,传输大体积嵌入向量(1536 维 float 数组)时可能成为瓶颈。
规避策略:
- 嵌入向量生成在主进程完成,渲染进程只需传递文本内容,不传输向量
- 搜索结果返回时只传递记忆内容和相似度分数,不返回完整向量
- 对于批量操作,合并为单次 IPC 调用而非多次独立调用
设计取舍
为什么选择 SQLite 而非专用向量数据库?
Cherry Studio 定位为本地优先的桌面应用,选择 SQLite 的核心理由:
- 零运维:无需额外部署向量数据库服务,安装即用
- 离线可用:记忆数据完全存储在本地,不依赖网络
- 轻量级:
@libsql/client的打包体积小,不影响应用分发体积 - 足够的性能:对于桌面应用的记忆数据量(通常 < 10 万条),SQLite 的向量搜索性能完全够用
取舍代价:
- 不支持分布式记忆共享
- 向量搜索算法需要自行实现(余弦相似度计算),无法使用 HNSW 等高级索引
- 大规模数据量下搜索性能不如专用向量数据库
委托模式 vs 渲染进程直连
选择委托模式的核心理由:
- 安全性:API 密钥和嵌入配置由主进程保管,不会泄露到渲染进程
- 一致性:所有记忆操作集中到主进程,避免多渲染进程并发写入导致的数据竞争
- 可维护性:数据库操作逻辑集中在一处,升级数据库 schema 时只需修改主进程代码
嵌入模型可配置 vs 内置固定模型
Cherry Studio 选择了可配置方案,通过 Redux store 管理 MemoryConfig,允许用户自行选择嵌入模型提供商。这是对灵活性的取舍,代价是需要在维度对齐和配置管理上投入更多工程精力。