0%

基于请求驱动的面向Agent的通用记忆基础设施设计

本文面向大模型本身无状态、上下文窗口有限,而 Agent 需要跨会话长期记忆与个性化能力的场景,设计了一套基于请求驱动的通用记忆基础设施系统。该系统对调用方只暴露写入与召回两个高频接口,在内部自动完成事实抽取、记忆演化、权重衰减、容量淘汰、生命周期管理与隐私治理。

第一章 问题背景与设计目标

Agent 为什么需要记忆系统

我们需要知道,Agent 与单次 LLM 调用最大的区别在于:它在一个循环里反复决策,而模型本身是无状态的。每一轮请求承载的上下文来自系统提示词、工具定义、历史对话和检索结果,一轮对话结束后,这些上下文随之消失。由此带来四个具体问题:

  • 对话无法延续。用户上一轮说过的事,下一轮就忘了,多轮一致性只能靠把历史全部塞进上下文硬撑。
  • 无法积累个性化。模型不记录用户偏好。
  • 知识有时效缺口。模型参数里的知识有截止时间。
  • 上下文窗口是稀缺资源。历史全塞会挤占窗口,成本、延迟和模型注意力一起恶化。

因此,记忆系统的职责就是把对话沉淀为可检索的长期记忆,在需要时按相关度召回,让 Agent 在每一轮都能想起该记的事。业界对 Agent 记忆的分类已经比较一致,通常按认知科学映射成四类:

  • 工作记忆(会话内临时信息)
  • 情景记忆(具体事件经历)
  • 语义记忆(抽象知识概念)
  • 感知记忆(多模态信息)

本设计聚焦前三种,感知记忆作为扩展预留。

概念

有别于 Agent 的基础记忆系统,我们今天要介绍的 Agent 记忆系统是一套被动模式(请求驱动)的记忆基础设施:调用方(Agent)把对话丢进来,系统自动从中提炼事实、沉淀为长期记忆;当 Agent 下次需要读取记忆时,用一句自然语言召回最相关的记忆即可。中间所有工作,包括记忆的去重合并、衰减淘汰、删除归档等,全部由系统内部自动完成,调用方对此完全无感。

对调用方只暴露两个高频动作:写入(Add)召回(Recall);更新、删除、历史、隐私四类为低频管理动作。这是整套设计的出发点:把复杂性收敛在系统内部,把简单留给调用方

设计目标

目标 技术指标 说明
低延迟召回 召回 P99 ≤ 500ms,超时熔断 召回在对话主链路上,延迟敏感
异步写入 写入不阻塞主流程 抽取+演化是秒级耗时,必须与主链路解耦
高隔离性 用户 × 应用 × 会话 三维隔离 每条记忆只能被允许的场景看到
最终一致 并发写入以时间戳新者为准 记忆场景对写入实时可见性不敏感
隐私合规 删除限时生效、加密存储与传输 被遗忘权是硬约束
可演进 LLM 可插拔、跨应用/多模态预留 模型与存储都会换代

设计原则

  1. 对上层透明:调用方只关注写入与召回,演化、衰减、淘汰、归档全自动。
  2. 读写分离:召回走在线低延迟链路;抽取、演化、生命周期走异步链路,互不阻塞、互不拖累。
  3. LLM 可插拔:抽取与演化的智能能力经统一适配层接入,不绑定具体供应商。
  4. 存储分层:元数据、向量索引、变更日志、冷归档分库存储,各用其长。
  5. 配置驱动:容量、权重、衰减、保留期、排序配比等阈值全部下沉配置中心,可动态调整、便于灰度调参。
  6. 被动不外推:系统不主动产生记忆、不主动推送,一切由调用方请求触发。

第二章 核心设计决策

这一章节我们将进一步描述核心设计决策。每个决策都按照问题、方案与取舍理由三个层面展开,这些决策共同构成整套系统的骨架,后续章节则是在骨架上填充血肉。

为什么写入异步、召回同步?

  • 问题:写入需要调用 LLM 完成抽取和演化,这是秒级耗时且可能失败重试的操作,而召回位于对话主链路上,对延迟极其敏感。
  • 决策:写入接口只做受理,即完成校验并投递消息队列后立即返回受理凭证,真正的抽取演化放到异步 Worker 中执行;召回则走纯在线链路,由向量检索、元数据并行与排序构成。
  • 理由:抽取演化的耗时不应阻塞 Agent 与用户的对话节奏,异步化之后写入接口的响应时间可以控制在毫秒级;同时读写物理隔离部署,召回服务可以按读流量独立扩缩容,写入洪峰不会拖垮召回延迟,这是满足低延迟召回目标的前提。代价是写入最终一致,即写入后短暂时间内不可召回,但记忆场景对此完全可接受。

为什么要事实抽取 + 记忆演化两段式,而不是直接存对话?

  • 问题:直接存储原始对话会导致记忆冗余、矛盾与膨胀,召回的噪声会很大。
  • 决策:先用 LLM 把对话抽取成结构化事实,再让演化引擎把新事实与已有记忆比对,做 ADD / UPDATE / MERGE / SKIP / DELETE 五选一决策。
  • 理由:抽取把口语化的对话压缩成可检索的事实,显著降低存储量与召回噪声;演化保证记忆库自我去重、自我纠错,比如用户改了住址,应该是 UPDATE 而不是新增一条矛盾记忆,这正是记忆区别于日志的本质;五种决策覆盖了记忆增删改的全部语义,且决策过程会写入变更日志,可追溯。

为什么用向量库 + 元数据库双存储,而不是单库?

  • 问题:召回既需要语义相似,比如叫车能召回打车偏好,又需要精确过滤,比如按用户、应用、类型、状态过滤,还需要权重排序
  • 决策向量数据库负责语义近邻检索(ANN,HNSW/IVF),元数据库负责精确过滤、权重、状态等结构化属性与强一致更新,两者通过引用标识关联。
  • 理由:纯向量库做不了高效的结构化过滤与强一致更新,纯关系库做不了高维语义检索,各取所长是业界记忆与 RAG 系统的标准做法。召回时两库并行取数,向量库取候选与相关性、元数据库取权重等属性,再在排序引擎合并,兼顾语义与延迟。代价是双写一致性问题,用事务消息、幂等重试与定时对账兜底。

