--- 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) ```