跳到主要内容

SimpleMem — 架构与原理(总览)

30 秒导读: SimpleMem 是给 LLM agent 用的高效终身记忆栈。一条主张贯穿全部:把记忆语义无损地压缩成高信息密度的存储,让 agent 回忆更多、花的 token 更少。它把四支工作装进一个 simplemem——文本核、多模态 Omni、自进化检索 EvolveMem、跨会话 cross——对外只暴露 from simplemem import SimpleMem 一个入口,靠 auto 路由按你第一次调用的方法自动挑后端。

本章是这套文档的顶层入口(Layer 0 + Layer 1):讲清「这是什么」和「统一包怎么把各支拼起来」。各支内部的原理不在这里展开——那些交给后面各章(见文末阅读地图)。


1. 这是什么(零基础也能懂)

一句话定义: SimpleMem 是一个给 LLM agent 用的长期记忆系统——你把对话/事实/图片喂进去,它压成紧凑的记忆存起来;之后你提问,它按语义(而非关键词)把最相关的那几条捞回来拼成上下文。

它解决谁的什么问题。 假设你在做一个会跨很多天、很多次对话陪着用户的 agent。你面前只有两条老路,都不好走:

  • 被动堆原始历史:把所有聊天记录原样塞进上下文——冗余、极度吃 token,窗口很快就爆。
  • 每次现场推理过滤:每轮都跑一遍昂贵的 LLM 推理去筛噪声——慢、贵。

