1674252d81
把"逐轮盲写抽取"升级为 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>
309 lines
14 KiB
Markdown
309 lines
14 KiB
Markdown
# 生产级 AI Agent 长期记忆 — 行业主流方案
|
||
|
||
## 行业共识:四层记忆架构
|
||
|
||
2025 年以来,行业在长期记忆上已经形成了**四层分类共识**(模仿认知科学):
|
||
|
||
| 层 | 类型 | 内容 | 生命周期 |
|
||
|---|---|---|---|
|
||
| L1 | **工作记忆** | 当前对话的上下文窗口 | 会话内 |
|
||
| L2 | **短期/情景记忆** | 近期对话摘要、事件日志 | 数小时 ~ 数天 |
|
||
| L3 | **语义记忆** | 提炼出的事实、偏好、知识 | 持久(可衰减) |
|
||
| L4 | **程序记忆** | 学到的行为规则、约束、SOP | 持久 |
|
||
|
||
> **sundynix-agentix 当前只覆盖了 L1(context window)+ L2(history)+ 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 传入 query,Milvus 语义检索 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 1(Consolidate)> Phase 2(语义检索)> Phase 3(图谱)。
|
||
> Phase 1 改动最小(只改 `memory_extract.go` 的抽取 prompt 和处理逻辑),
|
||
> 但效果最显著——直接解决记忆腐烂这个最大的 Day-2 问题。
|
||
|
||
---
|
||
|
||
# 落地方案(修正版 · 实施计划)
|
||
|
||
> 在上面分析基础上做了三处修正:**① Consolidate 改异步攒批(不逐轮,省成本);② DELETE 用 soft-delete(失效不物删);③ Phase 2 语义检索暂缓(当前画像小,全量注入够用)**。并把读路径目标定为 Generative Agents 检索打分公式。
|
||
|
||
## 设计四原则
|
||
|
||
1. **对账而非盲写** —— 引入 Consolidate(ADD / 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~10,consolidate 时 LLM 打分(poignancy)→ 读路径权重
|
||
Confidence float64 // 0~1 信念强度(可选,强弱偏好)
|
||
Source string // "user"(显式) / "extracted"(推断)
|
||
LastSeenAt time.Time // 最近被印证时间 → Recency 衰减依据
|
||
}
|
||
```
|
||
|
||
## 写路径:异步攒批 Consolidate(核心)
|
||
|
||
```
|
||
每轮结束 → 只 append history(已有,便宜)
|
||
↓ 每 N 轮(用 history 长度判断,无需新基建)
|
||
Consolidate(1 次 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** | 异步攒批 Consolidate(ADD/UPDATE/DELETE/NOOP)+ soft-delete + Profile 加 Importance/LastSeenAt(consolidate 时 LLM 打 importance 分)| 记忆腐烂 + 成本 |
|
||
| **P2** | 读路径:按 Score(Recency+Importance) 排序 + 衰减遗忘 + 截断 top-N;桌面端记忆面板可看/改/删 | 噪声 + 遗忘 + 用户控制 |
|
||
| **P3(暂不做)** | 补 Relevance:memory 存时 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 解析 / 应用决策)可单测。
|