为什么权重用指数衰减模型?

  • 问题:记忆要像人一样越久不用越淡、用到就加深。设计上需要一个可配置的硬锚点:普通记忆完全不召回时,90 天降到归档阈值 0.10。
  • 决策:采用指数衰减 w(t)=w0eλαtw(t) = w_0 \cdot e^{-\lambda \alpha t},由锚点反解 λ0.0231\lambda \approx 0.0231(每天);重要记忆用因子 α=0.1\alpha = 0.1 让衰减放慢到 1/10。
  • 理由:指数衰减天然符合遗忘曲线,单参数即可精确命中 90 天锚点,数学上可推导、可配置;用乘性因子表达重要性,既能显著放缓衰减、又不做永久豁免,符合近似不衰减但不永久的语义。同时支持惰性衰减优化:召回时按上次衰减时间实时补算,定时任务兜底,避免每天全量扫描全表。

为什么删除用软删除 + 定时物理清理?

  • 问题:用户的一键清除需要限时生效(比如 24h 内),但实时物理删除海量记忆代价高,且需要保留审计与可恢复窗口。
  • 决策:删除即打删除标记实时退出召回,满足体验与合规时效,物理删除交由定时任务在保留期(如 30 天)后执行,并对变更日志脱敏。
  • 理由:软删可以实时生效、可以对账、可以在保留期内追溯;物理清理离线批量做,成本低且不影响在线链路。

为什么状态机是活跃 → 归档 → 物理清理三段,而不是直接删?

  • 问题:权重衰减到很低的记忆未必要立即丢弃,可能偶尔还会用到,但应该退出活跃召回以保证召回质量与索引规模。
  • 决策:低于阈值先归档,即移出活跃向量索引、转冷存储、默认不召回但可显式查归档,归档超 180 天再物理清理。
  • 理由:归档是降级而非删除,兼顾召回质量、索引成本与数据可追溯,给了一层缓冲。

为什么强调配置驱动与 LLM 可插拔?

  • 记忆系统的关键阈值,包括容量、衰减系数、排序权重配比、保留期等,都需要上线后持续调参,硬编码会导致每次调整都要发版,下沉配置中心可以灰度、可以回滚。
  • LLM 与 Embedding 供应商会演进,也可能需要多供应商降级容灾,统一适配层让上层逻辑与具体模型解耦。

第三章 总体架构

分层架构

整套系统按职责切分为六层,每一层只解决一类问题:

图1:分层架构

每一层独立成层的理由如下:

解决的核心问题 为什么独立成层
接入网关层 统一入口、鉴权限流、调用方识别、记忆开关前置拦截 把横切关注点(认证/限流/路由/关闭短路)从业务服务剥离;关闭记忆的用户在网关层即被短路,不消耗下游 LLM 与检索算力
核心服务层 编排写/读/管理/隐私四类能力 读写物理隔离是低延迟的关键:召回服务可按读流量独立扩缩容,写入洪峰不影响召回;隐私服务独立便于合规审计与权限收口
智能处理层 抽取、演化、排序,LLM/Embedding 经适配层接入 智能能力变化最快(模型迭代、供应商切换),独立成层 + 适配器隔离,上层逻辑不随模型变动;抽取与演化复用同一套相似检索能力
异步处理层 承载抽取/演化/提升/生命周期等耗时任务 用消息队列削峰解耦写入与处理,Worker 幂等消费保证可重试;把慢操作与在线请求彻底分离
存储层 元数据/向量/日志/缓存/冷归档分库 不同数据访问模式差异巨大(点查 vs 近邻检索 vs 追加写 vs 冷读),分库各用其长,用事务消息 + 对账保证最终一致
基础设施 配置中心、监控告警、密钥管理 配置驱动需要配置中心;记忆内容敏感需要密钥管理加密;全链路可观测需要监控

三条链路在架构上的物理隔离

  • 在线低延迟链路:召回服务 → 排序引擎 → 向量库 / 元数据库 / 缓存。独立部署、独立扩缩容。
  • 近线异步链路:写入服务 → 消息队列 → Worker → 抽取引擎 / 演化引擎。削峰解耦。
  • 离线批处理链路:定时调度 → Worker → 衰减 / 归档 / 清理 / 对账。错峰限速,与在线资源隔离。

核心取舍:用最终一致换取召回低延迟、写入高吞吐与系统可演进。记忆场景对写入实时可见性不敏感,这个取舍成立。

模块职责

模块 职责
WriteService 接收写入请求,校验容量与记忆开关,投递异步抽取/演化任务,支持直写结构化记忆
RecallService 解析召回维度,调用排序引擎,更新召回元信息,处理空结果与超时
MgmtService 单条/批量更新、软删除、历史与变更日志查询
PrivacyService 记忆查看、一键清除、按维度清除、记忆开关、数据导出
Extractor 调 LLM 从对话原文抽取结构化事实,失败重试
Evolution 新事实与已有记忆对比,决策 ADD/UPDATE/MERGE/SKIP/DELETE
Ranker 两阶段排序:相关性阈值过滤 + 综合分排序
LifecycleManager 状态机流转:活跃 → 归档 → 删除 → 物理清理
CapacityManager 容量检查与淘汰策略执行
DecayCalculator 权重衰减与召回恢复计算
AuditLogger 所有变更生成不可篡改的变更记录
IsolationResolver 将召回维度参数解析为存储查询条件,保证隔离

下面用一句话概括各模块之间的协作关系:

  • 写链:WriteService → MQ → Extractor → Evolution →(CapacityManager 把关)→ 元数据库/向量库/变更日志
  • 读链:RecallService → IsolationResolver → 向量库+元数据库(并行)→ Ranker → 返回 + 异步权重恢复
  • 治理链:定时调度 → DecayCalculator/LifecycleManager/CapacityManager → 元数据库/向量库/冷存储/变更日志
  • 横切:配置客户端给所有模块下发阈值;审计日志记录所有写变更;隔离解析守住隔离边界。

