Files
sundynix-agentix/memory_industry_analysis.md
T
Blizzard 1674252d81 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>
2026-06-22 14:27:16 +08:00

309 lines
14 KiB
Markdown
Raw 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.
# 生产级 AI Agent 长期记忆 — 行业主流方案
## 行业共识:四层记忆架构
2025 年以来,行业在长期记忆上已经形成了**四层分类共识**(模仿认知科学):
| 层 | 类型 | 内容 | 生命周期 |
|---|---|---|---|
| L1 | **工作记忆** | 当前对话的上下文窗口 | 会话内 |
| L2 | **短期/情景记忆** | 近期对话摘要、事件日志 | 数小时 ~ 数天 |
| L3 | **语义记忆** | 提炼出的事实、偏好、知识 | 持久(可衰减) |
| L4 | **程序记忆** | 学到的行为规则、约束、SOP | 持久 |
> **sundynix-agentix 当前只覆盖了 L1context window+ L2history+ L3 的最简形态(KV profile)。**
---
## 四大主流方案详解
### 1️⃣ OpenAI ChatGPT — "Dreaming" 机制
**思路**:不靠向量检索,靠**后台异步合成摘要**。
```
用户对话 → 结束后异步触发 "Dreaming"
分析历史 → 合成/更新 Memory Summary(结构化事实列表)
下次对话 → 整个 Memory Summary 注入 context window
```
**架构特点**
- **存储**:结构化事实列表(非向量),存为 user-scoped 文档
- **写入**:后台异步 "Dreaming" 进程,周期性回顾历史、合成偏好
- **读取****全量注入** context window(不做检索,依赖结构化摘要足够紧凑)
- **治理**:自动失效旧记忆("traveling to Singapore" → "went to Singapore"),用户可查看/编辑/删除
- **不用 RAG**:刻意避免向量检索的延迟和复杂度,用轻量摘要替代
> [!NOTE]
> sundynix-agentix 的方案与 ChatGPT 早期版本(2024.4 Saved Memories)非常接近——都是**逐轮 LLM 抽取 + 全量注入**。
> ChatGPT 后来演进到 Dreaming 是因为发现逐轮抽取会导致"记忆腐烂"noisy/stale),需要**周期性整理合并**。
---
### 2️⃣ Mem0 — 行业最主流的记忆中间件
**思路**:三阶段流水线 + 混合存储。被认为是当前**事实上的行业标准**。
```
对话消息 → Extract → Consolidate → Store
┌───────────┴───────────┐
Vector Store Graph Store (Mem0g)
(语义检索) (关系推理)
查询时 → Retrieve (语义相似 + 图谱关联) → 注入 prompt
```
**三阶段流水线**
| 阶段 | 做什么 | 关键区别 |
|---|---|---|
| **Extract** | LLM 从对话中抽取结构化事实和关系 | 类似 sundynix 的 `extractMemory` |
| **Consolidate** | LLM 将新事实与已有记忆**对账**,输出 ADD / UPDATE / DELETE / NOOP | ⚠️ sundynix 缺失这一层 |
| **Store** | 分别写入 Vector Store + Graph Store + KV Store | sundynix 只有 KV |
**Consolidate 是核心差异**
```
已有记忆: "用户喜欢 Vue"
新抽取: "用户说最近在学 React,打算迁移"
Mem0 Consolidate → UPDATE: "用户从 Vue 迁移到 React"
sundynix → 如果 key 不同 → 两条并存("前端框架:Vue" + "技术学习:React"
如果 key 恰好相同 → 覆盖(但依赖 LLM 用同一个 key,不可控)
```
**检索方式**
- **不是全量注入**,而是用当前 query embedding 做 top-K 语义检索
- 同时走 Graph Store 做关系关联检索("用户在 X 公司" → 关联出 "X 公司用 Go 技术栈"
- 大幅减少 token 浪费
---
### 3️⃣ Letta(原 MemGPT)— 操作系统式自管理记忆
**思路**:Agent 像操作系统一样**自己管理**自己的内存层级。
```
┌─────────────────────────────────────────┐
│ Context Window ("RAM") │
│ ┌─────────┐ ┌──────────┐ ┌─────┐ │
│ │ System │ │ Recall │ │User │ │
│ │ Prompt │ │ Memory │ │Msg │ │
│ │(可编辑) │ │(检索结果) │ │ │ │
│ └─────────┘ └──────────┘ └─────┘ │
└─────────────┬───────────────────────────┘
│ Agent 主动调用内存管理工具
┌─────────────────────────────────────────┐
│ Archival Memory ("Disk") — 向量存储 │
│ Agent 可以 search / insert / delete │
└─────────────────────────────────────────┘
```
**关键设计**
- Agent 自己决定什么时候存记忆(不是外部异步抽取)
- Agent 自己决定什么时候查记忆(通过 tool call)
- System prompt 是**可编辑的**Agent 可以修改自己的"人格"和"核心记忆"
- Archival Memory 是无限的向量存储,Agent 按需检索
**与 sundynix 的对比**sundynix 是外部驱动(orchestrator 异步抽取),Letta 是 Agent 自驱动。
---
### 4️⃣ Zep / Graphiti — 双时态知识图谱
**思路**:面向**时间敏感**场景,用图谱追踪事实的时间演变。
```
对话/文档 → Graphiti 引擎 → 三层子图:
├─ Episode 子图:原始事件(无损保留,带时间戳)
├─ Semantic Entity 子图:提取的实体和关系(语义检索)
└─ Community 子图:实体聚类(高层概览)
```
**双时态模型(Bi-temporal**
每条事实记录两个时间维度:
| 维度 | 含义 | 示例 |
|---|---|---|
| **Valid Time** | 事实在现实中为真的时间 | "2024-01 ~ 2025-03 用户在 A 公司" |
| **Ingestion Time** | 系统何时发现这个事实 | "2025-04 从对话中得知" |
```
2024-01: 用户说 "我在 A 公司做后端"
2025-03: 用户说 "我上个月从 A 离职了"
Zep 存储:
(用户) -[就职于]-> (A公司) valid: 2024-01 ~ 2025-02, invalidated_at: 2025-03
(用户) -[离职于]-> (A公司) valid: 2025-03 ~
普通 KV 存储:
公司: A公司 → 被覆盖为空,或两条矛盾并存
```
- 可以回答"用户去年这个时候在做什么?"
- 旧事实**失效但不删除**,保留完整时间线
- 企业级场景(合规、审计、保单变更)的首选
---
## 对比总览
| 维度 | sundynix 现状 | ChatGPT | Mem0 | Letta | Zep |
|---|---|---|---|---|---|
| **存储** | PG 扁平 KV | 结构化文档 | Vector + Graph + KV | Vector (Archival) | 双时态知识图谱 |
| **抽取** | 逐轮 LLM 抽取 | 后台周期 Dreaming | Extract 阶段 | Agent 自主存储 | Graphiti 引擎 |
| **去重/合并** | exact key match | Dreaming 合成 | **Consolidate (ADD/UPDATE/DELETE)** | Agent 自管理 | 双时态失效 |
| **检索** | **全量注入** | 全量注入(紧凑摘要) | **语义 top-K + 图谱关联** | Agent 主动搜索 | 语义 + 时间 + 图谱 |
| **时间维度** | ❌ | 有(自动过时) | ❌ | ❌ | ✅ 双时态 |
| **遗忘** | ❌ | ✅ 自动 | ✅ DELETE 操作 | ✅ Agent 决定 | ✅ 失效不删 |
| **用户控制** | ❌ | ✅ 查看/编辑/删除 | 有 API | 有 API | 有 API |
| **复杂度** | ★☆☆☆☆ | ★★☆☆☆ | ★★★☆☆ | ★★★★☆ | ★★★★★ |
---
## sundynix-agentix 的差距与改造路线
### 与行业标准(Mem0 模式)的 3 个关键差距
```mermaid
flowchart LR
A["1. 缺 Consolidate 层<br/>无法 ADD/UPDATE/DELETE<br/>只有盲目 upsert"] --> D["记忆腐烂<br/>冗余/过时/矛盾"]
B["2. 全量注入<br/>不做相关性检索"] --> E["token 浪费<br/>噪声干扰"]
C["3. 无时间/置信度<br/>维度"] --> F["无法遗忘<br/>无法区分强弱偏好"]
```
### 建议改造路线(利用已有基础设施)
项目已有 **Milvus + Neo4j + Postgres**,完全可以不引入新依赖,渐进实现 Mem0 级别的能力:
#### Phase 1 — 加 Consolidate 层(改 `memory_extract.go`,约 50 行)
把现在的 "抽取 → 盲目 upsert" 改为 "抽取 → LLM 对账 → 决策执行":
```go
// 现在的流程:
fresh := filterNewPrefs(parsePrefs(txt), existing) // exact match 去重
for _, p := range fresh {
o.upsertMemory(ctx, uid, p.Key, p.Value) // 盲 upsert
}
// 改为 Mem0 式 Consolidate
// 1. 把新抽取 + 已有画像一起交给 LLM
// 2. LLM 输出操作列表:[{op:"ADD/UPDATE/DELETE", key, value, reason}]
// 3. 按操作执行
```
#### Phase 2 — 按需检索代替全量注入(复用 Milvus,改 `compile.go`
```go
// 现在 (compile.go:27-31):全量注入
if rc.Profile != "" {
sys.WriteString("\n\n关于当前用户的已知信息:\n")
sys.WriteString(rc.Profile) // 全部偏好
}
// 改为:memory_get 传入 queryMilvus 语义检索 top-5 相关偏好
// memory store 存偏好时同时 embedding → Milvus memory collection
// 读取时用 query embedding → top-K → 只注入相关的
```
#### Phase 3 — 图谱记忆(复用 Neo4j)
```
memory_upsert 时同时写入 Neo4j
(User:uid) -[偏好 {key, since, confidence}]-> (Value:value)
检索时可以做关系推理:
"用户的公司" → "公司的技术栈" → 关联出相关偏好
```
> [!TIP]
> **最务实的改造优先级**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 解析 / 应用决策)可单测。