318 lines
11 KiB
Markdown
318 lines
11 KiB
Markdown
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 读取
|