orca.ai/thoughts/shared/designs/2026-05-10-memory-system-design.md
2026-05-12 00:09:01 +08:00

318 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

date: 2026-05-10
topic: "三层记忆系统 + 向量检索 + 子Agent隔离"
status: validated
## Problem Statement
当前 orca.agent 使用 JSONL 文件存储会话历史,存在以下问题:
1. **检索方式原始**:只能按时间窗口取消息,无法语义检索
2. **无记忆分层**:所有消息一视同仁,早期重要信息被截断
3. **无跨会话知识**:用户偏好、项目背景每次重新说明
4. **子 Agent 污染上下文**:子 agent 的详细推理过程进入主 agent 上下文
## Constraints
1. **存储**:使用 SQLite + sqlite-vec用户已安装
2. **Embedding**:使用硅基流动 APIPro/BAAI/bge-m31024维8K上下文
3. **向后兼容**:保留 JSONL 作为备份/迁移选项
4. **API 密钥**:通过 `~/.orca/config.toml``[siliconflow]` 段配置
## Approach
采用 **三层记忆架构** + **向量检索** + **子 Agent 隔离**
**三层记忆**
- **工作记忆**:当前对话窗口(最近 N 条消息)
- **短期记忆**:本会话历史摘要(语义检索)
- **长期记忆**:跨会话关键知识(语义检索)
**向量检索**
- 每条消息保存时生成 Embedding
- 检索时使用余弦相似度匹配相关记忆
- 支持跨会话语义检索
**子 Agent 隔离**
- 子 agent 推理过程存储在独立表
- 主 agent 只接收结果摘要
- Web UI 可查看完整子 agent 执行过程
## Architecture
```
用户输入
├─→ [工作记忆] SQL 查询最近 N 条
│ └─→ 直接注入 Prompt
├─→ [短期记忆] 向量检索当前会话相关历史
│ └─→ 语义匹配 → 注入 Prompt
├─→ [长期记忆] 向量检索跨会话知识
│ └─→ 语义匹配 → 注入 Prompt
└─→ LLM 生成回复
├─→ 保存到 main_messages生成 Embedding
└─→ 如果是工具调用 → 子 Agent 执行
├─→ 子 Agent 输出 → subagent_messages
│ └─→ 完成后返回结果
└─→ 结果摘要 → main_messages
```
## Components
### 1. SQLiteStore替代 JSONLStore
- 文件:`pkg/session/sqlite_store.go`
- 实现 `Store` 接口
- 使用 SQLite 存储消息
- 初始化时创建表结构
- **向后兼容**:提供 JSONL → SQLite 迁移脚本
### 2. VectorStore向量层
- 文件:`pkg/session/vector_store.go`
- 封装 sqlite-vec 操作
- 提供 SaveEmbedding、SearchSimilar 方法
- 使用硅基流动 Embed API 生成向量Pro/BAAI/bge-m31024维
- **优化策略**
- 只对用户消息和完整 Assistant 回复生成向量(跳过短消息 < 10
- 异步生成消息先存 SQLiteEmbedding 后台 goroutine 生成
- 批量生成累积 5 条消息后一次性生成
### 3. MemoryManager三层记忆管理
- 文件`pkg/session/memory_manager.go`
- 管理工作记忆短期记忆长期记忆
- 提供 GetWorkingMemoryGetShortTermMemoryGetLongTermMemory
- **Token 预算管理**
- 总预算模型窗口的 60% 8K 模型 = 4800 tokens
- 工作记忆预算的 50% 2400 tokens
- 短期记忆预算的 30% 1440 tokens
- 长期记忆预算的 20% 960 tokens
- 自动维护记忆生成摘要压缩
### 4. SubAgentStore子 Agent 隔离)
- 文件`pkg/session/subagent_store.go` 或复用 sqlite_store
- 独立表存储子 agent 输出
- **存储策略**只存最终完整回复一条记录不存流式 token
- 不与主 agent 上下文混合
### 5. LLM Agent 集成
- 修改`pkg/actor/llm_agent.go`
- `buildLLMMessages()` 整合三层记忆
- agent 调用时设置隔离存储
- **触发时机**每轮对话结束生成短期摘要会话结束压缩到长期记忆
## Database Schema
### main_messages主对话表
```sql
CREATE TABLE main_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
content TEXT NOT NULL,
msg_type TEXT DEFAULT 'normal' CHECK(msg_type IN ('normal', 'fact', 'todo', 'decision', 'preference', 'error')),
token_count INTEGER DEFAULT 0,
has_embedding BOOLEAN DEFAULT FALSE,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
metadata JSON
);
CREATE INDEX idx_main_session_time ON main_messages(session_id, timestamp DESC);
CREATE INDEX idx_main_role ON main_messages(role) WHERE role IN ('user', 'assistant');
```
### subagent_messages子 Agent 隔离表)
```sql
CREATE TABLE subagent_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_msg_id INTEGER NOT NULL,
session_id TEXT NOT NULL,
agent_name TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('assistant', 'system')),
content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (parent_msg_id) REFERENCES main_messages(id) ON DELETE CASCADE
);
CREATE INDEX idx_subagent_parent ON subagent_messages(parent_msg_id);
```
### short_term_memories短期记忆表
```sql
CREATE TABLE short_term_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
content TEXT NOT NULL,
source_count INTEGER DEFAULT 1,
confidence REAL DEFAULT 0.8 CHECK(confidence BETWEEN 0 AND 1),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME
);
CREATE INDEX idx_stm_session ON short_term_memories(session_id, updated_at DESC);
```
### long_term_memories长期记忆表
```sql
CREATE TABLE long_term_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
memory_type TEXT NOT NULL CHECK(memory_type IN ('preference', 'fact', 'decision', 'project')),
source_session TEXT,
confidence REAL DEFAULT 0.5 CHECK(confidence BETWEEN 0 AND 1),
access_count INTEGER DEFAULT 0,
last_accessed DATETIME,
expires_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_ltm_type ON long_term_memories(memory_type, confidence DESC);
```
### vec_main_messages向量虚拟表
```sql
CREATE VIRTUAL TABLE vec_main_messages USING vec0(
msg_id INTEGER PRIMARY KEY,
embedding FLOAT[1024] -- 硅基流动 Pro/BAAI/bge-m3 维度
);
```
## Data Flow
### 消息存储流程
```
AddMessage(msg)
├─→ SQLiteStore.Save(msg) → main_messages 表
│ └─→ 计算 token_count估算或计数
├─→ 【异步】如果 msg.role IN ('user', 'assistant') 且 len(content) > 10
│ └─→ EmbeddingQueue <- msg
│ └─→ 后台 goroutine 批量生成 Embedding
│ └─→ VectorStore.Save(msg.id, embedding)
│ └─→ UPDATE main_messages SET has_embedding = TRUE
└─→ 返回
```
### 记忆检索流程
```
buildLLMMessages()
├─→ MemoryManager.GetWorkingMemory(sessionID, tokenBudget=2400)
│ └─→ SQL: SELECT * FROM main_messages
│ WHERE session_id = ?
│ ORDER BY timestamp DESC
│ └─→ 累积 messages 直到 token_count ≈ 2400
│ └─→ 反转顺序( chronological
├─→ MemoryManager.GetShortTermMemory(sessionID, query, maxItems=3)
│ └─→ 如果 query 存在且 has_embedding:
│ └─→ 硅基流动 API.Embed(query)
│ └─→ 向量检索 short_term_memories同 session
│ └─→ 返回 Top-3 相关摘要
│ └─→ 否则SQL 取最近 3 条摘要
└─→ MemoryManager.GetLongTermMemory(query, maxItems=2)
└─→ 如果 query 存在且 has_embedding:
│ └─→ 硅基流动 API.Embed(query)
│ └─→ 向量检索 long_term_memories
│ └─→ 返回 Top-2 相关记忆
└─→ 否则SQL 取 access_count 最高的 2 条
└─→ UPDATE access_count += 1, last_accessed = NOW()
```
### 子 Agent 执行流程
```
agent_call.Execute(coder)
├─→ 创建 SubAgent 上下文(隔离存储)
├─→ coder.Process(task)
│ ├─→ 流式输出 → subagent_messages
│ └─→ 完成后返回结果
└─→ 结果摘要 → main_messages
```
## Error Handling
1. **Embedding 失败**降级为纯文本存储不影响功能
- 标记 `has_embedding = FALSE`
- 后续检索使用 SQL 时间排序代替向量相似度
2. **sqlite-vec 不可用**降级为纯 SQL 查询无向量检索
- 短期记忆取最近 N 条摘要按时间排序
- 长期记忆 access_count 最高的记忆按访问计数排序
3. **硅基流动 API 不可用**跳过向量检索纯文本模式运行
- 启动时检测 API 可用性读取 config.toml 验证 api_key
- 不可用时禁用向量功能回退到 SQL 时间排序
- 记录警告日志不影响主功能
4. **存储失败**保留 JSONL 作为备份
- 初始化时如果 SQLite 失败回退到 JSONLStore
- 提供迁移脚本 `cmd/migrate-jsonl-to-sqlite`
5. **Token 预算超限**优先保留工作记忆
- 先削减长期记忆降为 1
- 再削减短期记忆降为 1
- 最后削减工作记忆减少消息数量
6. **子 Agent 流式输出过大**截断保护
- 单条子 Agent 回复限制 10000 tokens
- 超出部分截断并标记 `[内容已截断]`
## Testing Strategy
1. **单元测试**
- SQLiteStore CRUD 操作
- VectorStore 相似度搜索
- MemoryManager 三层检索
2. **集成测试**
- 端到端消息存储和检索
- Agent 隔离验证
- 向量检索准确性
3. **迁移测试**
- JSONL SQLite 数据迁移
- 向后兼容性
## Memory Maintenance Strategy
### 短期记忆生成
- **触发时机**每轮对话结束Assistant 回复完成后
- **生成方式**调用 LLM 生成 1-2 句话摘要
- **Prompt 示例**"请用一句话总结本轮对话的关键信息不超过 50 "
- **存储**插入 `short_term_memories`
### 长期记忆生成
- **触发时机**会话结束或超过 20 轮对话
- **生成方式**
1. 合并短期记忆摘要
2. 调用 LLM 提取关键事实偏好决策
3. 分类为 preference/fact/decision/project
- **去重**与新长期记忆做文本相似度比较相似度 > 0.8 则更新旧记录
### 记忆清理
- **短期记忆**:保留最近 10 条,旧的自动删除
- **长期记忆**:过期检查(`expires_at`),过期后降权而非删除
- **访问计数**:长期记忆根据 `access_count` 排序,低频记忆逐步淘汰
## Open Questions
1. **Embedding 模型选择**:硅基流动 Pro/BAAI/bge-m31024维8K上下文
- **配置**:通过 `SILICONFLOW_API_KEY` 环境变量认证
- **模型ID**`Pro/BAAI/bge-m3`
2. **向量维度**1024维sqlite-vec 完全支持
3. **性能**:大量消息时向量检索性能如何?是否需要索引优化?
- **优化**sqlite-vec 自动创建 IVF 索引10 万条消息内性能良好
4. **Token 计数**:是否需要在 Go 端实现 tiktoken
- **建议**初期用简单估算1 中文字 ≈ 1.5 tokens后续引入 tiktoken
5. **迁移策略**:是否强制迁移现有 JSONL
- **建议**:不强制迁移,新会话使用 SQLite旧会话仍从 JSONL 读取