先分清会话记忆与长期记忆——Hy Memory 2.0 的存储后端怎么选

文章导读
想给智能体加记忆,真正要先定的不是选哪个数据库,而是先把记忆拆成几堆,再看每堆的寿命和读取方式。会话原话通常只在会话活跃期被读到,长期事实则要跨会话反复查。这两类数据塞进同一个后端一般不会出大问题,但会带来两个后续成本:一是原话把存储撑大,二是换后端时事实和原话混在一张表里没法分头迁。比较稳妥的做法是同一套 ID 与字段规范,分开的存储落点,中间用一层路由配置隔开。
📋 目录
  1. Ⅰ 先列出要存的三类东西:原话、摘要、事实
  2. Ⅱ 按读取频率和寿命给三类数据分堆
  3. Ⅲ 对照几种存储后端的写入与查询方式
  4. Ⅳ 用配置切换后端跑同一段对话
  5. Ⅴ 留一条迁移路径:字段与 ID 设计
A A

想给智能体加记忆,真正要先定的不是选哪个数据库,而是先把记忆拆成几堆,再看每堆的寿命和读取方式。会话原话通常只在会话活跃期被读到,长期事实则要跨会话反复查。这两类数据塞进同一个后端一般不会出大问题,但会带来两个后续成本:一是原话把存储撑大,二是换后端时事实和原话混在一张表里没法分头迁。比较稳妥的做法是同一套 ID 与字段规范,分开的存储落点,中间用一层路由配置隔开。

适用场景:给智能体接记忆,还没定存储后端,担心以后迁移。操作动作:把记忆拆成原话、摘要、事实三类,按读多写少、写多读少、需长期保留分堆,再决定哪堆放带 TTL 的缓存、哪堆放可持久化存储。验证方式:同一段固定对话在两种后端配置下各跑一次,比对写入条数、按时间范围查询的顺序、事实命中结果。风险边界:Hy Memory 2.0 的驱动名、参数名和默认值要以自己环境的配置文件为准,下文给的是可替换骨架,不是官方接口说明。

先列出要存的三类东西:原话、摘要、事实

混在一张表里,最常见的后果是查询条件越来越长、字段越来越多,最后没人说得清哪列是必填。先按内容形态拆开,每类单独定样例和体积量级,后面选后端才有依据。

  • 原话(raw turn):用户与模型的一轮或多轮原始消息,带角色、会话 ID、时间。样例:role=user,content=「帮我查下上周的报销单」,session_id=s-1001,turn_index=3。体积估算方式:单条按正文字节数加上固定元数据(角色、时间、会话 ID 这类字段,通常一两百字节)估算,再乘日均轮次。
  • 摘要(summary):对一段会话窗口的压缩描述,一条对应一个窗口。样例:session_id=s-1001,window=turn 1-12,text=「用户咨询报销周期与提交入口」。体积估算方式:窗口数乘单条摘要长度,量级通常在原话的十分之一上下,具体要看摘要长度怎么设。
  • 事实(fact):从对话里抽出的结构化断言,比如用户偏好、实体属性。样例:subject=user-42,predicate=报销周期,object=每月5日,source_session=s-1001。体积估算方式:实体数乘每实体事实条数,单条通常几百字节。

先写这三段样例,不是为了好看,而是为了确认字段。三类数据的样例如果对不上同一套字段名,说明还没拆干净。

按读取频率和寿命给三类数据分堆

分堆的依据是访问特征,不是习惯。会话相关就放缓存、长期记忆就放数据库,这种按名字对号入座的做法常常把「写多读少」的数据放进了一个按查询优化的存储里。

数据类访问特征建议介质过期策略迁移风险
原话写多读少,读通常只按 session 回放带 TTL 的 KV 缓存,必要时追加一份落盘日志按会话活跃期设 TTL,过期即弃低,丢了可接受
摘要读写中等,常随会话列表展示可持久化的文档型存储,按 session_id 加时间建索引长期保留,不做自动过期中,重算成本取决于摘要模型
事实读多写少,需按主体或谓词过滤支持条件查询的持久化存储,关系型或带索引的文档库不设 TTL,靠版本或状态字段标记失效高,依赖 ID 稳定