第四章 数据模型

存储选型及理由

数据类别 选型 访问模式 为什么这样选
记忆元数据主表 分布式 KV / 关系库(按用户分片) 高并发点查、按维度范围扫描、强一致更新 召回过滤、权重更新、容量管控都需要结构化属性与强一致;按用户分片天然贴合记忆按用户隔离,可水平扩展
语义向量索引 向量数据库(HNSW/IVF) 高维近似最近邻(ANN)检索 语义召回必须靠向量相似度,关系库无法高效做高维近邻;HNSW 兼顾召回率与延迟
变更日志 追加写日志库 / 宽表 只增不改、按时间/记忆/用户范围查询 审计与历史要求不可篡改、可追溯;追加写性能高,天然适合日志型数据
热点缓存 分布式缓存 超高频读、低写 记忆开关、热点用户记忆、容量计数读取频繁,缓存挡住大部分读压力,是关闭拦截与低延迟召回的关键
归档冷数据 对象存储 + 二级索引 低频冷读 归档记忆访问稀少,冷存储成本低;保留二级索引以支持偶发的归档查询

实现以能力抽象描述,不绑定具体组件,便于落地选型。

为什么不用一个库搞定

  • 纯向量库:缺乏高效结构化过滤与强一致更新,权重、状态、容量管控难做。
  • 纯关系库:无法高效支持高维语义近邻检索。
  • 结论:双存储(向量 + 元数据)并行取数、排序层合并,是语义召回系统的标准范式;代价(双写一致性)用事务消息 + 对账兜底。

记忆主表(Memory)

字段 类型 说明
memory_id string 全局唯一记忆 ID(雪花/UUID)
user_id uint64 所属用户标识(分片键)
app_id string 所属应用/Agent 实例标识,空 = 全局记忆
session_id string 所属会话标识,可空
level enum GLOBAL / APP / SESSION(作用域层级)
ownership enum USER / AGENT(记忆归属,谁产生的)
mem_type enum SEMANTIC / EPISODIC / PROCEDURAL(语义/情景/过程)
content text 结构化记忆文本(加密存储)
weight decimal 记忆权重 0.00~1.00(排序/淘汰索引)
status enum ACTIVE / ARCHIVED / DELETED
is_important bool 重要性标记
recall_count uint 召回累计次数
created_at / updated_at timestamp 创建与更新时间
last_recalled_at timestamp 最后召回时间,未召回为空(淘汰二级排序)
expire_at timestamp 过期时间,可空 = 永久
archived_at timestamp 归档时间,仅归档态有值
content_hash string 内容哈希,写入幂等去重的最后一道闸
source_session_id / source_msg_id string 来源会话与消息标识(可解释性)
custom_tags json 调用方自定义标签
vector_ref string 向量库检索标识
deleted_at timestamp 软删除时间

关键索引设计,每个索引服务于一类高频操作:

  • 召回过滤复合索引:(user_id, app_id, level, ownership, mem_type, status),服务在线召回的精确过滤。
  • 淘汰排序索引:(user_id, level, app_id, weight ASC, last_recalled_at ASC),服务容量淘汰先淘汰低权重久未用的记忆。
  • 生命周期扫描索引:(status, weight)(status, expire_at)(status, archived_at)(status, deleted_at),服务各定时任务的批量扫描。

变更记录表(MemoryChangeLog)

字段 说明
change_id 变更记录唯一 ID
memory_id 关联记忆 ID
user_id 所属用户(便于按用户审计/清除)
op_type ADD / UPDATE / DELETE / MERGE / ARCHIVE / RESTORE
content_before / content_after 变更前后内容(物理清理后脱敏)
reason 变更原因(MERGE 记录参与合并的事实来源)
op_source SYSTEM / USER / ADMIN
op_time 操作时间
sanitized 内容是否已脱敏

配置表与容量计数表

记忆配置表写少读多,由热点缓存承载,供网关在写入与召回前前置判断是否短路,内容包括用户级开关与按应用粒度的开关。

容量计数表(user_id, dimension) 维护实时计数,覆盖全局、各应用、总量与重要标记数,写入或删除时原子增减,避免每次容量判断都做全表 count 扫描。

为什么单独建计数表:容量判断在每次写入前都要执行,实时 count 海量记忆不可行;维护增量计数可以把 O(N) 扫描降为 O(1) 读取。

第五章 接口设计

接口总览

设计取向:高频动作(写入、召回)优先简单、异步化;管理与隐私动作低频、强一致。

分类 接口 方法 同步性 优先级
写入 /memory/v1/add POST 异步 P0
写入 /memory/v1/add_structured POST 异步/同步 P1
召回 /memory/v1/recall POST 同步 P0
更新 /memory/v1/update POST 同步 P0
删除 /memory/v1/delete/batch_delete POST 同步 P0
历史 /memory/v1/history/change_log POST 同步 P1
隐私 /memory/v1/user/list/purge/switch/export POST 同步/异步 P0/P1
会话 /memory/v1/session/close POST 异步 P0

统一响应结构为 { code, msg, trace_id, data }。统一错误码覆盖:参数错误、记忆功能已关闭、容量已满且淘汰后仍无法写入、无相关记忆、抽取失败、召回超时熔断、跨应用能力未开放(预留)、重要标记超限。

写入接口 /memory/v1/add

请求携带:用户标识、应用标识(可空,空 = 全局)、会话标识、归属(USER/AGENT)、作用域层级(GLOBAL/APP/SESSION)、待抽取的对话原文(角色 + 内容 + 消息 ID),以及可选的抽取策略(侧重点、演化模式、自定义标签)。

接口响应只返回受理凭证(task_id),真正的执行将在后台运行,请求如下:

1
{ "code": 0, "data": { "task_id": "t_789", "accepted": true } }

设计说明add 默认异步,只返回受理凭证。真正的抽取演化在后台完成,避免阻塞对话主链路。

