一文讲清 Agent 的记忆系统:设计思路与工程实现

0 阅读20分钟

一、方案目标

这套方案要解决 4 个核心问题:

  1. 当前会话不丢上下文
  2. 当前任务能够连续推进
  3. 跨会话能够找回长期经验和稳定偏好
  4. 不同层记忆不打架、不重复、不污染 prompt

这不是一套“把所有内容都塞进一个存储”的方案,而是一套分层记忆体系。每一层只负责一种时间尺度、一种检索问题、一种注入目的。

核心原则:

  • 原始历史和可注入记忆分开
  • 当前状态和长期知识分开
  • 权威结构化信息和语义召回信息分开
  • 存储决策由系统层控制,不由 agent 自由落库
  • 检索与注入分开,召回到的内容必须经过去重、排序和压缩后才能进上下文

二、整体分层

说明

下面的“第 0 层、第 1 层、第 2 层、第 3 层”是逻辑分层编号,不是运行时的固定访问顺序。

运行时是否访问某一层,由路由规则决定。默认逻辑优先级通常可以理解为:

PG -> Redis -> LTM

但这表示的是默认判断顺序和职责优先级,不表示所有实现都必须严格串行执行。某些场景可以跳过某层,某些场景可以并行查。


第 0 层:会话原始历史层

存储介质

  • 本地文件
  • 对象存储
  • append-only log

存什么

  • user 原始输入
  • assistant 原始输出
  • tool 原始结果
  • 系统事件

作用

  • 回放
  • 审计
  • debug
  • 必要时重新抽取记忆
  • 作为低频 fallback 回源材料

注意 这一层不是短期记忆层。它是 raw transcript,不直接作为主检索源,也不应该直接整段塞进 prompt。

什么时候会用到它

  • Redis / PG / 长期记忆结果之间出现冲突时
  • 某条短期记忆的来源需要核验时
  • 怀疑某条长期记忆晋升错误,需要重建时
  • 需要重新抽取结构化事实时
  • 会话压缩后,想回看更早原始过程时

第 1 层:PG 结构化权威层

存储介质

  • PostgreSQL

存什么

  • 用户画像
  • 用户长期偏好
  • 权限、角色
  • tenant / project / workspace 元数据
  • agent 配置
  • 显式保存的稳定结构化信息
  • 从短期记忆中晋升出的稳定结构化事实

作用

  • 权威结构化事实源
  • 精确查询
  • 给其他检索层提供 filter 条件
  • 初始化运行时元数据
  • 作为 profile / constraints / config 类信息的最终准据

不负责什么

  • 不负责当前任务的临时状态
  • 不负责语义相似案例召回
  • 不负责高频工作记忆
  • 不负责保存原始聊天历史

典型表

  • users
  • user_profiles
  • user_preferences
  • projects
  • workspaces
  • user_project_roles
  • agent_settings
  • stable_memory_facts

第 2 层:Redis 短期工作记忆层

存储介质

  • Redis

存什么

  • 当前任务的关键事实
  • 当前会话的稳定事实
  • 已确认结论
  • 已排除项
  • 当前决策
  • 用户当前约束
  • 已验证 observation
  • 短期偏好

作用

  • 连续几轮会话续接
  • debug / repair / verify 场景的上下文补全
  • 快速、低延迟、高精度的工作记忆召回
  • 作为当前任务状态的主要检索层

本质 它是 working memory / short-term memory,不是聊天记录库。

它解决的问题

  • 当前这件事做到哪了
  • 哪些原因已经排除
  • 当前重点是什么
  • 当前约束是什么
  • 下一步该做什么

第 3 层:长期记忆检索层

存储介质

  • Elasticsearch
  • Milvus

存什么

  • 跨会话仍有价值的长期记忆卡片
  • 类似案例
  • 通用经验
  • 长期用户偏好
  • 文档型知识摘要
  • 从 STM 晋升后的长期知识

