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

11 KiB
Raw Blame History

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
  • 管理工作记忆、短期记忆、长期记忆
  • 提供 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主对话表

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 隔离表)

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短期记忆表

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长期记忆表

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向量虚拟表

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 环境变量认证
    • 模型IDPro/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 读取