直写结构化记忆 /memory/v1/add_structured

统计聚合类记忆(如使用频率)已经是结构化结论,无需 LLM 抽取与演化,走旁路直写以降本提速:skip_evolution=true,直接指定内容、类型、重要性、过期时间与自定义标签。

这条接口还有一层身份:它同时是 Agent 的记忆自我编辑入口,Agent 想主动记一条关于自己的事实,直接结构化直写即可(见第九章)。

召回接口 /memory/v1/recall(核心)

请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"user_id": 123456789,
"query": "帮我叫个车", # 自然语言查询或上下文
"scope": {
"include_global": true, # 是否含全局记忆
"app_id": "app_didi888", # 召回该应用记忆
"cross_app": { "enabled": false } # 跨应用(预留)
},
"filter": { "ownership": [...], "mem_type": [...], "custom_tags": [...] },
"top_k": 10, # 返回上限
"min_relevance": 0.3, # 最低相关性阈值
"include_archived": false, # 是否召回归档
"timeout_ms": 500
}

响应:返回命中标识与记忆列表,每条记忆包含内容、归属、类型、权重、相关性、综合分与作用域层级。召回命中后会异步更新该记忆的召回时间与召回次数,并触发权重恢复,整个过程不阻塞响应返回。

更新、删除、历史与隐私

  • 更新:按 memory_id 更新内容或元信息(重要性、过期时间),支持只更新元信息不动内容。
  • 删除:单条或按维度批量,均为软删除,打标记后实时退出召回,物理清理由定时任务在保留期后执行。
  • 历史:单条记忆的完整变更链;按用户、维度与时间范围查询的变更日志。
  • 隐私:查看用户全部记忆(按维度组织)、一键清除(实时软删 + 异步物理清理)、记忆开关(全局/按应用)、异步导出。

会话结束接口 /memory/v1/session/close

会话结束时触发会话级记忆价值评估:有价值的记忆提升为 APP/GLOBAL 长期记忆,无价值的丢弃;若标记待续,则保存为短有效期(默认 24h)的上下文连续性记忆,供下一次会话衔接。

第六章 核心流程

写入与演化(异步链路)

1
2
3
4
5
调用方 → 网关(鉴权 + 记忆开关前置拦截)→ WriteService(校验 + 投递任务)
→ 立即返回受理凭证
消息队列 → Worker → 抽取引擎(LLM 提炼事实,失败重试一次,仍失败记日志放弃)
→ 演化引擎:每条新事实 → 向量化 → 检索相似已有记忆 → LLM 决策
→ 落库(元数据 + 向量 + 变更日志 + 容量计数)

演化决策的五个分支:

  • ADD:容量检查 → 满则触发淘汰 → 仍满则拒绝写入;否则写新记忆(初始权重)+ 向量 + 变更记录。
  • UPDATE:覆盖更新,权重重置为该类型初始值。
  • MERGE:合并替换,权重取双方较高者。
  • DELETE:软删旧记忆 + 新增新记忆(注意:这是演进后的语义,见第十章标失效改进)。
  • SKIP:记录决策,不落库。

召回(在线低延迟链路)

1
2
3
4
5
调用方 → 网关 → RecallService(校验记忆开关)→ 隔离解析(scope/filter → 精确查询条件)
→ query 向量化
→ 并行:向量库近邻检索(带维度过滤)+ 元数据库拉取权重
→ 排序引擎两阶段:相关性阈值过滤 → 综合分排序 → TopK 返回
→ 异步:更新召回元信息 + 权重恢复

整条链路超时熔断返回。命中后的异步更新不阻塞响应。

删除与隐私清除

一键清除的执行过程是:批量标记删除(实时生效)→ 失效缓存 → 生成变更记录 → 即时返回。保留期过后由定时任务物理删除,并对变更记录内容脱敏。

生命周期状态机(衰减 / 归档 / 清理)

每日批处理(错峰执行):

  1. 权重衰减:拉取活跃记忆(分片批量),按衰减公式计算新权重;跌破归档阈值(0.10)触发归档(移出活跃向量索引、转冷存储、记变更)。
  2. 过期归档:expire_at 到期的时效记忆转归档(非删除)。
  3. 物理清理:归档超 180 天转冷存储并脱敏;软删超 30 天物理删除并脱敏。
  4. 对账:校验主表 / 向量库 / 计数表一致性,修复漂移。

关键点:即使用户关闭了记忆开关,生命周期任务仍继续执行;重新开启后未清理的记忆自动恢复参与召回。

会话级记忆提升

会话结束时评估会话级记忆的价值:有价值的提升为 APP/GLOBAL 记忆(目标维度容量检查),无价值的丢弃(不持久化);待续会话保存为短有效期上下文连续性记忆。

多存储一致性保障

  • 主表与容量计数同事务保证强一致。
  • 向量库、变更日志通过事务消息 + 幂等重试保证最终一致。
  • 对账任务定期校验主表与向量库/计数表的一致性,修复漂移。

第七章 关键算法

权重衰减算法

采用指数衰减模型,满足设计锚点:普通记忆完全未召回时 90 天降至归档阈值 0.10。

设初始权重 w0w_0,距上次召回天数 tt,衰减系数 λ\lambda,重要因子 α\alpha(重要记忆 α=0.1\alpha = 0.1,普通 α=1.0\alpha = 1.0):

w(t)=w0eλαtw(t) = w_0 \cdot e^{-\lambda \cdot \alpha \cdot t}

由锚点求 λ\lambda:取普通语义记忆 w0=0.80w_0 = 0.80,要求 w(90)=0.10w(90) = 0.10

λ=ln(w0/0.10)90=ln8900.0231 (每天)\lambda = \frac{\ln(w_0 / 0.10)}{90} = \frac{\ln 8}{90} \approx 0.0231 \ \text{(每天)}

λ\lambda 为可配置参数,默认值以满足 90 天预期为准。重要记忆 α=0.1\alpha = 0.1 时衰减速率为普通记忆的 1/10。