作用

  • 语义相似召回
  • 历史案例召回
  • 关键词/BM25 检索
  • 混合检索
  • 远距离知识联想

本质 它是 long-term semantic memory / archival memory,不是当前状态层。

注意 这里建议把 ES + Milvus 看成一个统一的 LTM 子系统:

  • ES 负责关键词、过滤、正文返回
  • Milvus 负责语义相似召回
  • 二者共同产出长期记忆候选

三、每层职责边界

PG 回答的问题

  • 这个用户是谁
  • 有什么权限
  • 默认配置是什么
  • 当前 workspace / project 是什么
  • 用户长期偏好是什么
  • 当前租户或项目有哪些稳定约束

Redis 回答的问题

  • 当前这件事进行到哪了
  • 哪些原因已经排除
  • 当前约束是什么
  • 当前下一步做什么
  • 这个会话中已经确认过什么

LTM 回答的问题

  • 以前有没有类似问题
  • 有没有相似案例
  • 有没有可复用经验
  • 这个模糊表达可能关联什么历史知识
  • 跨会话是否有相关长期信息

原始历史层回答的问题

  • 当时原始输入和工具结果到底是什么
  • 某条记忆最初是怎么来的
  • 当前结构化结论是否可追溯

四、Redis 里到底怎么存

1. 内容本体

每条短期记忆存成一个独立对象,例如:

memory_item_{id}

可以用 Redis Hash 或 RedisJSON。

建议在 v1 里优先选择一种统一格式,不要混用。若内容结构经常变化,优先 RedisJSON;若追求极简和性能,优先 Redis Hash。

推荐字段

{
  "id": "m_1001",
  "user_id": "u_42",
  "session_id": "s_123",
  "task_id": "bug_456",
  "memory_type": "fact",
  "subtype": "diagnosis",
  "summary": "createOrder 在 prod-k8s 超时,数据库已排除",
  "detail": {
    "symptom": "timeout",
    "service": "payment-service",
    "api": "createOrder",
    "env": "prod-k8s",
    "excluded_root_causes": ["database"],
    "next_focus": ["thread_pool", "downstream_latency"]
  },
  "entities": ["createOrder", "payment-service", "database", "prod-k8s"],
  "tags": ["timeout", "diagnosis", "prod"],
  "source": {
    "kind": "conversation+tool",
    "message_ids": ["msg_91", "msg_94"],
    "tool_call_ids": ["tool_18"]
  },
  "confidence": 0.92,
  "importance": 0.84,
  "created_at": 1713861000,
  "updated_at": 1713861100,
  "last_access_at": 1713861200,
  "expires_at": 1713947400,
  "fingerprint": "hash(user+task+subtype+normalized_fact)",
  "canonical_fact_key": "bug456:createOrder:db_excluded",
  "version": 3,
  "status": "active"
}

2. 字段解释

  • id:记忆条目 ID
  • user_id:用户 ID
  • session_id:会话 ID
  • task_id:当前任务 ID,没有任务时可为空
  • memory_type:主类别
  • subtype:更细的子类别
  • summary:给模型看的短摘要
  • detail:结构化内容,给程序和压缩器用
  • entities:实体列表,用于索引和打分
  • tags:主题标签,用于筛选和统计
  • source:来源追踪,便于追溯
  • confidence:这条记忆是否可靠
  • importance:这条记忆在当前任务中的重要性
  • created_at / updated_at / last_access_at:时间信息
  • expires_at:条目级生命周期
  • fingerprint:近重复去重键
  • canonical_fact_key:同一事实的规范键
  • version:upsert 更新版本
  • status:活动状态,例如 active / superseded / deleted

3. memory_type 建议

至少分这几类:

  • fact:稳定事实
  • decision:当前决策
  • hypothesis:当前假设
  • observation:工具或日志观测
  • constraint:用户或环境约束
  • preference:短期用户偏好

4. 可选字段

如果是多 agent 协作系统,可以额外带:

  • agent_id
  • owner_agent
  • shared_scope

