feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen

把"逐轮盲写抽取"升级为 Mem0 式对账(方案见 memory_industry_analysis.md 落地节):

mcp-go:
- Profile 加 Importance(1~10, poignancy) + LastSeenAt(为 Generative Agents 读路径
  Score=w1·Relevance+w2·Recency+w3·Importance 铺路)。
- Upsert 收 importance + 每次置 last_seen(印证);新增 Delete(软删,BaseModel.DeletedAt
  已具备,失效不物删可审计)+ Touch;memory_upsert 透传 importance、新增 memory_delete 工具。

dispatcher:
- extractMemory → consolidateMemory:一次 LLM 调用同时做 抽取+对账,输出
  [{op:ADD|UPDATE|DELETE|NOOP,key,value,importance}];ADD/UPDATE→upsert、DELETE→软删;
  sanitizeOps 防幻删(DELETE 须命中已有)/夹 importance[1,10]/同key保末个/丢 NOOP。
- 攒批:每 3 轮(per-session 计数)才 consolidate 一次,省成本,对齐 ChatGPT 周期整理。
  从根上解决 exact-key 盲写的记忆腐烂。

验证:parseOps/sanitizeOps/parseProfile 纯逻辑单测;store 集成测试(真 PG)覆盖
importance/last_seen 写入 + 软删(live 0 / 物理 1);dispatcher -race 全过。
(注:完整多轮 LLM consolidate 未做实跑,属构造性验证 + 沿用已证 pool.Chat 模式。)

P2 待做:读路径按 Score(Recency+Importance) 排序/衰减/截断 + 桌面端记忆面板。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-22 14:27:16 +08:00
parent b06c768f11
commit 1674252d81
8 changed files with 352 additions and 98 deletions
+80
View File
@@ -226,3 +226,83 @@ memory_upsert 时同时写入 Neo4j
> **最务实的改造优先级**Phase 1Consolidate> Phase 2(语义检索)> Phase 3(图谱)。
> Phase 1 改动最小(只改 `memory_extract.go` 的抽取 prompt 和处理逻辑),
> 但效果最显著——直接解决记忆腐烂这个最大的 Day-2 问题。
---
# 落地方案(修正版 · 实施计划)
> 在上面分析基础上做了三处修正:**① Consolidate 改异步攒批(不逐轮,省成本);② DELETE 用 soft-delete(失效不物删);③ Phase 2 语义检索暂缓(当前画像小,全量注入够用)**。并把读路径目标定为 Generative Agents 检索打分公式。
## 设计四原则
1. **对账而非盲写** —— 引入 ConsolidateADD / UPDATE / DELETE / NOOP),解决 exact-key 去重不可控导致的记忆腐烂。
2. **异步攒批而非逐轮** —— 每 N 轮跑一次"抽取+对账",一次 LLM 调用,省成本,对齐 ChatGPT 周期整理。
3. **失效不物删** —— DELETE = GORM 软删(`BaseModel.DeletedAt` 已具备,零成本),白拿审计/可恢复。
4. **按当下规模右尺寸** —— 小画像继续全量注入;语义检索/图谱留接口、暂不做。
## 数据模型(扩 mcp-go 的 `Profile`
```go
type Profile struct {
BaseModel // 已含 id/created/updated/deleted_at(软删)
UserID string
Key string // (user_id,key) 唯一
Value string
Importance float64 // 1~10consolidate 时 LLM 打分(poignancy)→ 读路径权重
Confidence float64 // 0~1 信念强度(可选,强弱偏好)
Source string // "user"(显式) / "extracted"(推断)
LastSeenAt time.Time // 最近被印证时间 → Recency 衰减依据
}
```
## 写路径:异步攒批 Consolidate(核心)
```
每轮结束 → 只 append history(已有,便宜)
↓ 每 N 轮(用 history 长度判断,无需新基建)
Consolidate1 次 LLM 调用,extract + reconcile 合一,Mem0 式):
输入 = 近 K 轮对话 + 当前 active 画像(带 key/importance)
输出 = [{op: ADD|UPDATE|DELETE|NOOP, key, value, importance, reason}]
↓ 按 op 执行
ADD/UPDATE → memory_upsert(带 importance, last_seen=now)
DELETE → memory_delete(软删)
NOOP → bump last_seen(印证即强化)
```
- 决策(LLM 对账)在 dispatcher;落库在 mcp-go 工具(新增 `memory_delete` 软删 + `memory_upsert` 收 importance)。
- 触发:`memorize` 里 append 后看 `history_get` 长度,跨 N 轮才跑。
## 读路径目标 ★ Generative Agents 检索打分公式
最终读路径按此公式排序、取 top-K(论文 Park et al. 2023):
```
Score = w1·Relevance + w2·Recency + w3·Importance
```
| 项 | 取值 | 本项目映射 | 何时上 |
|---|---|---|---|
| **Importance** | 写入时 LLM 打 1~10(存下,不在检索时算)| `Profile.Importance`consolidate 顺手打分 | **P1 就存** |
| **Recency** | `decay^(距 last_seen)` 指数衰减 | `LastSeenAt` + 衰减 | **P2** |
| **Relevance** | query×memory 余弦相似 | 需 embedding → Milvus | **P3(画像大才上)** |
- 三项各 min-max 归一到 [0,1],加权求和;**w1/w2/w3 设为可配**。
- 现在画像小、全量注入:先用 **Recency + Importance** 两项排序/截断/遗忘(无向量、便宜);画像变大后补 Relevance 凑齐三项。
- `memory_get(user_id, query?)` 签名预留 `query`,将来接 Milvus 不破坏调用方。
## 分阶段
| 阶段 | 内容 | 解决 |
|---|---|---|
| **P1** | 异步攒批 ConsolidateADD/UPDATE/DELETE/NOOP+ soft-delete + Profile 加 Importance/LastSeenAtconsolidate 时 LLM 打 importance 分)| 记忆腐烂 + 成本 |
| **P2** | 读路径:按 Score(Recency+Importance) 排序 + 衰减遗忘 + 截断 top-N;桌面端记忆面板可看/改/删 | 噪声 + 遗忘 + 用户控制 |
| **P3(暂不做)** | 补 Relevancememory 存时 embedding→Milvus,读时 query top-K,凑齐三项公式;Neo4j 图谱记忆 | 大规模 / 关系推理 |
**故意不做**:双时态完整模型(soft-delete + last_seen 已够)、Letta 式 Agent 自管理(与现有外部编排架构不合)。
## P1 改动点(下午动手)
- `mcp-go/internal/memory/store.go``Profile``Importance/LastSeenAt``Upsert` 收 importance + 置 last_seen;新增 `Delete`(软删)。
- `mcp-go/internal/mcp/gateway.go`:新增 `memory_delete` 工具;`memory_upsert` 透传 importance。
- `dispatcher/internal/eino/memory_extract.go``extractMemory``consolidateMemory`:合并 extract+reconcile 为一次 LLM 调用,输出 op 列表(含 importance 打分),按 op 调 upsert/delete;触发改为每 N 轮(看 history 长度)。
- 纯逻辑(op 解析 / 应用决策)可单测。