数据截至 (上游 commit db80b6a7c591)
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 别名 | 对外入口名,其实就是 AutoMemory | simplemem/__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 调检索,返回 Config | simplemem/__init__.py:25(optimize) |
Config | 统一检索配置对象(optimize 的产物、推理时的输入) | simplemem/config.py:15(Config) |
2.3 主线走一遍(高层,不进代码)
把上面的图翻成一句话流程:
SimpleMem()造出一个AutoMemory空壳,_backend=None、_mode=None,只把构造参数存进self._kwargs(router.py:280-283)。- 你调
add_dialogue(...)。它内部先self._require("text", "add_dialogue")(router.py:312-317),发现还没后端,就初始化 text 后端。 - 之后所有调用都被转发给这同一个后端;若你再调一个属于 omni 的方法(如
add_image),会直接报错——后端一旦选定,终身不可换(router.py:295-308)。 - 想调优检索?另走一条线:
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)
重点看两处:一是 _require 里 self._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_memories | text | 对话式文本记忆 |
add_text / add_image / add_audio / add_video / query | omni | 多模态记忆 |
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-308、AutoMemorydocstringrouter.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__.py | SimpleMem(= AutoMemory) |
| optimize 薄封装 | simplemem/__init__.py | optimize |
| 工厂总闸 | simplemem/router.py | create |
| 惰性路由门面 | simplemem/router.py | AutoMemory、_require、_init_backend |
| 后端描述符 / 惰性加载 | simplemem/router.py | _Backend、load_class、check_deps |
| 后端注册表 | simplemem/router.py | _registry、register、list_modes |
| omni 自定义构造 | simplemem/router.py | _init_omni |
| 统一检索配置 | simplemem/config.py | Config、load_config |
| optimize 实现 / 剥门面 | simplemem/evolver/optimize.py | run_optimization、_resolve_backend |
| text 后端类 | simplemem/text/system.py | SimpleMemSystem |
| omni 后端类 | simplemem/multimodal/orchestrator.py | OmniMemoryOrchestrator |
| cross 跨会话入口 | cross/orchestrator.py | CrossMemOrchestrator、create_orchestrator |