这些不是 v1 的必要字段,单 agent 系统可以不存。

5. Redis 里不该存什么

  • 原始聊天全文
  • 大段日志全文
  • 模型中间推理
  • 未验证猜测
  • 明显重复内容
  • 与当前 task / session 无关的一次性碎片
  • 暂时没有结构化价值的原始文本搬运

五、zset 里存什么

原则:zset 只存 ID,不存内容本体。

1. 最近更新索引

memory:user:{userId}:recent
  • member = memory_id
  • score = updated_at

用途

  • 最近记忆兜底召回
  • 用户续接刚才任务时快速回捞

2. session 索引

memory:user:{userId}:session:{sessionId}
  • member = memory_id
  • score = updated_at

用途

  • 当前会话续接
  • 代词和上下文依赖输入的快速召回

3. task 索引

memory:user:{userId}:task:{taskId}
  • member = memory_id
  • score = updated_at

用途

  • 多步任务
  • debug / repair / verify 场景
  • 当前任务上下文召回的主索引

4. 过期索引

memory:user:{userId}:expire
  • member = memory_id
  • score = expires_at

用途

  • 定时扫描和逐条清理

5. entity 索引

memory:user:{userId}:entity:{entity}
  • member = memory_id
  • score = updated_atimportance

用途

  • 按接口名、服务名、环境名、工单号等实体快速召回

6. type 索引(可选增强)

memory:user:{userId}:type:{memory_type}
  • member = memory_id
  • score = updated_at

用途

  • 只查特定类型记忆,例如 constraint / decision

注意 这不是 v1 必需项,而是可选增强索引。v1 核心索引是 recent / session / task / expire / entity。


六、PG 扮演什么角色

PG 不是又一个记忆库,而是权威结构化事实层

1. Source of Truth

存稳定、明确 schema 的信息:

  • 用户画像
  • 用户长期偏好
  • 角色、权限
  • project / tenant / workspace 元数据
  • agent 配置
  • 显式长期规则
  • 稳定结构化约束

2. 检索过滤器来源

PG 经常不是直接给模型看,而是给检索器提供 filter:

  • tenant_id
  • project_id
  • workspace_id
  • role
  • permission scope
  • language
  • 当前环境标签

3. 稳定信息归档层

有些信息一开始来自 Redis STM,后来被证明是稳定事实,就晋升到 PG:

  • 用户长期偏好
  • 稳定配置
  • 长期适用的结构化约束
  • 明确的项目级静态事实

4. PG 查出来后怎么用

PG 的结果通常不进入普通 candidate pool,而是组装成:

  • profile_block
  • constraints_block
  • runtime_metadata_block

例如:

{
  "language": "zh-CN",
  "answer_style": "brief_first",
  "permission": "read_only",
  "project_default_env": "prod-cn"
}

5. PG 与其他层冲突时的规则

PG 通常不参与 Redis / LTM 的普通排序式去重,但如果出现冲突:

  • profile.*
  • permission.*
  • config.*
  • project_metadata.*

这些域默认以 PG 为权威来源。


七、ES + Milvus 混合检索里各存什么

1. 总体原则

ES 和 Milvus 不是两套相互独立的长期记忆,而是同一条长期记忆卡片的双索引系统。

同一条长期记忆卡片:

  • 在 ES 中用于关键词检索、过滤和正文返回
  • 在 Milvus 中用于向量相似召回
  • 通过同一个 memory_id 汇合

2. ES 存什么

ES 存长期记忆卡片的正文和结构化过滤信息:

{
  "memory_id": "ltm_9001",
  "user_id": "u_42",
  "tenant_id": "t_1",
  "project_id": "p_payment",
  "memory_type": "semantic_fact",
  "title": "createOrder timeout 常见排查经验",
  "summary": "历史上多次出现 createOrder timeout,数据库正常时常见根因是线程池饱和",
  "content": "完整的长期记忆正文或经验卡片内容",
  "entities": ["createOrder", "database", "thread_pool", "payment-service"],
  "tags": ["timeout", "case", "diagnosis"],
  "source_type": "promoted_from_stm",
  "bm25_text": "给 BM25/关键词检索用的归一化文本",
  "created_at": 1713861000,
  "updated_at": 1713861100,
  "importance": 0.88,
  "confidence": 0.91,
  "access_count": 7,
  "status": "active"
}

