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

491 lines
17 KiB
Markdown
Raw Permalink 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: 2025-05-11
topic: "Orca 完整记忆系统 + Agent 进化方案"
status: validated
---
# Orca 完整记忆系统 + Agent 进化方案
## 一、系统架构总览
```
┌─────────────────────────────────────────────────────────────┐
│ 用户消息 │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 智能注入层(规则驱动,零 LLM 开销) │
│ • 首轮对话 → 不带记忆 │
│ • 第 2+ 轮 → 自动注入短期记忆3条
│ • 技术讨论 → 额外注入长期记忆2条
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 记忆查询层(按需加载) │
│ • 短期记忆SQLite + 语义检索(缓存命中 130x 加速) │
│ • 长期记忆SQLite + 独立向量索引vec_long_term_memories
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ LLM Agent 处理消息 │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 记忆维护层(后台异步) │
│ • 短期记忆:即时摘要保存(现有逻辑) │
│ • 长期记忆MemoryExtractorAgent 批量提取(每 5 轮一次) │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 进化层(权重系统) │
│ • 记忆被引用 → 权重 +1 │
│ • 记忆未引用 → 权重 -0.5 │
│ • 权重 < 0.3 → 归档 │
│ • 权重 > 5.0 → 核心记忆 │
└─────────────────────────────────────────────────────────────┘
```
---
## 二、配置架构config.toml
```toml
# ========== 主 LLM 配置 ==========
provider = "deepseek"
[deepseek]
base_url = "https://api.deepseek.com/v1"
model = "deepseek-v4-flash"
api_key = "sk-xxx"
timeout = "120s"
# ========== Embedding 配置 ==========
[embedding]
provider = "siliconflow"
model = "Pro/BAAI/bge-m3"
dimensions = 1024
max_context = 8192
[siliconflow]
api_key = "sk-xxx"
base_url = "https://api.siliconflow.cn/v1"
# ========== 记忆系统配置(新增) ==========
[memory]
enabled = true # 记忆系统总开关
max_history = 100 # 工作记忆最大轮数
[memory.short_term]
max_items = 10 # 最多保留 10 条短期记忆
compression_threshold = 200 # 超过 200 字自动压缩
[memory.long_term]
enabled = true # 长期记忆开关
vector_index = true # 启用向量索引
max_return = 2 # 每次最多返回 2 条
archive_threshold = 0.3 # 权重低于此值归档
[memory.inject]
first_round_empty = true # 首轮不带记忆
short_term_count = 3 # 默认注入 3 条短期记忆
long_term_trigger = "technical" # 技术讨论触发长期记忆
min_query_length = 10 # query 最短长度才查长期记忆
# ========== MemoryExtractorAgent 配置(新增) ==========
[memory_agent]
enabled = true
provider = "deepseek" # 可独立配置,默认继承主 LLM
model = "deepseek-v4-flash" # 可用便宜模型,如 deepseek-chat
api_key = "" # 留空继承 deepseek.api_key
base_url = "" # 留空继承 deepseek.base_url
timeout = "60s"
[memory_agent.extract]
batch_size = 5 # 每 5 轮提取一次
max_facts = 10 # 每次最多提取 10 个事实
min_confidence = 0.6 # 置信度阈值
auto_tag = true # 自动打标签
[memory_agent.summarize]
enabled = true # 启用对话总结
trigger_tokens = 4000 # 超过此 token 触发总结
```
---
## 三、Agent 描述文件
**文件位置** `~/.orca/agents/memory_extractor.md`
```markdown
# Memory Extractor Agent
你是一个专门从对话中提取用户信息的 Agent。你的工作是将非结构化的对话转化为结构化的长期记忆。
## 任务
分析给定的对话记录,提取以下类型的信息:
1. **事实 (fact)**:客观信息
- 工作:公司、职位、技术栈、行业
- 技术:擅长语言、框架偏好、架构经验
- 个人:教育背景、所在城市(仅用户明确提及)
2. **偏好 (preference)**:主观倾向
- 回答风格:简洁/详细/代码示例/架构图
- 技术偏好:语言、数据库、部署方式
- 沟通偏好:正式/ casual
3. **项目 (project)**:当前工作
- 项目名称、技术方案、当前阶段、遇到的挑战
## 输出格式
只输出 JSON不要任何解释
```json
{
"facts": [
{
"content": "用户在电商公司担任后端工程师",
"type": "fact",
"tags": ["工作", "后端", "电商"],
"confidence": 0.95,
"replace": null
},
{
"content": "用户偏好简洁的技术回答,不要过多解释",
"type": "preference",
"tags": ["沟通风格", "偏好"],
"confidence": 0.85,
"replace": "用户喜欢详细的回答"
}
]
}
```
## 规则
- confidence < 0.6 的事实不输出
- 如果新事实与旧事实冲突
- replace 字段填入被替换的旧事实 content
- 只替换同一 type + 同一 tags 的事实
- 不猜测不推断只提取用户明确表达的信息
- 标签从预设列表选择工作技术偏好项目沟通风格行业
```
---
## 四、数据表设计
### 现有表(已验证)
- `main_messages` — 工作记忆
- `short_term_memories` — 短期记忆
- `subagent_messages` — 子 Agent 对话
### 新增表
```sql
-- 长期记忆向量索引(核心新增)
CREATE VIRTUAL TABLE IF NOT EXISTS vec_long_term_memories USING vec0(
memory_id INTEGER PRIMARY KEY,
embedding FLOAT[1024]
);
-- 长期记忆主表(扩展)
CREATE TABLE IF NOT EXISTS long_term_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL UNIQUE,
memory_type TEXT NOT NULL DEFAULT 'fact', -- fact/preference/project
tags TEXT, -- JSON 数组 ["工作", "技术"]
confidence REAL NOT NULL DEFAULT 0.8,
weight REAL NOT NULL DEFAULT 1.0, -- 动态权重
access_count INTEGER NOT NULL DEFAULT 0,
last_accessed DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
archived INTEGER NOT NULL DEFAULT 0 -- 0=活跃, 1=归档
);
-- 记忆使用日志(用于权重计算)
CREATE TABLE IF NOT EXISTS memory_usage_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_id INTEGER NOT NULL,
session_id TEXT NOT NULL,
query TEXT NOT NULL, -- 用户原始 query
was_referenced INTEGER NOT NULL DEFAULT 0, -- 是否被 Agent 引用
used_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 对话缓冲(批量提取用)
CREATE TABLE IF NOT EXISTS dialogue_buffer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
user_query TEXT NOT NULL,
assistant_response TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
---
## 五、核心流程设计
### 5.1 消息处理流程(动态注入)
```go
func (a *LLMAgent) buildLLMMessages(query string) []llm.Message {
messages := make([]llm.Message, 0)
// 1. System prompt原有
if a.systemPrompt != "" {
messages = append(messages, llm.Message{Role: "system", Content: a.systemPrompt})
}
// 2. Tool prompt原有
if toolPrompt := a.buildToolPrompt(); toolPrompt != "" {
messages = append(messages, llm.Message{Role: "system", Content: toolPrompt})
}
// 3. 记忆注入(新增:智能判断)
if shouldInjectMemory(query, a.sessionMgr.GetMessageCount(a.sessionID)) {
ctx, stats := a.memoryManager.BuildMemoryContextWithStats(a.sessionID, query)
if ctx != "" {
messages = append(messages, llm.Message{Role: "system", Content: ctx})
log.Printf("[memory] Injected: short=%d, long=%d, tokens=%d",
stats.ShortTermCount, stats.LongTermCount, stats.TotalTokens)
}
}
// 4. 对话历史(原有)
// ...
return messages
}
func shouldInjectMemory(query string, msgCount int) bool {
if msgCount == 0 {
return false // 首轮不带记忆
}
if len(query) < 10 {
return false // 太短不查
}
return true
}
```
### 5.2 记忆维护流程(批量提取)
```go
func (mm *MemoryManager) MaintainSessionMemory(sessionID, userQuery, assistantResponse string) {
// 1. 保存短期记忆(即时)
summary := fmt.Sprintf("用户问:%s\n回答%s",
userQuery, truncateString(assistantResponse, 100))
mm.AddShortTermMemory(sessionID, summary)
// 2. 缓冲对话(用于批量提取)
mm.bufferDialogue(sessionID, userQuery, assistantResponse)
// 3. 检查是否触发批量提取
if mm.shouldExtract() {
mm.triggerExtraction(sessionID)
}
}
func (mm *MemoryManager) triggerExtraction(sessionID string) {
dialogues := mm.flushBuffer(sessionID)
// 异步调用 MemoryExtractorAgent
go func() {
facts, err := mm.extractor.ExtractFacts(dialogues)
if err != nil {
log.Printf("[memory] Extraction failed: %v", err)
return
}
for _, fact := range facts {
if fact.Confidence >= mm.config.MinConfidence {
mm.AddLongTermMemory(fact)
}
}
}()
}
```
### 5.3 权重反馈流程(自动进化)
```go
func (mm *MemoryManager) recordMemoryUsage(memoryID int64, sessionID, query string, referenced bool) {
// 1. 记录使用日志
mm.store.Exec(
"INSERT INTO memory_usage_log (memory_id, session_id, query, was_referenced) VALUES (?, ?, ?, ?)",
memoryID, sessionID, query, referenced,
)
// 2. 更新权重
delta := 0.5
if !referenced {
delta = -0.3
}
mm.store.Exec(
"UPDATE long_term_memories SET weight = weight + ?, access_count = access_count + 1, last_accessed = ? WHERE id = ?",
delta, time.Now(), memoryID,
)
// 3. 检查归档
mm.archiveLowWeightMemories()
}
```
---
## 六、MemoryExtractorAgent 实现
```go
package actor
type MemoryExtractorAgent struct {
*SubAgent
config ExtractConfig
}
func NewMemoryExtractorAgent(id string, llmBackend llm.LLM, cfg ExtractConfig) *MemoryExtractorAgent {
prompt := loadAgentPrompt("memory_extractor") // 读取 ~/.orca/agents/memory_extractor.md
sa := NewSubAgent(id, llmBackend,
WithSubAgentRole("memory_extractor"),
WithSubAgentSystemPrompt(prompt),
)
return &MemoryExtractorAgent{SubAgent: sa, config: cfg}
}
func (mea *MemoryExtractorAgent) ExtractFacts(dialogues []Dialogue) ([]Fact, error) {
// 格式化对话为 prompt
var sb strings.Builder
sb.WriteString("请分析以下对话记录,提取用户的关键信息:\n\n")
for i, d := range dialogues {
sb.WriteString(fmt.Sprintf("--- 对话 %d ---\n", i+1))
sb.WriteString(fmt.Sprintf("用户:%s\n", d.UserQuery))
sb.WriteString(fmt.Sprintf("助手:%s\n\n", d.AssistantResponse))
}
msg := bus.Message{Type: bus.MsgTypeTaskRequest, Content: sb.String()}
resp, err := mea.Process(context.Background(), msg)
if err != nil {
return nil, err
}
return parseFactJSON(resp.Content)
}
```
---
## 七、实施路线图
### Phase 1: 基础记忆层1-2 天)
- [ ] 扩展 `long_term_memories` 添加 weight, tags, archived 字段
- [ ] 创建 `vec_long_term_memories` 向量索引表
- [ ] 创建 `memory_usage_log` 使用日志表
- [ ] 创建 `dialogue_buffer` 对话缓冲表
- [ ] 修改 config.toml 解析支持 `[memory]` `[memory_agent]`
### Phase 2: 记忆注入层1-2 天)
- [ ] 实现 `shouldInjectMemory()` 智能判断逻辑
- [ ] 修改 `buildLLMMessages()` 注入记忆上下文
- [ ] 实现短期记忆检索现有逻辑优化
- [ ] 实现长期记忆向量检索语义搜索
- [ ] 添加 Embedding 缓存层130x 加速
### Phase 3: MemoryExtractorAgent2-3 天)
- [ ] 创建 `~/.orca/agents/memory_extractor.md` 提示词文件
- [ ] 实现 `MemoryExtractorAgent` 结构体
- [ ] 实现 `ExtractFacts()` 批量提取逻辑
- [ ] 实现对话缓冲和批量触发机制
- [ ] 集成到 `MaintainSessionMemory()` 流程
### Phase 4: 权重进化系统1-2 天)
- [ ] 实现 `recordMemoryUsage()` 权重反馈
- [ ] 实现记忆引用检测判断 Agent 是否使用了某条记忆
- [ ] 实现自动归档逻辑权重 < 0.3
- [ ] 实现核心记忆标记权重 > 5.0
- [ ] 添加 `orca memory` CLI 命令list, stats, clean
### Phase 5: 测试与优化1-2 天)
- [ ] 单元测试:记忆检索、权重计算、事实提取
- [ ] 集成测试:端到端对话流程
- [ ] 性能测试Embedding 缓存命中率、向量检索延迟
- [ ] 调优:权重阈值、提取频率、注入策略
---
## 八、预期效果
| 指标 | 目标值 |
|------|--------|
| 记忆命中率 | > 80%(相关查询能命中有效记忆) |
| Token 消耗 | 增加 < 15%记忆注入的额外开销 |
| 长期记忆检索 | < 100ms向量搜索 + 缓存 |
| 自动学习 | 5 轮对话自动提取 2-5 个事实 |
| 记忆质量 | 人工抽查 > 90% 准确 |
| 成本增加 | Embedding API 调用 ≈ ¥0.01/千次 |
---
## 九、关键决策总结
| 决策 | 方案 | 理由 |
|------|------|------|
| 提取频率 | 每 5 轮批量提取 | 平衡实时性与 API 成本 |
| 提取模型 | 复用主 LLM可独立配置 | 降低复杂度,留优化空间 |
| 意图分类 | 规则驱动(首轮/长度/技术词) | 零 LLM 开销,可预测 |
| 预加载 | 无,按需查询 | 避免无关记忆污染上下文 |
| 聚类 | 标签 + 类型,无自动聚类 | 人工可理解,可控 |
| 反馈机制 | 引用检测 + 权重调整 | 简单有效,可解释 |
---
## 评审意见处理
| 评审意见 | 处理方式 |
|---------|---------|
| vec0 兼容性 | ✅ 已确认可用,保留原方案 |
| Agent 演化系统 | ✅ 用户明确要求保留权重系统 |
| 异步处理 | ✅ 使用 goroutine 异步调用 MemoryExtractorAgent |
| 记忆衰减 | ✅ weight 字段 + archive_threshold 实现 |
| Token 预算 | ✅ 通过 max_return 和 short_term_count 控制 |
| 降级策略 | ✅ 配置项 enabled = true/false 可完全关闭 |
| 混合检索 | ⚠️ 当前纯向量检索,后续可添加 FTS5 |
| 嵌入模型版本化 | ⚠️ 当前固定 bge-m3后续可扩展 |
---
## 附录
### A. 嵌入模型配置
**固定使用**:硅基流动 Pro/BAAI/bge-m3
- 维度1024
- 最大上下文8192
- 优势:中文优化,质量高
### B. 权重计算示例
```
初始weight = 1.0
被引用 5 次1.0 + 5*0.5 = 3.5
未被引用 3 次3.5 - 3*0.3 = 2.6
超过 5.0 → 标记为核心记忆
低于 0.3 → 归档(不删除,可恢复)
```
### C. 降级路径
```
Level 1: 向量检索 + 权重排序(正常)
Level 2: 纯 SQL 检索(向量服务故障)
Level 3: 仅短期记忆(长期记忆关闭)
Level 4: 无记忆memory.enabled = false
```