召回恢复算法

召回命中时权重提升,但不超过该类型初始权重上限:

wnew=min(wcurrent+Δrecall, winit_type)w_{new} = \min\big(w_{current} + \Delta_{recall},\ w_{init\_type}\big)

Δrecall\Delta_{recall} 为可配置恢复幅度;同时刷新 last_recalled_at 重置衰减起点。

召回综合排序算法(两阶段)

1
2
3
4
5
# 阶段1(过滤): 保留 relevance ≥ min_relevance(默认 0.3) 的记忆
# 阶段2(排序): final_score = relevance × W_rel + weight × W_wt
# 默认 W_rel = 0.7, W_wt = 0.3(可配置)
# relevance、weight 均归一化至 [0, 1]
# 按 final_score 降序取 TopK

为什么相关性权重更高:召回首先要相关,权重用于在相关结果之间体现记忆强度,做次级区分。配比可经配置中心灰度调参。

初始权重表

归属 类型 初始权重
用户记忆 语义 0.80
用户记忆 情景 0.60
用户记忆 过程 0.70
Agent 记忆 语义 0.70
Agent 记忆 情景 0.50
Agent 记忆 过程 0.60
时效性事件 不区分 0.85
重要标记 不区分 0.90(原值更高则保持)

容量淘汰算法

1
2
3
4
5
6
7
写入前/巡检触发容量检查 → 维度计数 ≥ 上限?
否 → 允许写入
是 → 按 weight ASC, last_recalled_at ASC 排序
→ 排除 is_important=true 记忆
→ 选取待淘汰集(淘汰至容量 90%)
→ 记忆 weight > 0.10? 归档 ARCHIVED : 软删除 DELETED
→ 腾出空间后可写入? 是 → 允许写入; 否 → 拒绝写入(42901)

容量维度上限(均可配置):全局记忆/用户 500 条,单应用/用户 200 条,全维度总量/用户 2000 条,重要标记上限为各维度容量的 20%。

演化决策提示(LLM Prompt 设计要点)

演化引擎调用 LLM 时,输入新事实与 TopN 条相似已有记忆,约束输出五选一决策及目标记忆 ID:

决策 权重处理
ADD 按初始权重表设定
UPDATE 重置为该类型初始权重
MERGE 取双方较高者,且不低于初始权重
DELETE(+ADD) 旧软删,新记忆按 ADD 规则
SKIP 不变

决策过程与结果写入变更日志便于排查。自定义策略(抽取侧重点、保守/激进、自定义标签)通过抽取策略参数注入,但不可覆盖隐私与容量规则

第八章 抽取与演化的工程细节

设计篇停在取舍层面,这一章下探到代码颗粒度。我们对照开源实现(Mem0、MemGPT、Zep)逐条把抽象设计落到能照着写的程度。先看抽取与演化这两个最核心的环节。

抽取:prompt 到底怎么写的

抽取听起来抽象,但开源实现把它具体化成了一段可照抄的 prompt。核心结构:给模型一个角色设定(你是个人信息整理员),列 7 类要记的信息(个人偏好、重要个人信息、计划与意图、活动与服务偏好、健康相关、职业信息、杂项),再用一组 few-shot 把什么该记、什么不该记掰开揉碎。few-shot 的价值在于教模型不该记什么

1
2
3
4
5
6
输入 "Hi."                                  → {"facts": []}          // 寒暄,无事实
输入 "There are branches in trees." → {"facts": []} // 通识,不是关于用户的事实
输入 "Hi, I am looking for a restaurant in San Francisco."
→ {"facts": ["Looking for a restaurant in San Francisco"]}
输入 "Hi, my name is John. I am a software engineer."
→ {"facts": ["Name is John", "Is a Software engineer"]}

有几个设计里容易遗漏、但 prompt 中必须硬性约束的要点:

  • 当前日期动态注入。在 prompt 里写明 Today's date is {now}。这对时效记忆很关键,比如我下周要去上海这类表述,没有当前日期模型无法算出绝对时间。
  • 语言对齐。检测用户输入语言,用相同语言记事实。中文场景这条必须有。
  • 不从系统消息抽取。只看 user 和 assistant 的内容,避免把系统提示词当成用户事实存进去。
  • 空就返回空。没东西可记就返回空列表,不要硬凑。

抽取的 ownership 分流

一个容易忽略的细节:抽取应该按消息归属分流,而不是先混抽再打标签。实现上是三套 prompt 变体:通用抽取看 user + assistant;用户记忆抽取只看 user 消息;Agent 记忆抽取只看 assistant 消息。这样从源头保证用户记忆里不混进 Agent 的话,反之亦然,比先混抽再打标签干净得多。

演化决策 prompt 的细节

演化决策 prompt 定义四个操作(注意是四个,不是五个,合并被并进了 UPDATE):

操作 触发条件 关键约束
ADD 新事实在记忆里不存在 生成新 ID
UPDATE 信息已有但内容不同 保持原 ID 不变,且带 old_memory 字段记录改之前的内容
DELETE 新事实与旧记忆矛盾 删旧的
NONE 新事实已存在或无关 啥都不做

few-shot 里几个很见功力的细节:

  • 含义相同就别更新。专门举例:旧记忆是 Likes cheese pizza,新事实是 Loves cheese pizza,意思一样,不更新。这是在防 LLM 把同义改写当成新信息反复刷库。
  • UPDATE 要合并而不是覆盖。旧记忆是我喜欢芝士披萨,新事实是喜欢鸡肉披萨,应更新成喜欢芝士和鸡肉披萨。不是拿新的盖掉旧的,而是揉成一条更全的。覆盖更新其实偏粗暴。
  • ID 纪律。prompt 里反复强调:UPDATE/DELETE 只能用输入里给的 ID,不许造新 ID,只有 ADD 能造。这是为了让决策结果可以直接映射回数据库操作,不会出现 LLM 编造一个不存在的记忆 ID 的脏数据。

一个必须讲清楚的分歧:决策式演化 vs 纯追加