ES 的职责

  • 关键词/BM25 检索
  • 结构化过滤
  • 返回可读正文
  • 作为混合检索中的 lexical 分支

3. Milvus 存什么

Milvus 存同一条长期记忆卡片的向量表示和少量 metadata:

{
  "id": "ltm_9001",
  "vector": [0.12, -0.08, 0.44],
  "user_id": "u_42",
  "tenant_id": "t_1",
  "project_id": "p_payment",
  "memory_type": "semantic_fact",
  "importance": 0.88
}

Milvus 的职责

  • 语义相似召回
  • 向量近邻搜索
  • 产出同一 memory_id 的候选集合

4. LTM 子系统内部怎么工作

当路由层决定“需要查长期记忆”时,不一定等于只查向量。

更准确的做法是:

  • 先判断是否需要长期记忆子系统 need_ltm
  • 然后在 LTM 内部再决定:
    • 只查 ES
    • 只查 Milvus
    • ES 和 Milvus 都查后做融合

典型情况

  • 用户明确提到实体、错误码、接口名 -> ES 更重要
  • 用户表达模糊,想找相似案例 -> Milvus 更重要
  • 又要关键词准确,又要语义泛化 -> ES + Milvus 混合查

八、什么时候查哪一层

不是每次都查三层,而是先做 query-profile,再按规则路由。

1. 什么是 query-profile

query-profile 是把用户输入转成程序可判定的结构化查询画像。它不是必须由 LLM 生成,v1 完全可以通过规则、词典和运行时状态抽取。

例如:

{
  "intent": "debug_continue",
  "has_reference": true,
  "has_profile_request": false,
  "has_similarity_request": false,
  "entities": ["createOrder", "database"],
  "active_task_id": "bug456",
  "session_recent": true,
  "topic_shift": false,
  "needs_exact_metadata": false,
  "recent_entity_overlap": 2,
  "profile_cache_missing": false,
  "profile_cache_stale": false,
  "redis_result_insufficient": false
}

2. 它怎么来

通过三类特征抽取:

文本规则

  • 指代词:这个/那个/刚才/继续/还是/第二个
  • 画像词:偏好/习惯/权限/角色/默认/配置
  • 相似词:类似/以前/历史/案例/经验
  • 调试词:报错/超时/排查/修复/验证

实体抽取

  • 词典匹配
  • 接口名 / 服务名 / 工单号 / 环境名正则
  • 最近 session 实体表匹配

运行时状态

  • 是否有 active_task_id
  • 当前 session 是否刚活跃过
  • profile cache 是否缺失或过期
  • 最近 session 实体有哪些
  • 上一轮 Redis 召回是否不足
  • 当前是否明显发生 topic shift

3. 为什么先用规则而不是 LLM

v1 用纯规则更容易落地,也更稳定:

  • 可解释
  • 易调试
  • 延迟低
  • 成本低
  • 行为稳定

只有规则误判很多、业务规模变大后,才考虑引入轻量分类器。默认不需要先上 LLM。


九、怎么判断查 PG / Redis / LTM

1. PG

适合查 PG 的场景

  • 问画像 / 偏好 / 权限 / 配置
  • session 初始化或恢复
  • 检索前需要 filter 条件
  • PG 缓存缺失或过期
  • 需要精确结构化值,而不是语义近似

规则示例

need_pg = (
    profile_cache_missing
    or profile_cache_stale
    or intent in {"profile_query", "permission_query", "config_query"}
    or needs_exact_metadata
)

2. Redis

