11 KiB
11 KiB
date: 2026-05-10 topic: "三层记忆系统 + 向量检索 + 子Agent隔离" status: validated
Problem Statement
当前 orca.agent 使用 JSONL 文件存储会话历史,存在以下问题:
- 检索方式原始:只能按时间窗口取消息,无法语义检索
- 无记忆分层:所有消息一视同仁,早期重要信息被截断
- 无跨会话知识:用户偏好、项目背景每次重新说明
- 子 Agent 污染上下文:子 agent 的详细推理过程进入主 agent 上下文
Constraints
- 存储:使用 SQLite + sqlite-vec(用户已安装)
- Embedding:使用硅基流动 API(Pro/BAAI/bge-m3,1024维,8K上下文)
- 向后兼容:保留 JSONL 作为备份/迁移选项
- 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(主对话表)
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
-
Embedding 失败:降级为纯文本存储,不影响功能
- 标记
has_embedding = FALSE - 后续检索使用 SQL 时间排序代替向量相似度
- 标记
-
sqlite-vec 不可用:降级为纯 SQL 查询(无向量检索)
- 短期记忆:取最近 N 条摘要(按时间排序)
- 长期记忆:取 access_count 最高的记忆(按访问计数排序)
-
硅基流动 API 不可用:跳过向量检索,纯文本模式运行
- 启动时检测 API 可用性(读取 config.toml 验证 api_key)
- 不可用时禁用向量功能,回退到 SQL 时间排序
- 记录警告日志,不影响主功能
-
存储失败:保留 JSONL 作为备份
- 初始化时如果 SQLite 失败,回退到 JSONLStore
- 提供迁移脚本
cmd/migrate-jsonl-to-sqlite
-
Token 预算超限:优先保留工作记忆
- 先削减长期记忆(降为 1 条)
- 再削减短期记忆(降为 1 条)
- 最后削减工作记忆(减少消息数量)
-
子 Agent 流式输出过大:截断保护
- 单条子 Agent 回复限制 10000 tokens
- 超出部分截断并标记
[内容已截断]
Testing Strategy
-
单元测试:
- SQLiteStore CRUD 操作
- VectorStore 相似度搜索
- MemoryManager 三层检索
-
集成测试:
- 端到端消息存储和检索
- 子 Agent 隔离验证
- 向量检索准确性
-
迁移测试:
- JSONL → SQLite 数据迁移
- 向后兼容性
Memory Maintenance Strategy
短期记忆生成
- 触发时机:每轮对话结束(Assistant 回复完成后)
- 生成方式:调用 LLM 生成 1-2 句话摘要
- Prompt 示例:"请用一句话总结本轮对话的关键信息,不超过 50 字:"
- 存储:插入
short_term_memories表
长期记忆生成
- 触发时机:会话结束或超过 20 轮对话
- 生成方式:
- 合并短期记忆摘要
- 调用 LLM 提取关键事实、偏好、决策
- 分类为 preference/fact/decision/project
- 去重:与新长期记忆做文本相似度比较,相似度 > 0.8 则更新旧记录
记忆清理
- 短期记忆:保留最近 10 条,旧的自动删除
- 长期记忆:过期检查(
expires_at),过期后降权而非删除 - 访问计数:长期记忆根据
access_count排序,低频记忆逐步淘汰
Open Questions
-
Embedding 模型选择:硅基流动 Pro/BAAI/bge-m3,1024维,8K上下文
- 配置:通过
SILICONFLOW_API_KEY环境变量认证 - 模型ID:
Pro/BAAI/bge-m3
- 配置:通过
-
向量维度:1024维,sqlite-vec 完全支持
-
性能:大量消息时向量检索性能如何?是否需要索引优化?
- 优化:sqlite-vec 自动创建 IVF 索引,10 万条消息内性能良好
-
Token 计数:是否需要在 Go 端实现 tiktoken?
- 建议:初期用简单估算(1 中文字 ≈ 1.5 tokens),后续引入 tiktoken
-
迁移策略:是否强制迁移现有 JSONL?
- 建议:不强制迁移,新会话使用 SQLite,旧会话仍从 JSONL 读取