SimpleMem 走第三条:在写入时就把交互压成语义无损的记忆单元(README ## 🌟 Overview,README.md:293-317),把「过滤」这件事从查询时提前到构建时。于是查询时不用再啃海量原文,回忆更全、花的 token 反而少一个数量级。

这个包能做什么。 一个 simplemem 包里合了四支互补的工作。前三支(文本核、Omni、EvolveMem)对应 README ## 🌟 Overview 里并列讲的三块工作(README.md:295-315);第四支 cross(跨会话)在仓库里是独立子系统(cross/ 目录),README 以单独条目呈现——本文把它一并归纳进「四支」,是本套文档为读者做的统一梳理:

支柱一句话职责面向
SimpleMem(文本核)三阶段语义无损压缩 + 意图感知检索,纯文本效率基线
Omni-SimpleMem把压缩优先的思路扩到文/图/音/视频四模态多模态记忆
EvolveMem检索机制本身自进化(评估→诊断→提议→护栏)自动调优
cross跨会话记忆:会话钩子、consolidation、上下文注入长期陪伴

用起来什么样(最小示例)。 auto 模式下,你甚至不用声明用哪个后端——第一次调用的方法就替你选好了:

from simplemem import SimpleMem

mem = SimpleMem() # mode="auto",后端待定

# 第一次调用是 add_dialogue → 自动选中「文本」后端
mem.add_dialogue("Alice", "Bob, 明天下午 2 点星巴克见", "2025-11-15T14:30:00")
mem.add_dialogue("Bob", "好,我带上市场分析报告", "2025-11-15T14:31:00")
mem.finalize() # 收尾:把对话压成记忆并建索引

answer = mem.ask("Alice 和 Bob 什么时候、在哪见面?")
# → "16 November 2025 at 2:00 PM at Starbucks"

这段来自 README ## 🚀 Quick Start(README.md:171-210)。一条记忆的生命周期就三步:存 → 建索引 → 按语义召回(README.md:159-165)。

一句话直觉/类比。 把 SimpleMem 当成 agent 的「会做笔记的秘书」:它不把每句话都录下来(那是录音机,费磁带),而是听完一段就记要点、标好时间、去掉重复,等你要用时按意思翻笔记——翻得快,还翻得准。

本节不出现底层代码细节。目标:完全不懂的人读完知道「这是干嘛的」。各支内部怎么压、怎么召回,见后续章节。


2. 顶层全景(统一包怎么转)

这一节讲最外层入口:一个统一包 + auto 路由,是怎么把四支工作粘在一起、又不逼你提前做选择的。

2.1 一张图看清「一个包,四支工作」

先看整体结构。从上到下是「你的入口 → 路由层 → 各后端」;auto 路由的关键是惰性——真正的后端在你第一次调用某个方法时才被造出来。

你的代码
from simplemem import SimpleMem


┌───────────────────────────────────────────────┐
│ SimpleMem = AutoMemory (router.py) │ ← 只是个空壳门面
│ mode="auto":后端待定,记住 kwargs,先不造后端 │
└───────────────────────────────────────────────┘
│ 第一次调用某方法时,按方法名惰性选后端

├── add_dialogue / finalize / ask ──► ┌──────────────────────────┐
│ │ text 后端 │
│ │ SimpleMemSystem │──► 见 01 / 02 章
│ │ 三阶段压缩 + 混合检索 │
│ └──────────────────────────┘

└── add_text / add_image / ┌──────────────────────────┐
add_audio / add_video / query ──► │ omni 后端 │
│ OmniMemoryOrchestrator │──► 见 04 章
│ 多模态压缩 + 渐进检索 │
└──────────────────────────┘

┌───────────────────────────────────────────────┐
│ simplemem.optimize(mem, dev_questions) │ ← 离线调优,另一条入口
│ 取出 mem._backend,喂给 EvolveMem 自进化循环 │──► 见 03 章
│ 产出一个 Config,可 save / 之后 load 部署 │
└───────────────────────────────────────────────┘

┌───────────────────────────────────────────────┐
│ cross 跨会话记忆 (cross/ 独立子系统) │ ← 不在 auto 路由内
│ 会话钩子 → 双存储 → 合并 → 下次开场注入 │──► 见 04 章
└───────────────────────────────────────────────┘

怎么读这张图:上半部是运行时的记忆读写(auto 按第一次调用选文本或多模态);中间 optimize()离线的检索调优,独立于上半部,产物是一个可持久化的 Config;底部 cross 是独立子系统,有自己的入口,不经 auto 路由(见 §4)。

2.2 部件一句话职责表

顶层入口只涉及少数几个符号,先把它们钉死:

部件干什么文件:符号
SimpleMem 别名对外入口名,其实就是 AutoMemorysimplemem/__init__.py:21(from ... import AutoMemory as SimpleMem)
create()工厂:mode="auto" 返回 AutoMemory,否则查注册表造对应后端simplemem/router.py:169(create)
AutoMemory门面/惰性路由器:记住 kwargs,首个方法调用决定后端simplemem/router.py:253(AutoMemory)
_Backend单个后端的元数据 + 惰性加载器(import 路径、类名、依赖)simplemem/router.py:46(_Backend)
_registry{mode: _Backend} 注册表simplemem/router.py:103(_registry)
register()往注册表登记一个后端simplemem/router.py:106(register)
optimize()薄封装:把 mem 交给 EvolveMem 调检索,返回 Configsimplemem/__init__.py:25(optimize)
Config统一检索配置对象(optimize 的产物、推理时的输入)simplemem/config.py:15(Config)

2.3 主线走一遍(高层,不进代码)

把上面的图翻成一句话流程:

  1. SimpleMem() 造出一个 AutoMemory 空壳,_backend=None_mode=None,只把构造参数存进 self._kwargs(router.py:280-283)。
  2. 你调 add_dialogue(...)。它内部先 self._require("text", "add_dialogue")(router.py:312-317),发现还没后端,就初始化 text 后端。
  3. 之后所有调用都被转发给这同一个后端;若你再调一个属于 omni 的方法(如 add_image),会直接报错——后端一旦选定,终身不可换(router.py:295-308)。
  4. 想调优检索?另走一条线:simplemem.optimize(mem, dev_questions) 取出 mem._backend 喂给 EvolveMem,跑完返回一个 Config(__init__.py:25-50)。

目标:看懂「大盘」——一个包,两条入口(运行时读写 / 离线调优),外加一个独立的 cross 子系统,共四支工作。


3. 统一包 + auto 路由(最外层入口的原理)

这是本章唯一深入的机制。它借鉴了两个熟悉的模式(router.py:5 注释自陈):Mem0 的 provider 工厂HuggingFace 的 AutoModel——「一个入口名,按线索自动挑真正的实现」。

3.1 注册表:后端只登记、不加载

问题:包里有 text 和 omni 两个后端,omni 还依赖重型多模态库。如果 import 包时就把两个后端全加载,慢且可能因缺依赖直接崩。

思路:先登记元数据,用到才 import。每个后端用一个 _Backend 描述——记住它的 import 路径、类名、依赖包名、可选的自定义构造器,但不真的 import(router.py:46-96)。register() 只是把这个描述塞进 _registry 字典(router.py:106-146)。

包加载时,router.py 末尾登记了两个内置后端:

# simplemem/router.py:221 与 :240,示意其形,非逐字
register(mode="text", module_path="simplemem.text.system",
class_name="SimpleMemSystem",
description="Single-modal text memory with semantic lossless compression")

register(mode="omni", module_path="simplemem.multimodal.orchestrator",
class_name="OmniMemoryOrchestrator",
description="Multimodal memory — text, image, audio, video",
init=_init_omni) # omni 需要自定义构造:没给 config 就补一个默认的

真正的「重活」推迟到 _Backend.load_class():它才去跑可选的 setup()、检查依赖 check_deps()importlib.import_module 加载类(router.py:85-96)。omni 后端还带一个 init=_init_omni,在没传 config 时补一个默认 OmniMemoryConfig(router.py:233-237)——这就是为什么 text 和 omni 构造签名不同也能塞进同一个工厂。

3.2 create():工厂的分叉点

create(mode="auto", **kwargs) 是所有构造的总闸(router.py:169-214)。它只有两条岔路:

  • mode == "auto" → 返回一个 AutoMemory,什么后端都还没造(router.py:200-201)。
  • 否则 → 查注册表,load_class() 拿到类,有 init 就用 init,没有就 cls(**kwargs)(router.py:203-214)。

SimpleMem() 默认走第一条,所以你拿到的其实是个待定的门面。

3.3 AutoMemory:第一次调用定终身

AutoMemory 是门面,它的巧妙全在两个内部方法:

  • _init_backend(mode):只跑一次,create(mode=mode, **self._kwargs) 把真后端造出来存进 self._backend(router.py:287-293)。
  • _require(target_mode, method):每个公开方法开头都调它。逻辑很短——没后端就初始化成 target_mode;已有后端但模式不符,就抛一条带修复建议RuntimeError(router.py:295-308)。

于是「按第一次调用选后端」不是什么魔法,就是每个方法自报家门:文本类方法报 "text",多模态类方法报 "omni"。下面这段示意把机制演出来:

# 示意,非源码 —— 复刻 AutoMemory 的路由骨架
class AutoMemory:
def __init__(self, **kwargs):
self._kwargs = kwargs
self._backend = None # 后端待定
self._mode = None

def _require(self, target_mode, method):
if self._backend is None: # 第一次调用:惰性造后端
self._mode = target_mode
self._backend = create(mode=target_mode, **self._kwargs)
elif self._mode != target_mode: # 事后想换模式:直接拒绝
raise RuntimeError(f"{method}() 需要 {target_mode} 模式,"
f"但已被前一次调用锁成 {self._mode} 模式")

def add_dialogue(self, *a): # 报 text
self._require("text", "add_dialogue"); return self._backend.add_dialogue(*a)

def add_image(self, *a): # 报 omni
self._require("omni", "add_image"); return self._backend.add_image(*a)

重点看两处:一是 _requireself._backend is None 那个分支——惰性就在这;二是 self._mode != target_mode 分支——终身锁定就在这。真实实现见 router.py:295-308(_require)、router.py:312-359(各转发方法)。

哪些方法路由到哪个后端(router.py:310-359,AutoMemory.__doc__ 也列了同表):

方法路由到目的
add_dialogue / add_dialogues / finalize / ask / get_all_memoriestext对话式文本记忆
add_text / add_image / add_audio / add_video / queryomni多模态记忆
close / mode 属性共享释放资源 / 查当前模式

3.4 optimize():把 EvolveMem 接进来

optimize() 是另一条入口,和 auto 路由平行,负责离线调检索。它在 __init__.py 里是个薄封装(__init__.py:25-50):延迟 import,再转调 simplemem.evolver.optimize.run_optimization

它怎么接到 EvolveMem:run_optimization_resolve_backend(mem)——即 getattr(mem, "_backend", mem),把 AutoMemory 门面剥开,拿到里面真正的后端(evolver/optimize.py:97-99),再从后端取出已建好的记忆和 LLM/embedder 句柄,喂给 EvolutionEngine 跑循环(evolver/optimize.py:44-60)。

一个诚实的边界:这里的 optimize() 是 EvolveMem 的降级(degraded)模式,不是论文对齐的完整入口——它关掉了 per-category 覆盖、adapter 提示词、extractor 进化,只在全局检索超参上搜索(evolver/optimize.py:1-19 的模块 docstring 明说)。要复现论文数字,得用 EvolveMem/run_evolution.py。完整机制见 03-evolvemem.md

产物 Config 是个 dataclass(config.py:15),装着各通道 top_k、context 预算、fusion 模式与权重、几个增强开关、以及 evolved / evolution_rounds 等进化元数据(config.py:14-49),可 save() 成 JSON、之后 load_config() 回来用 SimpleMem(config=config) 部署。


4. 边界与局限(顶层入口这一层)

只讲统一包/路由这一层的边界,各支自身的局限见对应章节。

  • 后端终身锁定。 一个 AutoMemory 实例选了 text 就不能再用 omni 的方法,反之亦然;要换得新建实例(router.py:295-308AutoMemory docstring router.py:264-267)。
  • auto 只在 text/omni 之间路由。 cross(跨会话)和 EvolveMem 不在 auto 的方法表里——cross 是独立子系统、有自己的入口(见 04-omni-and-cross.md),EvolveMem 通过 optimize() 接入。
  • optimize() 是降级版。 见 §3.4,不等于论文完整流水线。
  • omni 依赖更重。 _Backend.check_deps() 会在加载 omni 后端时校验依赖(router.py:70-83);缺包会抛带 pip install 提示的 ImportError,而非静默失败。
  • 能力按渠道不同。 Python 包支持文本 + 多模态 + optimize();MCP server 目前只有文本记忆,多模态与自进化检索是 planned(README.md:508-522 的 Roadmap 表)。

5. 阅读地图(各章顺序与选章指引)

本文档把 SimpleMem 拆成一个总览 + 四章。建议按下面顺序读;赶时间可按「你想干嘛」直接跳章。

建议顺序(由浅入深):

index(你在这)──► 01 文本管线 ──► 02 混合检索 ──► 03 EvolveMem ──► 04 Omni & Cross
顶层入口 怎么压 怎么召回 怎么自进化 多模态 + 跨会话
想搞清楚…去这一章
一个包怎么把四支拼起来、auto 怎么选后端本章 index.md
对话怎么被三阶段压成语义无损的记忆单元01-text-pipeline.md
一次提问怎么经意图规划 + 三视图召回 + 反思拿到答案02-hybrid-retrieval.md
optimize() 背后的评估→诊断→提议→护栏闭环03-evolvemem.md
图/音/视频记忆,以及跨会话的 cross 怎么工作04-omni-and-cross.md

给 agent 的选章提示: 任务里出现 add_dialogue/ask/压缩/去重 → 读 01;retrieve/planning/reflection/fusion → 读 02;optimize/evolve/diagnosis/config 调优 → 读 03;image/audio/video/session/context injection → 读 04。


6. 代码地图(导航索引)

顶层入口层的关键符号,agent 可凭符号名 grep 定位(比行号抗漂移):

主题文件符号
SimpleMem 对外别名simplemem/__init__.pySimpleMem(= AutoMemory)
optimize 薄封装simplemem/__init__.pyoptimize
工厂总闸simplemem/router.pycreate
惰性路由门面simplemem/router.pyAutoMemory_require_init_backend
后端描述符 / 惰性加载simplemem/router.py_Backendload_classcheck_deps
后端注册表simplemem/router.py_registryregisterlist_modes
omni 自定义构造simplemem/router.py_init_omni
统一检索配置simplemem/config.pyConfigload_config
optimize 实现 / 剥门面simplemem/evolver/optimize.pyrun_optimization_resolve_backend
text 后端类simplemem/text/system.pySimpleMemSystem
omni 后端类simplemem/multimodal/orchestrator.pyOmniMemoryOrchestrator
cross 跨会话入口cross/orchestrator.pyCrossMemOrchestratorcreate_orchestrator