读开源实现时发现一个重要的反转,值得在写系统之前就想清楚:主流实现的主流程已经把 LLM 决策式的 ADD/UPDATE/DELETE 砍掉了。早期版本确实是抽取 → 检索相似旧记忆 → 让 LLM 五选一决策,和我们上面画的演化时序几乎一模一样;但现在的主管道改成了纯追加 + 哈希去重:抽取出来的事实全部按 ADD 处理,靠内容哈希去重,UPDATE 和 DELETE 退化成独立的显式 API,不在写入链路里自动跑。

从实现里可以推断出这样设计的原因:

  • 决策式演化每写一条都要多一次 LLM 调用(先抽取、再决策),延迟和成本翻倍,还多一个失败点。
  • LLM 决策不稳定。同样的新旧事实,模型今天判 UPDATE 明天判 ADD,记忆库会飘。
  • 矛盾就 DELETE 旧的这一做法很危险,模型一旦误判,真实信息就被删了,不可逆。追加 + 去重则永远不丢信息,最多冗余,而冗余可以靠检索时排序消化掉。

这对设计意味着什么:决策式路线在记忆库干净、召回噪声低上更优,但要清醒地接受代价。一是写入链路上有两次 LLM 调用,端到端耗时预算要按两次算;二是 DELETE 决策必须有护栏,别让 LLM 直接物理删,软删是对的,保留了可恢复窗口;三是建议加一个降级开关,当演化 LLM 不可用或超时,自动退化成纯 ADD + 去重的模式,保证写入不中断。这是很实在的容灾设计。

相似检索取几条(topN 的取值)

演化决策需要输入新事实与若干条相似已有记忆,那么这些相似记忆到底应该取多少条?不同场景的取值并不一样:

场景 取值 用意
写入时拉相似旧记忆(给演化/决策当上下文) top_k = 10 够判断有没有重复就行,多了费 token
写入时拉会话最近消息 limit = 10 给抽取补上下文
召回检索 max(limit × 4, 60) 过取:先多捞 4 倍,打完分再截到 limit
实体关联检索 top_k = 500 实体匹配要广撒网

最值得抄的是召回的过取再截断。召回链路是 ANN 检索 → 阈值过滤 → 排序 → TopK 截断,但 ANN 阶段捞多少条很容易被忽略。如果只捞 top_k 条,经过相关性阈值过滤后可能就剩三五条,排序失去意义。max(limit × 4, 60) 是个好默认,先捞够再筛。请求 top_k=10 时,底层 ANN 应捞 60 条候选,过滤排序后再返回 10 条。

向量什么时候重算

一个隐藏的大坑:文本变了向量没变,召回就会失准。规则必须明确:

操作 向量怎么处理
新增记忆 批量向量化,失败回退逐条
已有记忆(检索出来当上下文的) 不重算,只取 payload 里的哈希用来去重
召回的 query 算一次
UPDATE 且文本变了 必须重算向量并覆盖向量库里的旧向量
UPDATE 但只改元信息(如 is_important) 不动向量

关键结论是:UPDATE 或 MERGE 只要改了内容,就必须重算向量,否则会出现记忆内容已经变成 A、但向量还保留 B 的语义的情况,召回时驴唇不对马嘴;只修改元信息不碰向量,还能省一次向量化调用。

去重与幂等

Worker 幂等不能只靠消息队列的消息 ID(那只防消息重投),还得防不同消息抽出了相同事实。做法很朴素但有效:每条记忆文本算内容哈希存字段,写入时拿新事实的哈希去比两组集合,一组是库里已有的哈希,另一组是本批次内已经见过的哈希,防止同一批里抽出两条一样的事实。命中任一就跳过。这是写入侧最后一道闸。

实体级去重(如果做了实体提取)分两层:精确层做文本归一化(去空格、转小写、折叠多空格)后精确匹配;语义层用向量相似度 ≥ 0.95 才算同一个实体。

这个 0.95 的阈值很有意思,它比召回的 0.3 阈值高得多。原因是:召回是找相关的,去重是找几乎一样的,后者必须严。将来如果做记忆合并(MERGE)的相似判定,阈值也应该往高了取(0.9+),不能用召回那套 0.3,否则会把相关但不同的记忆错误合并掉。这是个容易踩的坑。

并发与健壮性

几个值得记的实现细节:

  • 异步卸载:所有阻塞 I/O(向量化、向量库、数据库)丢线程池,不堵事件循环。
  • 批量失败回退逐条:批量写入/批量向量化失败时,自动退化成逐条重试,单条失败只记日志、不中断整批。别让一条脏数据拖垮一整批写入
  • 删除的竞态规避:批量删除别拆成 N 次单删去并发跑,尤其当多条记忆挂在同一个用户、同一个计数器上时,并发单删会让容量计数表的原子增减打架。批量删应该走一次聚合操作,把计数一次性扣减到位。

第九章 记忆分层与调度

设计里我们把记忆按作用域分成 GLOBAL / APP / SESSION 三层,但有一个开源项目(MemGPT)提供了另一套更有解释力的分层视角,它不是按作用域分,而是把记忆当成操作系统管内存那样分。两套分法的视角不同,放一起看能照出设计里没想透的地方。

核心隐喻:LLM 是 CPU,上下文窗口是 RAM

这个项目最初的洞察很简单:LLM 的上下文窗口就像内存条,容量有限且昂贵。装不下的东西怎么办?操作系统早就解决过这个问题——分页:把不常用的换出到磁盘,要用了再换回来。于是记忆分成三层,完全对应内存层次:

对应硬件 特点 谁来管
Core memory(核心记忆) 寄存器 / L1 缓存 常驻上下文窗口,随时可见,容量极小 Agent 自己用工具编辑
Recall memory(对话记忆) 内存 完整对话历史,超窗后移出,可检索调回 系统自动
Archival memory(归档记忆) 磁盘 容量无限的外部库,按需检索调入 Agent 显式存取

关键区别在在不在上下文里:core 永远在,archival 永远不在(用时才调),recall 是溢出区。