适合查 Redis 的场景

  • 正在续当前任务
  • 输入依赖当前上下文
  • 当前正在 debug / verify / fix
  • 命中最近 session / task 实体

规则示例

need_redis = (
    has_reference
    or (active_task_id is not None and intent in {"continue_task", "debug_continue", "verify_current_state"})
    or recent_entity_overlap > 0
)

为什么 Redis 优先于 LTM

  • 更贴合当前任务
  • 延迟更低
  • 结果噪音更小
  • 召回语义更偏“当前状态”而不是“历史相似”

3. LTM

适合查 LTM 的场景

  • 用户问类似历史 / 相似案例
  • 表达模糊但明显在找旧经验
  • Redis 结果不足
  • 新会话但可能和过去任务相关

规则示例

need_ltm = (
    has_similarity_request
    or fuzzy_history_reference
    or redis_result_insufficient
)

4. 是否必须串行

不是。

更准确地说:

  • 逻辑上通常先判断 PG、再判断 Redis、再判断 LTM
  • 但实现上不一定严格串行
  • 例如 PG profile 已缓存时,可以直接查 Redis
  • 某些明确需要类似案例的场景,也可以在 Redis 的同时触发 LTM

十、查询优先级和合并方式

1. 运行时总体原则

  • 先路由,决定查不查
  • 查的话,PG 产出 metadata block
  • Redis 和 LTM 产出 memory candidates
  • memory candidates 再统一去重、排序、压缩

2. PG 的处理方式

PG 结果通常不进普通 candidate pool,而是直接组装为:

  • profile_block
  • constraints_block
  • runtime_metadata_block

3. Redis + LTM 的处理方式

Redis 和 LTM 结果进入统一候选池:

  1. 合并
  2. 去重
  3. 重打分
  4. 取 topK
  5. 压缩成 memory summary
  6. 注入 prompt

4. 最终注入结构建议

用户/环境画像:
- 语言:中文
- 回答风格:先结论后细节
- 权限:只读
- 默认环境:prod-cn

当前任务短期记忆:
- 当前任务:bug456
- 问题:createOrder 在 prod-k8s 超时
- 已排除:数据库
- 当前重点:线程池、下游延迟

相关长期经验:
- 历史上有一次类似超时案例,根因是应用线程池饱和

十一、什么时候写入 Redis 短期记忆

1. 适合写入的时机

适合写入 Redis STM 的时刻:

  1. 工具结果出来后
  2. 当前任务状态发生变化时
  3. 用户给出稳定约束时
  4. 回合结束时做一次轻量候选抽取

2. 适合写入的内容

  • 已验证 observation / fact
  • 当前任务的重要 decision
  • 当前任务的 constraint
  • 当前会话的稳定结论
  • 高置信的状态更新
  • 与已有内容相比有明显新信息的事实

3. 不该写入 Redis 的内容

  • 闲聊
  • 原始长日志全文
  • 模型中间推理
  • 一次性碎片信息
  • 没验证的猜测
  • 重复事实
  • 低置信、低价值、短时即失效的原始噪音

十二、谁来判断存不存

不是 agent 自己决定,也不是异步子 agent 最终裁决。

最稳的分工是:

  1. 异步子 agent / 抽取器:从对话和 tool result 里提取候选事实
  2. system memory manager / policy engine:判断值不值得写、写哪层、给什么 TTL
  3. 规则去重 / upsert 层:按 fingerprint 和 canonical_fact_key 合并
  4. 存储层:写 Redis,必要时异步晋升到 PG 或 LTM

一句话:

  • 子 agent 负责提议
  • 规则引擎负责裁决
  • 去重器负责落库前合并

十三、写入规则怎么判

推荐为每条候选事实构建一个 write_profile

{
  "is_verified": true,
  "is_stable_for_current_task": true,
  "is_user_constraint": false,
  "is_decision": true,
  "is_preference": false,
  "novelty": 0.8,
  "confidence": 0.92,
  "estimated_ttl_minutes": 180,
  "duplicate": false,
  "transient": false,
  "raw_unstructured_blob": false
}

