date, topic, status
| date |
topic |
status |
| 2025-05-11 |
Orca 完整记忆系统 + Agent 进化方案 |
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)
# ========== 主 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
# 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 消息处理流程(动态注入)
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 记忆维护流程(批量提取)
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 权重反馈流程(自动进化)
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()
}
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 天)
Phase 2: 记忆注入层(1-2 天)
Phase 4: 权重进化系统(1-2 天)
Phase 5: 测试与优化(1-2 天)
八、预期效果
| 指标 |
目标值 |
| 记忆命中率 |
> 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)