作用域轴与调度轴是正交的

很容易把这套三层和我们的 GLOBAL/APP/SESSION 对应起来,但对应是错的,先把这点掰清楚:

  • 我们的分层是作用域维度:这条记忆该被哪些场景看到(全局 / 单个应用 / 单次会话)。回答的是可见性问题。
  • MemGPT 的分层是在不在工作内存维度:这条记忆现在要不要占用宝贵的上下文窗口。回答的是调度问题。

这两个维度正交。一条全局记忆,既可能因为高频用而该常驻(core),也可能很久没用该躺在归档(archival)。设计里只有作用域这一根轴,缺了调度这根轴。

这不是说设计漏了,托管记忆库不直接管 Agent 的上下文窗口,调度那根轴本来就该留给调用方。但理解这个正交关系,能帮我们想清楚一件事:召回返回的 TopK,本质上就是在帮调用方做换页决策,从记忆库这个磁盘里,挑出这一轮该进 Agent 内存(上下文)的那几条。排序算法的角色,等同于操作系统的换页策略。

记忆的自我编辑

这个设计里最有名的一块是 core memory 可以被 Agent 自己用工具改:追加内容到核心记忆的某个块、替换某段、往归档存或取。core memory 通常分成有标签的块,最经典的两块是:

  • persona:我是谁(Agent 的人设)
  • human:我面对的用户是谁(关于用户的核心事实)

Agent 在对话中发现用户说了重要的事,就主动调用工具把它写进 human 块。这就是记忆的自我编辑。

这对 Agent 记忆(ownership=AGENT)是个直接的补充:Agent 记忆怎么产生、记什么,设计里往往很模糊。这里给了两个答案:

  • Agent 记忆里应该有一块类似 persona稳定人设记忆,定义这个 Agent 是什么风格、什么定位。这种记忆不该走衰减,它不是越久不用越淡,而是人设就该稳定。衰减模型对记忆一视同仁的话,这里该开个口子:人设类 Agent 记忆应豁免衰减,比重要标记的处理更彻底。
  • 我们的直写结构化记忆接口(add_structured)其实就可以当成自我编辑的接口:Agent 想主动记一条关于自己的事实,直接 add_structured(ownership=AGENT, skip_evolution=true) 写进去,跳过抽取演化。这条链路设计里已经有了,只是没意识到它对应的就是记忆的自我编辑能力。

上下文快满了怎么办:压缩是第三条路

换页是被上下文快满了触发的:对话累积到接近窗口上限,触发压缩,把旧对话总结成摘要,腾出空间,细节沉到归档。托管记忆库没有上下文窗口的概念,但有容量上限。两者其实是同一类问题:有限空间满了,得决定留谁、走谁

对照下来有两点可以借鉴:

第一,压缩可以作为淘汰之外的第三条路。 容量满了通常只有两招:归档或软删。压缩提示了第三招——把多条琐碎记忆总结成一条,既腾了空间又没全丢信息。比如用户点了 20 次外卖,与其存 20 条情景记忆挤占容量,不如压缩成 1 条语义记忆:用户常点外卖,偏好 X。统计聚合任务其实就是这个思路的雏形,但只用在推荐统计上;可以推广成通用的记忆压缩任务:同一维度下高度相似的低权重记忆,定期总结合并。

第二,会话级记忆的提升正是换出到长期。 对话结束或压缩时,把有价值的东西从对话记忆沉淀到归档。会话结束 → 评估价值 → 有价值的提升为长期记忆,是完全一样的动作。区别是那边让 Agent 自己判断价值,我们让演化引擎判断。这给了一个选项:提升的价值评估可以部分下放给调用方 Agent,它比系统更懂这次会话里哪句话重要。会话关闭接口可以加一个可选字段,让 Agent 显式标记这几条请务必提升。

远期动向:版本化记忆与做梦

新版本引入的两个机制,方向上值得记一笔:

  • MemFS:把记忆当成一个带 git 版本追踪的文件系统来管。这和我们变更日志是同一个诉求,记忆的历史可追溯、可回溯,但它做得更彻底,直接是文件系统级的版本控制。变更日志已经能支撑查历史变更链,但要回滚到某个历史版本还差一层。
  • Agent dreaming(做梦):Agent 在空闲时自动整理、巩固记忆,把零散记忆归纳、剔除矛盾。这其实就是离线批处理链路该干的事的智能版:现在的离线任务(衰减/归档/对账)是机械规则,dreaming 是让 LLM 在低峰期主动梳理记忆库。可以放进远期规划:离线链路除了机械治理,还可以有 LLM 巡检整理任务,定期发现并合并冗余、消解矛盾。

第十章 时序记忆与知识图谱

记忆如何随时间变化,如果只靠过期就归档这一种粗粒度机制来处理,那设计还停留在很浅的层面。有一个开源项目(Zep,底层是 Graphiti 引擎)在这件事上走得最远,它的核心观点能直接照出不足。

记忆图

前面把记忆当成一条条独立的文本(带向量、带权重)。而图模型不是这样,它把记忆建成一张时序知识图谱,节点是实体(用户、地点、商品、人),边是事实或关系,比如用户住在深圳就是一条连接用户和深圳的边。新数据进来不是新增一条孤立记忆,而是往图里加节点和边,并且让过时的边失效。

这个差别落到具体场景:用户先说住在深圳南山区,三个月后说搬到广州了。

  • 条模型的做法:演化引擎检索到旧记忆,判 UPDATE 或 DELETE+ADD,旧记忆被覆盖或软删。住过深圳这个历史事实,就没了。
  • 图模型的做法:不删旧边。给住深圳这条边标上失效时间,同时新增住广州这条边标上生效时间。两条事实都在,只是一条已失效、一条当前有效,且都带着时间戳。

valid_at / invalid_at:时效该有的样子

单点过期是非黑即白的:到点了就归档。图模型的时间模型是区间有效性,每条事实带两个时间:

字段 含义
valid_at 这条事实从什么时候开始成立
invalid_at 这条事实从什么时候开始不再成立(被新事实推翻时才填)