1. 适合写 Redis 的条件

  • 已验证 observation / fact
  • 当前任务的重要 decision
  • 当前任务的 constraint
  • 高置信的稳定状态
  • 相比已有内容有新信息

2. 不适合写 Redis 的条件

  • 低置信
  • 重复
  • 太短命
  • 只是原始文本搬运
  • 无法结构化
  • 与当前 task / session 无关

3. 一个简单打分

write_score =
  3 * is_verified
+ 3 * is_user_constraint
+ 2 * is_decision
+ 2 * is_stable_for_current_task
+ 2 * novelty_high
+ 1 * confidence_high
- 3 * duplicate
- 2 * transient
- 2 * raw_unstructured_blob

write_score >= 3 就写 Redis。


十四、什么时候从 Redis 晋升到 PG 或 LTM

不要自动把所有 Redis 都同步到长期层。晋升必须分为两条不同路径:

路径 A:晋升为稳定结构化事实 -> PG

适合进入 PG 的内容:

  • 用户长期偏好
  • 稳定角色和权限信息
  • 稳定项目配置
  • 长期适用的结构化约束
  • 稳定画像信息

路径 B:晋升为长期检索知识卡片 -> LTM

适合进入 ES + Milvus 的内容:

  • 可复用经验
  • 相似案例总结
  • 通用排障模式
  • 长期有价值的知识卡片
  • 跨会话仍值得召回的经验摘要

晋升的通用触发条件

只有满足下面条件才考虑晋升:

  • 被多次命中 / 复用
  • 跨多个 session 仍然出现
  • 用户显式说“记住这个”
  • 属于长期偏好 / 稳定画像
  • 是通用经验,不只是当前任务临时状态
  • 存活超过阈值

适合晋升的例子

  • 用户偏好先结论后细节
  • 某类 timeout 问题常见根因是线程池
  • 某项目默认环境是 prod-cn
  • 某用户长期只有只读权限

不适合晋升的例子

  • 当前 bug456 下一步先看线程池
  • 刚才那次日志输出
  • 当前会话的临时排查路径

特别注意

STM 和长期记忆之间最大的风险,不是漏召回,而是错误晋升导致长期记忆污染。一旦长期记忆被写脏,后续召回会长期带噪音,因此晋升条件应比短期记忆写入条件更严格。


十五、怎么去重和合并

1. Redis 和 LTM 都查到时怎么办

会重复,所以不能把两边结果直接都塞进 prompt。

正确流程是:

  1. 各路召回候选
  2. 进入统一 candidate pool
  3. 做归一化和去重
  4. 重打分
  5. topK
  6. 压缩再注入

2. 去重键建议

每条记忆维护:

  • fingerprint
  • canonical_fact_key
  • source_type
  • source_priority

例如:

{
  "canonical_fact_key": "bug456:createOrder:db_excluded",
  "fingerprint": "sha1(normalized_fact)",
  "source_type": "redis_stm",
  "source_priority": 100
}

3. 权威优先级原则

不是简单全局 PG > Redis > LTM,而是按数据域决定:

  • profile.* / permission.* / config.* / project_metadata.* -> PG 权威
  • task_state.* / session_state.* / current_decision.* -> Redis 权威
  • similar_case.* / semantic_hint.* -> LTM 只做补充

4. 默认来源优先级

在 Redis 和 LTM 的普通候选去重时,可以先用:

Redis STM > LTM > raw history fallback

5. 为什么默认保留 Redis 候选

当 Redis 和 LTM 命中同一事实时,优先保留 Redis 的原因是:

  • 更近
  • 更贴合当前任务
  • 更新鲜
  • 噪音更小

十六、会话级记忆和压缩机制

1. 为什么要压缩

会话原始历史会越来越长,但长期有价值的通常不是原始输出,而是:

  • 结论
  • 状态
  • 约束
  • 决策
  • 下一步动作

