491 lines
17 KiB
Markdown
491 lines
17 KiB
Markdown
---
|
||
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: MemoryExtractorAgent(2-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)
|
||
``` |