这张表可以直接当作评审材料:原话属于写多读少、摘要属于需要长期保留但读得不频繁、事实属于读多写少且必须长期保留。三类里只有原话适合用 TTL 收口,摘要和事实如果设了短 TTL,表现就是隔几天对话就「失忆」,而且这种问题在日志里往往只表现为一次空召回。

对照几种存储后端的写入与查询方式

选型时容易被「快不快」带偏,实际更影响维护成本的是查询方式:能不能按时间过滤、能不能按字段过滤。下面按写入延迟、查询方式、是否支持按时间过滤三个维度对比常见后端,写入延迟只给相对判断,具体数值需要在目标机器上用一段脚本压几十次取中位数。

后端类型写入延迟查询方式按时间过滤
进程内内存表最低,无网络开销只能按 key 命中需自己遍历,不支持
带 TTL 的 KV 缓存低,一次网络往返按 key 读写,部分支持前缀扫描一般不直接支持,需另建索引
文档型存储中等按字段条件查询,可建组合索引多数支持时间范围
关系型存储中等,事务一致性可控SQL 条件查询、联表天然支持,配合索引效率稳定
追加型日志或对象存储写入吞吐好以扫描为主,不适合在线点查靠分区或前缀实现

按这张表,原话落在 KV 缓存、事实落在关系型或带索引的文档库是比较自然的组合。如果团队希望只维护一套存储,通常的取舍是把原话也放进同一个持久化存储,用 TTL 或定期清理代替缓存过期,代价是写入压力上去,好处是回放和审计更容易。

先分清会话记忆与长期记忆——Hy Memory 2.0 的存储后端怎么选

用配置切换后端跑同一段对话

切换是否真的等价,只能靠跑同一段对话来验证。下面是一个通用接入骨架,驱动名和字段名都是占位,需要替换成自己环境里注册的名字。

memory:
  backends:
    raw_turn:
      driver: cache-kv          # 占位:换成环境里注册的驱动名
      ttl_seconds: 604800       # 只对原话生效,按会话活跃期调整
      namespace: hy.raw
    summary:
      driver: doc-store
      index_fields: [session_id, created_at]
    fact:
      driver: sql
      table: memory_fact
  routing:
    - match: {type: raw_turn}
      backend: raw_turn
    - match: {type: summary}
      backend: summary
    - match: {type: fact}
      backend: fact

几个占位字段的用途:driver 决定实际落点,换后端基本只改这一行;ttl_seconds 只影响原话,改动后要确认旧 key 的过期行为;index_fields 决定能否按时间范围过滤,缺了 created_at 就只能全量扫;routing 是写入分流规则,改这里等于换落点,建议改动前后各留一份配置快照。

验证方法:固定一段 8 到 12 轮的对话脚本,在两套配置下各跑一次,导出写入记录和一次查询结果做对比。重点看三件事——三类数据的写入条数是否一致、按 session_id 加时间范围查询返回的顺序是否一致、事实查询是否命中同一批 subject。差异最常见的地方是时间精度(秒和毫秒混用)和默认排序,先对齐这两项,再判断后端本身有没有问题。

留一条迁移路径:字段与 ID 设计

迁移成本基本在建模阶段就定了。三类数据如果 ID 体系不同、时间字段含义不同、来源会话字段有的有一半没有,换后端时只能重算一遍。

  • 统一 ID:每条记忆一个全局唯一 memory_id,用带类别前缀的形式区分类别,比如 raw_、sum_、fact_ 开头,取值用 ULID 或 uuid,不要拿自增主键当对外 ID。
  • 时间戳拆两个:created_at 表示事件实际发生的时间,stored_at 表示写入存储的时间。查询时明确说清按哪个过滤,否则换后端后排序会变,回放顺序对不上。
  • 来源会话:session_id、turn_index、source 三个字段在三类数据里都要有。原话和摘要天然带着,事实是在抽取阶段生成的,最容易丢,要在抽取模板里强制写入,不能依赖后补。
  • 原样保留:每类数据留一个 payload 或 extra 字段存原始结构。换后端时先跑一段双写,读旧写新,等新后端查询结果核对一致再停旧写入,不要先删旧数据。

这套字段定义不依赖具体后端,先从文档型迁到关系型、或者反过来,改的是配置和数据搬运脚本,不是上层调用。真的到了要换的那天,验证标准仍然是那三条:写入条数、按时间过滤的顺序、事实命中结果。