因此压缩不是简单删除旧消息,而是把原始过程逐渐转换为更稳的工作语义。

2. 两层压缩

第一层:轻量压缩

优先处理:

  • 较早的大块工具输出
  • 搜索原文
  • 文件读取原文
  • 长命令输出

这些内容体积大,但持续价值通常较低。

第二层:正式压缩

如果还不够,就把更早的过程折叠成结构化摘要,通常保留:

  • 当前任务目标
  • 用户最新要求
  • 已完成工作
  • 关键决定和约束
  • 已知问题和风险
  • 当前状态
  • 下一步动作

3. 压缩优先级原则

压缩不是单纯按时间,而是综合看:

  • 当前执行依赖度
  • 内容类型
  • 体积大小
  • 新旧程度
  • 持续价值

更准确的关系是:

当前执行依赖度 > 内容类型 > 体积 > 时间先后

4. 近场上下文

近场上下文不是最后一句,而是当前任务还能连续推进所必须保留的最近工作现场,通常包括:

  • 用户当前最新问题
  • 紧邻之前的一轮用户问题
  • 对那一轮的模型回复
  • 若最近几轮中有仍直接相关的工具结果,也一并保留

5. 工具结果依赖判断

判断一条回复是否依赖某个工具结果,重点不是时间,而是依赖关系:

  • 回复结论是否直接来自工具结果
  • 去掉工具结果后,回复是否还成立
  • 回复是在解释工具结果,还是独立推进任务
  • 工具结果是否已被消化为稳定语义

6. 工具结果依赖的三分类

  • 强依赖:回复直接消费工具结果,没有它回复就不成立
  • 弱依赖:工具结果提供背景,但回复主要在继续推进任务
  • 已消化:工具结果的核心信息已经沉淀成结论、状态、约束或计划

只有已被消化的原始工具结果,才更适合进入压缩候选。


十七、整体推荐流程

1. 查询流程

用户输入
  ↓
构建 query-profile
  ↓
规则路由 decide_pg / decide_redis / decide_ltm
  ↓
PG 产出 profile / constraints / runtime metadata block
  ↓
Redis / LTM 各自召回候选
  ↓
统一 candidate pool
  ↓
去重 / 重打分 / topK
  ↓
压缩成 memory summary
  ↓
注入模型

2. 写入流程

对话 / tool result
  ↓
候选事实抽取器(可异步)
  ↓
write_profile
  ↓
policy engine 判断存不存
  ↓
去重 / upsert / TTL 计算
  ↓
写 Redis STM
  ↓
异步判断是否晋升到 PG 或 LTM

3. 清理流程

定时任务扫描 expire zset
  ↓
找到已过期 memory_id
  ↓
删除 memory_item
  ↓
从 recent / session / task / entity / type / expire 各索引中删除对应 id

十八、最小可落地版本

建议先做 v1,不要一开始做复杂智能路由。

v1 建议

  • 会话历史:本地文件存 raw transcript
  • PG:存用户画像、偏好、权限、project / workspace 元数据
  • Redis:存当前任务短期记忆
  • ES + Milvus:存晋升后的长期记忆
  • 路由:纯规则 + 轻量打分
  • 写入:候选抽取 + 规则判断 + upsert
  • 压缩:先轻量删大块原始结果,不够再摘要压缩

v2 再考虑

  • 路由分类器
  • 更细粒度 rerank
  • 更复杂的长期记忆晋升策略
  • 召回质量评估闭环
  • LTM 子系统内部更细的混合检索策略

十九、一句话总结

最合理的 agent 记忆方案,不是把所有内容都塞进一个库,而是:

  • 用本地原始历史保存完整会话
  • 用 PG 保存权威结构化事实
  • 用 Redis 保存当前任务短期工作记忆
  • 用 ES + Milvus 保存长期语义记忆
  • 用 query-profile 和规则路由决定查哪层
  • 用 policy engine 和去重规则决定存哪层
  • 用统一候选池和压缩流程,避免重复和上下文污染