Agent记忆模块系列:01架构设计

0 阅读6分钟

Dream-SaaS 记忆模块架构复盘,ADR 全记录


0. 背景:Agent 最后缺的那块板——记忆

Dream-SaaS 多 Agent 平台开发中遇到一个很现实的问题:

代码审查 Agent 第一次审完代码,用户说"以后安全相关的多关注",第二次对话 Agent 忘了。同样的需求说了三遍。

Graph 编排解决了,RAG 解决了文档检索,但记住用户是谁、记住用户偏好这块——没有现成方案。自己写,核心三条诉求:

  1. Spring 生态原生接入,不要跨语言
  2. 按业务分层设计记忆单元,不能套固定模板
  3. HITL 机制,LLM 提取的记忆要经过确认,不能直接写进去

1. 记忆的三个层次:不是越多越好

Agent 的记忆需求其实分三层,这三层解决的问题完全不同:

┌─────────────────────────────────────────────────────────┐
│                     Agent 的记忆需求                      │
├────────────────┬──────────────────┬────────────────────┤
│   对话窗口      │   长期结构化记忆    │   语义知识记忆        │
│  ChatMemory    │  Structured KV   │  Vector Embedding  │
├────────────────┼──────────────────┼────────────────────┤
│ 最近 N 轮对话   │ 规则/事实/状态 KV  │ embedding 语义检索    │
│ 窗口滑动过期    │ 精确读写,按 ID   │ 模糊匹配,topK 返回  │
├────────────────┴──────────────────┴────────────────────┤
│         Dream-SaaS 记忆模块覆盖:L1 + L2                │
│         Spring AI ChatMemory 覆盖:L3(对话窗口)         │
└─────────────────────────────────────────────────────────┘

L1 结构化记忆(StructuredMemoryService) — 回答"用户是谁、规则是什么"

  • rules:行为准则。比如"回复用中文"、"代码审查优先看安全性"
  • facts:客观事实。比如"用户是 Java 开发者"、"正在做 Dream-SaaS 项目"
  • status:当前状态 KV。比如 current_project=dream-saasmood=tired

L2 向量语义记忆(VectorMemoryService) — 回答"之前讨论过什么相关的"

  • 语义检索:"之前讨论过 K8s 部署的内容吗?"
  • 跨对话积累:用户和 Agent 聊过多次 Spring AI,不需要每次重新说明上下文

L3 对话窗口(Spring AI ChatMemory) — 回答"最近几轮聊了什么"

  • 这是 Spring AI 自带的,不是我们做的
  • 纯消息历史,滑动窗口,超出窗口就"忘了"

2. 核心设计:双层接口 + 双套实现

dream-ai-agent-memory/
├── dream-ai-agent-memory-api        ← 接口层,对业务透明
│   ├── StructuredMemoryService       ← L1 接口
│   ├── VectorMemoryService           ← L2 接口
│   ├── MemorySnapshot               ← 返回模型(不可变快照)
│   └── MemoryPendingItem             ← HITL 待确认条目
├── dream-ai-agent-memory-pgvector   ← 生产实现(推荐)
│   ├── PgStructuredMemoryService    ← L1 PG 持久化
│   ├── PgVectorMemoryService         ← L2 pgvector 向量检索
│   ├── MemoryExtractService          ← LLM 自动提取 + HITL
│   └── metrics/MemoryMetrics         ← Micrometer 监控
├── dream-ai-agent-memory-redis      ← 降级实现(热缓存场景)
│   ├── RedisStructuredMemoryService ← L1 Redis(TTL 自动过期)
│   └── RedisVectorMemoryService     ← L2 降级(无真正向量)
└── dream-ai-agent-memory-mcp        ← MCP 工具暴露层
    └── MemoryMcpContributor         ← 暴露为 MCP 工具(read/search/store)

为什么分层

接口与实现分离,业务代码永远只依赖 StructuredMemoryService / VectorMemoryService 两个接口。切换存储引擎只需要改一行配置:

# 切 PGvector
ai.memory.pgvector.enabled: true

# 切 Redis 降级
ai.memory.pgvector.enabled: false
ai.memory.redis.enabled: true

@ConditionalOnProperty 自动装配,零代码改动。


3. L1 StructuredMemoryService:三种单元的 CRUD

记忆模型:MemorySnapshot

get(ownerId) 返回一个不可变快照:

MemorySnapshot snapshot = structuredMemory.get("author");
// snapshot.rules    → List<MemoryRule>
// snapshot.facts    → List<MemoryFact>
// snapshot.status  → Map<String, String>
// snapshot.pending → List<MemoryPendingItem>  // 待确认

注入 Agent 的方式很简单:

String systemPrompt = """
    你是一个 AI 搭子,用户信息如下:
    规则:%s
    事实:%s
    当前状态:%s
    """.formatted(
    snapshot.ruleContents(),
    snapshot.factContents(),
    snapshot.status()
);

三种单元的定位差异

单元语义典型场景修改频率
rules用户明确要求的行为准则"回复时先说结论再说原因"
facts关于用户/环境的客观事实"用户是 Java 开发者"
status当前工作的上下文状态project=dream-saas

HITL 待确认机制

LLM 自动提取记忆时,不是直接写入,而是走 HITL(Human-in-the-Loop)三色阈值:

置信度 ≥ 0.82  → 直接写入(高置信度)
置信度 ≥ 0.55  → 入队待用户确认(中等置信度)
置信度 < 0.55  → 跳过(低置信度不写入)

为什么这样设计?防幻觉。LLM 有时会"创造"用户没说过的事实,不经过确认就写入会影响 Agent 长期行为。


4. L2 VectorMemoryService:混合检索的三信号融合

为什么需要混合检索

纯向量检索有个问题:语义相关但用词不同可能漏掉(比如用户说"部署"而记忆中写的是"上线"),纯关键词检索则无法处理同义词扩展。解决方案:三个信号加权融合

hybrid_score = 0.6 × vector_score + 0.3 × keyword_score + 0.1 × time_score
信号来源权重解决什么问题
vector_scorepgvector cosine 相似度0.6语义理解("K8s"≈"Kubernetes")
keyword_scoreILIKE 关键词匹配率0.3精确召回("部署"必须匹配"部署")
time_score1/(1+days/30) 指数衰减0.1新记忆优先(防止古老记忆反复出现)

权重为什么这么定?0.6/0.3/0.1 是经验值,语义权重最高但不能独占(否则关键词退化严重),时间权重最低(只是微调,不喧宾夺主)。

去重策略

store 时检测相似记忆,cosine > 0.85 即认为是"重复":

store(content)
  → 生成 embedding
  → cosine > 0.85 → 合并(更新 content + 合并 metadata,保留原有 embedding)
  → cosine ≤ 0.85 → 新增

合并时 merged_frommerged_at 记录到 metadata,可追溯。


5. 核心设计决策

ADR-001:分层架构(接口与实现分离)

决策:接口层独立于实现层,业务代码只依赖接口。

理由:L1/L2 接口不变,实现可以在 Redis / PGvector / MCP 之间切换,不影响上层 Agent 代码。

放弃:无。要做多 Agent 共享记忆,存储灵活性是必须的。


ADR-002:选择 PGvector 而非 Milvus

决策:向量存储选 PostgreSQL + pgvector,不单独部署 Milvus。

理由

  • Dream-SaaS 已有 PostgreSQL,复用现有基础设施
  • pgvector 在 PG 15+ 性能已足够(1000条记忆检索 < 50ms)
  • 无额外运维成本

放弃:Milvus 的分布式向量检索能力。对于几千条记忆的场景,pgvector 完全够用。


ADR-003:混合检索三信号融合

决策hybrid_score = 0.6×vector + 0.3×keyword + 0.1×time

理由:纯向量在专有名词/产品名场景召回率差,纯关键词无法处理同义词扩展。三信号互补。

放弃:纯向量检索(简单但不够精准)。


ADR-004:HITL 置信度分层

决策:≥0.82 自动写入,≥0.55 待确认,<0.55 跳过。

理由:防止 LLM 幻觉导致错误记忆永久化。用户确认为记忆的参与感,也能提升用户对 Agent 的信任。

放弃:直接写入(省事但风险高)。纯人工确认(体验差)。


ADR-005:Embedding 可选注入

决策EmbeddingModel 有就注入,没有就走 ILIKE 降级。

理由:降低接入门槛。开发/测试环境不一定有可用模型,降级到 ILIKE 至少能跑通。

放弃:强制要求 EmbeddingModel(接入成本高)。


6. 篇2 预告

下篇讲双层实现的源码拆解:PgStructuredMemoryService 三种单元的 PG 存储、PgVectorMemoryService 混合检索三信号融合完整调用链、Redis 降级方案的读写模式,以及测试兼容的坑。