关键设计在于:事实失效不等于删除。当新数据推翻旧事实,旧事实不被删,只是被填上一个失效时间,然后继续留在图里,历史完整保留。这解决了单点过期解决不了的三类问题:

  • 曾经为真的可追溯。用户问之前是不是说过住深圳,条模型答不出来(记忆被覆盖了),区间模型能。
  • 矛盾不是靠删,是靠时间排序。遇到矛盾要 DELETE 旧的(有误删风险),区间模型根本不删,靠失效时间自然把旧事实排到历史里,召回默认只取当前有效的。这比删除安全得多。
  • 时间旅行式召回。理论上可以问用户在 X 时间点的偏好是什么,按生效时间过滤即可。这是条模型完全做不到的。

矛盾处理:从删除转向失效

这里和前面别让 LLM 直接删的教训正好呼应。那个教训是别删,这里给了正面答案:矛盾不删,标失效。演化引擎判定新事实与旧记忆矛盾时,不是软删旧记忆,而是把旧记忆标记为被新记忆取代,记下取代关系和时间。效果上召回时旧的退出(和软删一样),但信息不丢、关系可查。这几乎是零成本的改进,因为本来就是软删(不物理删),只是把语义从删除改成被取代,再把取代关系存进原因字段或新字段。

落地路径:针对会被新事实推翻的事实型记忆,加一个失效时间字段(或复用状态机):

  • UPDATE / DELETE 决策发生时,别直接覆盖或软删旧记忆,而是给旧记忆打上失效时间,状态转为被取代态。
  • 召回默认只看当前有效的(失效时间为空或未来)。
  • 查历史时把被取代的版本一并拉出来,真正做到记忆的时间线可追溯。

这其实是把变更日志记录历史升级成记忆本身就携带时间线。变更日志已经记了变更前内容,信息是有的,只是散在日志里、不能直接召回;区间模型把它变成一等公民

实体抽取:渐进式路径

实体关系预留了,但具体怎么做?图模型的思路:抽取时不只抽事实文本,还抽出事实里的实体和关系。渐进式路径分三步:

  1. 第一步(轻量):抽取时顺带抽出关键实体(人名、地点、商品),存进标签字段。不改架构,只是给记忆多打几个结构化标签,能让召回过滤更精准。
  2. 第二步(中期):建实体-记忆的关联表,让关于同一个实体的记忆能聚合召回。
  3. 第三步(远期):真正的图存储,边带时间区间。这是大改,放远期。

图模型还支持自定义实体/边类型。对多租户场景这意味着:不同应用可以声明自己关心的实体类型,比如电商关心商品、出行关心地点,正好对应多租户/开发者接入的扩展方向,抽取策略里可以让调用方声明关注的实体类型。

上下文原语与用户画像

图模型从图里能组装出好几种不同粒度的上下文:

原语 是什么
Facts 单条事实(边),对应我们的记忆
Entities 实体及其属性
Episodes 一段情节(一次交互)
Thread Summaries 会话摘要
User Summary 用户整体画像摘要

最值得补的是 User Summary(用户整体画像)。现在的召回是检索相关的几条,但有些场景 Agent 需要的是这个用户整体什么样,一句话画像,而不是十条零散事实。做法:做一个派生记忆,定期(或召回时)把用户的高权重记忆汇总成一条画像摘要,存为一条特殊的全局记忆。Agent 开场时召回它,就能快速认识用户。这和前面 MemGPT 的 human 块异曲同工,也是统计聚合任务可以扩展去做的事。

图模型的边界

图模型很优雅,但不一定划算,有几个现实约束:

  • 图谱构建更重:每次写入要做实体识别、关系抽取、图更新,比抽事实 + 存向量多好几步,延迟和成本都更高。
  • 核心诉求是快速召回相关记忆,不是推理实体间的复杂关系。后者是图模型的强项,但不是 Agent 记忆的高频需求。
  • 图存储的运维和扩展到大规模用户的复杂度,远高于分片 KV + 向量库。

所以判断是:时间模型(区间有效性、矛盾不删而失效)该吸收,因为它低成本高收益;但完整的图存储该克制,作为远期选项,别在第一期背这个包袱。 双存储是语义召回系统的标准范式,这个判断没错;图模型是另一条路线,适合关系推理重于语义召回的场景,和本设计的问题域不完全重合。

第十一章 隐私合规与治理

记忆系统存的是用户最私密的信息,合规不是加分项,是硬约束。这一章把治理链路讲清楚。

记忆开关(opt-out)控制链路

写入/召回请求到达后,第一件事是查记忆开关(缓存承载):

  • 全局开关关闭:不新增任何记忆;已有记忆不参与召回(但用户仍可查看/删除)。
  • 应用级开关关闭:该应用场景不新增任何记忆;该应用记忆不召回,全局记忆仍可召回。

一个关键设计是:关闭期间生命周期管理(衰减/归档/清理)继续执行;重新开启后未清理的记忆自动恢复参与召回。这样关闭只是退出服务,不是销毁数据,重开无成本。

被遗忘权实现

能力 实现 时效
删单条/多条 软删标记,实时退出召回 即时
一键清除全部 异步执行 + 实时标记,批量软删 限时生效(如 24h 内)
按维度清除 按 scope 批量软删 限时生效
物理清理 保留期后异步执行,变更记录脱敏 异步

数据保护

  • 传输加密:全链路 TLS 加密通道。
  • 存储加密:内容字段加密存储,密钥由密钥管理系统管理。
  • 第三方隔离:Agent 作为内部服务在授权范围访问,禁止透传记忆内容。
  • 遥测脱敏:日志与监控仅含脱敏统计,不含记忆内容。
  • 数据导出:异步导出用户全部记忆,签名下载链接限时有效。

可解释性

完整保留来源链路(来源会话 ID + 来源消息 ID)与变更日志,任何一条记忆都能追溯到它从哪句话来的、改过几次、为什么改。这是审计合规的基础,也是将来做记忆质量评估的原料。

参考文章

🌙