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**:使用硅基流动 API(Pro/BAAI/bge-m3,1024维,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-m3,1024维) - **优化策略**: - 只对用户消息和完整 Assistant 回复生成向量(跳过短消息 < 10 字) - 异步生成:消息先存 SQLite,Embedding 后台 goroutine 生成 - 批量生成:累积 5 条消息后一次性生成 ### 3. MemoryManager(三层记忆管理) - 文件:`pkg/session/memory_manager.go` - 管理工作记忆、短期记忆、长期记忆 - 提供 GetWorkingMemory、GetShortTermMemory、GetLongTermMemory - **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-m3,1024维,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 读取