一、为什么 Agent 需要一层独立的记忆:上下文窗口救不了长期记忆
过去两年,几乎所有做过 LLM Agent 的工程师都踩过同一个坑:Agent 在单次会话里表现聪明,但换个会话、隔了一天,它就彻底”失忆”。你上一轮告诉它”我的项目用 PostgreSQL,不要用 MongoDB”,这一轮它照样给你生成 MongoDB 的连接代码。问题的根源在于,LLM 的”记忆”本质上只是上下文窗口里的 token,窗口一满、会话一断,一切归零。

把所有历史塞进上下文窗口是一条死胡同。原因有三层:
1.1 上下文窗口的三重局限
第一是成本。主流模型按输入 token 计费,一个 Agent 如果每次请求都带上 10 万 token 的历史对话,单次调用成本可能是只带 2 千 token 系统提示的几十倍。长期运行的生产 Agent,账单会以指数速度膨胀。
第二是注意力稀释。即使模型宣称支持 128K 甚至 1M 上下文,实际测试反复证明:关键信息埋在超长上下文中段时,模型的召回率会显著下降——这就是常说的”lost in the middle”。上下文越长,模型越容易漏掉真正重要的那条用户偏好。
第三是结构缺失。对话历史是线性的、无序的、冗余的。用户三个月里说过 200 次自己的技术栈,这 200 次都躺在历史里,但没有任何机制告诉模型”这些信息等价,保留一份即可”。没有结构,就没有可靠的检索;没有可靠的检索,塞再长也没用。
1.2 独立记忆层的正确形态
于是”给 Agent 外挂一层持久记忆”成为共识。但外挂记忆怎么做,业界分歧很大:有人拿向量数据库存 embedding 做语义检索,有人用 RAG 思路把历史对话切块回灌。这些方案共同的问题是——记忆是”一袋子文本块”,没有类型、没有结构、没有关系。Agent 取回的永远是一堆相似度最高的句子,而不是一条结构化的”用户画像更新”。
这正是 Memu 的切入点。Memu 是 NevaMind-AI 出品的开源 Python Agent 记忆框架,采用 Apache-2.0 协议,定位非常清晰:给任意 LLM Agent 加一层持久记忆。它最反直觉也最有意思的一点是——核心代码只有约 500 行,相比同类动辄数万行的实现大幅精简。它用一套类型化记忆图谱(typed memory graph)来组织记忆,而不是无结构的向量堆。本文的主线就是讲清楚:这套 500 行的 Memu 记忆图谱怎么部署,怎么从 SQLite 起步,再平滑扩容到 PostgreSQL。
二、Memu 核心抽象:typed memory graph 与四类节点
在动手部署之前,必须先理解 Memu 的数据模型,否则后面的建表、写入、查询代码都会变成抄写天书。Memu 的记忆图谱(memory graph)由四类节点构成:Resources、MemoryItems、Categories、Relations。
2.1 Resources:记忆的”来源实体”
Resources 代表记忆的来源或宿主,通常是一个用户、一个 Agent、一个会话或者一份外部文档。你可以把它理解为记忆图谱的”锚点”:每条记忆都必须挂在某个 Resource 上。比如”用户 alice 的偏好”、”客服机器人 bot-01 的经验”,这里的 alice 和 bot-01 就是 Resource 节点。设计上,Resources 让记忆具备了归属边界——不同用户的记忆天然隔离,这在多租户场景下是刚需。
2.2 MemoryItems:最小记忆单元
MemoryItems 是真正的记忆条目,是图谱里的”叶子内容”。一条 MemoryItem 通常是一句被提炼过的陈述句,例如”用户偏好使用 PostgreSQL 而非 MongoDB”、”该项目部署在华东区域”。注意,MemoryItem 强调的是提炼后的事实,而不是原始对话原文。Memu 的设计哲学是:记忆写入时就要做归一化,把对话里冗长、模糊的表述压缩成一条条原子化的事实。这样后续检索时,Agent 拿到的直接是可用结论,而不是需要二次理解的原始文本。
2.3 Categories:记忆的类型标签
Categories 给 MemoryItems 提供分类维度。你可以按业务语义划分,比如 preference(偏好)、fact(事实)、skill(技能)、experience(经历)、task(任务),也可以按时间衰减属性划分,比如 short-term 与 long-term。类别存在的好处是检索时可以先过滤——用户问”我之前是怎么配置数据库的”,Agent 可以只在 fact / experience 类目里搜,命中率远高于全库语义扫描。
2.4 Relations:把记忆连成图
Relations 是四类节点中最容易被低估的一类。它描述节点之间的边:某条 MemoryItem 属于某个 Resource、某条记忆从属于某个 Category、某条记忆派生自另一条记忆(比如”用户喜欢深色模式”与”用户昨晚把 App 切到了深色模式”之间是印证关系)。有了 Relations,记忆不再是孤立条目,而是一张可遍历的图谱——Agent 可以沿着关系链做推理式召回,例如从”用户是后端工程师”这条记忆出发,顺着 Relations 找到它关联的所有技术栈偏好。
2.5 四类节点职责速查表
| 节点类型 | 图谱角色 | 典型内容示例 | 写入时机 |
|---|---|---|---|
| Resources | 记忆来源与归属锚点 | 用户 alice、Agent bot-01、会话 sess-42 | 会话开始或实体注册时 |
| MemoryItems | 最小记忆单元(原子事实) | “偏好 PostgreSQL”、”部署在华东” | 对话提炼后持续写入 |
| Categories | 记忆分类维度 | preference / fact / task / experience | 建库时预置,随业务扩展 |
| Relations | 节点之间的边(结构) | item 属于 resource、item 归入 category | 写入 MemoryItem 时同步建立 |
2.6 独到视角:typed graph 与向量库的记忆检索差异
这里值得展开说说我的观察。向量库方案的本质是相似度召回:查询文本和记忆文本算 embedding 距离,取 top-k。它擅长”模糊找相似”,但有三个结构性缺陷:其一,相似不等于相关,”用户喜欢 PostgreSQL”和”PostgreSQL 14 发布了新特性”在向量空间里可能很近,但后者对个性化毫无价值;其二,向量库记不住”关系”,两个各自正确的记忆条目之间可能存在矛盾或因果,向量空间无法表达;其三,向量检索没有归属边界,多租户隔离要靠外挂元数据过滤,容易出隐私事故。
typed memory graph 走的是结构优先的路线:先按 Resource / Category 收窄范围,再在结构内做检索。代价是写入口径要求更高(你得把记忆提炼成原子事实并正确归类),收益是召回可控、可解释、可审计。我的判断是:500 行的 Memu 并不是要取代向量库,而是把”记忆应该有结构”这件事用最少的代码做对——这也是它敢于把核心压缩到 500 行的底气:删掉的不是功能,而是抽象税。
三、安装与环境准备:十分钟跑通 Memu 部署
Memu 是标准 Python 包,环境要求很宽松。建议用 Python 3.10+,并用虚拟环境隔离依赖。
3.1 创建虚拟环境并安装
以下命令覆盖了从创建环境到验证安装的完整流程,这也是 Memu 部署的第一步:
# 创建并激活虚拟环境(推荐 venv 或 uv)
python3 -m venv .venv
source .venv/bin/activate
# 安装 Memu
pip install memu
# 验证安装
python -c "import memu; print(memu.__version__)"
# 可选:如需 PostgreSQL 后端,一并安装 psycopg 驱动
pip install "psycopg[binary]"
3.2 环境清单
部署 Memu 所需的东西非常少,这也是它作为”500 行轻量框架”的直观体现:
| 组件 | 最低要求 | 说明 |
|---|---|---|
| Python | 3.10+ | 建议 3.11,类型提示体验最好 |
| LLM 接入 | 任意 OpenAI 兼容 API | 记忆提炼(对话→原子事实)需要调用 LLM |
| 存储后端 | SQLite(零配置) | 生产建议 PostgreSQL |
| 磁盘 | < 100MB | 记忆图谱本体很小,SQLite 文件按条目线性增长 |
3.3 关于 LLM 依赖的说明
Memu 的记忆写入流程里有一步”对话 → 原子事实”的提炼,这一步需要 LLM 参与。工程实践上建议用便宜、快的模型(如各家的轻量档)做提炼,用主模型做对话——记忆层的 LLM 调用频率远低于主循环,成本占比通常不到 5%。
四、SQLite 后端最小起步:十行初始化,零运维成本
SQLite 是 Memu 的默认后端,也是个人项目、原型验证、单机部署的最佳起点。零安装、零配置、单文件存储,整个记忆图谱就是一个 .db 文件,备份等于复制文件。
4.1 最小可运行模板
下面这段代码是 Memu 部署的”最小起步模板”,包含初始化、配置 LLM、指定 SQLite 路径三件事:
from memu import MemuClient
# SQLite 起步:一个文件承载整个记忆图谱
memory = MemuClient(
llm_config={
"provider": "openai",
"api_key": "sk-...", # 建议改从环境变量读取
"model": "gpt-4o-mini", # 提炼用轻量模型即可
},
database_url="sqlite:///./agent_memory.db",
)
# 建库(首次运行时执行,幂等)
memory.initialize()
print("记忆图谱就绪:agent_memory.db")
跑完之后,工作目录下会生成 agent_memory.db。用任意 SQLite 客户端打开,你就能看到 Memu 为四类节点建的表结构——这也是学习 typed memory graph 最直观的方式:直接读表结构,比读文档更快。
4.2 什么规模下 SQLite 足够
凭经验给出一条务实界线:单机、单进程、写入 QPS 低于 50、记忆条目在 50 万条以内,SQLite 都能稳稳扛住。Agent 记忆的写入模式是”低频、小事务”,和 SQLite 的单写者模型非常契合。也就是说,绝大多数个人 Agent 和中小规模内部工具,用 SQLite 起步完全够用,不必一上来就上 PostgreSQL。
五、四类节点的写入与查询:让记忆图谱真正转起来
框架跑起来只是第一步,真正的价值在于按 Memu 的模型去写入和检索记忆。本节给出四类节点完整生命周期的代码。
5.1 注册 Resource 并写入 MemoryItem
from memu import MemuClient
memory = MemuClient(
llm_config={"provider": "openai", "api_key": "sk-...", "model": "gpt-4o-mini"},
database_url="sqlite:///./agent_memory.db",
)
memory.initialize()
# 1) 注册 Resource(记忆归属锚点)
user = memory.resources.create(
resource_type="user",
resource_id="alice",
)
# 2) 从对话中提炼并写入 MemoryItem(LLM 自动完成提炼)
item = memory.memories.create(
resource_id=user.id,
category="preference",
text="用户明确要求后端只使用 PostgreSQL,拒绝 MongoDB 方案。",
)
# 3) 也支持直接写入手工提炼好的原子事实(跳过 LLM,零成本)
memory.memories.create(
resource_id=user.id,
category="fact",
text="alice 的生产环境部署在华东区域。",
)
# 4) 建立 Relations:把新记忆与旧记忆关联(印证/派生关系)
related = memory.memories.search(resource_id=user.id, query="数据库", top_k=3)
for m in related:
memory.relations.create(source_id=item.id, target_id=m.id, relation_type="related_to")
5.2 检索:结构优先的两段式召回
# 检索:先按 Resource + Category 收窄结构范围,再在范围内匹配
results = memory.memories.search(
resource_id="alice",
category="preference", # 可选:按 Categories 过滤
query="该用什么数据库?",
top_k=5,
)
for r in results:
print(f"[{r.category}] {r.text} (score={r.score:.3f})")
# 图谱遍历:从一条记忆出发,沿 Relations 找关联记忆
neighbors = memory.relations.neighbors(item.id, depth=2)
注意检索代码里的两个参数:resource_id 和 category。这正是 typed memory graph 相对”裸向量库”的核心差异——范围先于相似度。先限定”这是 alice 的偏好类记忆”,再做匹配,既提升了命中率,也天然完成了多租户隔离。
5.3 写入节奏建议
实践中不要每轮对话都写入记忆。推荐策略:会话结束时批量提炼一次,或每 N 轮触发一次提炼。提炼时把上一段对话连同已有记忆摘要一起喂给 LLM,让它输出”新增 / 更新 / 无变化”三类决策,这样能显著减少重复条目,控制 memory graph 的膨胀速度。
六、PostgreSQL 扩容迁移:从单机文件到生产数据库
当 Agent 从原型走向生产——多实例部署、并发写入、需要 HA 备份——就该切到 PostgreSQL 了。Memu 的好消息是:因为存储层是抽象的,迁移主要是换连接串,四类节点的 API 一行都不用改。
6.1 目标库准备
-- 在 PostgreSQL 服务器上执行
CREATE USER memu WITH PASSWORD '强密码请替换';
CREATE DATABASE agent_memory OWNER memu;
\c agent_memory
GRANT ALL ON SCHEMA public TO memu;
6.2 修改连接配置
from memu import MemuClient
# SQLite -> PostgreSQL:只改 database_url,其余代码零改动
memory = MemuClient(
llm_config={"provider": "openai", "api_key": "sk-..."},
database_url="postgresql://memu:强密码请替换@10.0.0.8:5432/agent_memory",
)
memory.initialize()
生产环境务必把连接串放进环境变量或密钥管理服务,不要硬编码在代码里。若用连接池(推荐 pgBouncer),记得在 URL 上追加 ?pool_size=10 之类的参数按框架文档调整。
6.3 存量数据迁移
从 SQLite 迁移历史记忆图谱,用标准工具即可:
# 方案 A:SQLite 导出 SQL,转换后灌入 PostgreSQL
sqlite3 agent_memory.db .dump > memory_dump.sql
# 处理方言差异(AUTOINCREMENT、PRAGMA 等)后:
psql -h 10.0.0.8 -U memu -d agent_memory -f memory_dump.sql
# 方案 B(更稳):写个双端脚本,逐表读旧写新
python scripts/migrate_sqlite_to_pg.py --from sqlite:///./agent_memory.db \
--to postgresql://memu@10.0.0.8/agent_memory
迁移完成后,务必做数据校验:对比两边 Resources / MemoryItems / Categories / Relations 四张表的行数是否一致,再抽样比对若干条记忆内容。记忆数据丢一条,Agent 就会”遗忘”一件事,这种损失往往比丢业务数据更隐蔽。
6.4 SQLite vs PostgreSQL 对比表
| 维度 | SQLite | PostgreSQL |
|---|---|---|
| 部署成本 | 零配置,单文件 | 需安装运维,建议独立实例或云 RDS |
| 并发模型 | 单写者,读并发好 | 多写者 MVCC,高并发无忧 |
| 适用规模 | ≤ 50 万条记忆、单机单进程 | 百万级以上、多实例横向扩展 |
| 备份 | 复制 .db 文件 | pg_dump / 流复制 / PITR |
| 多租户隔离 | 应用层 Resource 过滤 | 可加 schema 级或 RLS 行级隔离 |
| 迁移路径 | 作为起步默认 | 生产扩容目标,API 零改动切换 |
七、与 Agent 主循环的集成:三层接入模式
记忆层装好了,怎么接进 Agent?我总结为三种由浅入深的模式,按项目阶段递进采用。
7.1 模式一:启动时拉取画像(最简单)
Agent 会话启动时,把该 Resource 下最核心的 N 条记忆拼进系统提示。适合记忆量小的场景:
profile = memory.memories.search(resource_id="alice", top_k=10)
system_prompt += "\n\n## 已知用户记忆\n" + "\n".join(f"- {m.text}" for m in profile)
7.2 模式二:检索式注入(推荐默认)
每轮对话先拿用户消息去记忆图谱检索一次,把 top-k 结果注入上下文。这就是把记忆层当成”Agent 专用的 RAG”来用,配合 resource_id / category 过滤效果最好。
7.3 模式三:会话末批量提炼回写(完整闭环)
会话结束(或每 N 轮)触发一次提炼:LLM 读取本轮对话,产出新增/更新的原子事实,写入 MemoryItems 并建立 Relations。这一步跑通后,记忆层就形成了”读 → 用 → 写”的完整闭环,Agent 具备真正的跨会话学习能力。
集成注意事项
4.1 别让记忆挤占上下文
注入记忆要设上限(建议 500~1000 token),否则又回到”上下文爆炸”的老路。记忆层存在的意义就是让上下文更精炼,不是更臃肿。
4.2 提炼调用要异步化
会话末的提炼是 LLM 调用,耗时一到数秒,务必放在后台任务里执行,别阻塞用户的下一轮对话。SQLite 场景尤其注意:提炼任务与主进程并发写同一个 .db 文件,可能出现锁等待,改成队列串行写入即可。
4.3 记忆要可撤销
用户说”忘掉我之前说的 XX”,Agent 必须能删除对应 MemoryItem 及其 Relations。提供记忆管理入口(查询、编辑、删除)不只是合规要求,也是用户信任的基础。
八、性能与容量估算:记忆图谱会膨胀到多大
给个量级感。一条 MemoryItem 的元数据(文本 + 类别 + 归属 + 关系)通常在 200~500 字节。假设一个高频 Agent 每天(每用户)提炼 50 条新记忆,一年约 1.8 万条:
- SQLite:1 万用户 × 1.8 万条 ≈ 1.8 亿条,这已超出 SQLite 舒适区。但单用户视角下 1.8 万条仅约 9MB,SQLite 完全没问题。
- PostgreSQL:1.8 亿条 × 400B ≈ 72GB 原始数据,加索引膨胀约 2~3 倍,200GB 级别云盘即可承载;查询侧配合 (resource_id, category) 复合索引,单用户检索始终是毫秒级。
性能上的经验值:SQLite 后端单用户检索 P99 在 10ms 以内;PostgreSQL 后端在索引合理的前提下 P99 同样可压到 20ms 以内——记忆检索不是 Agent 延迟的瓶颈,LLM 主调用才是。真正的性能风险点是提炼环节的 LLM 调用排队,务必异步化并做好限流。
九、常见报错排查清单
9.1 模块与依赖类
ImportError / ModuleNotFoundError:确认 pip 装在了虚拟环境里(pip show memu 核对路径)。psycopg 驱动缺失:切 PostgreSQL 后端前必须 pip install "psycopg[binary]",SQLite 后端则不需要任何驱动。
9.2 数据库类
SQLite “database is locked”:多进程并发写同一 .db 文件导致。解法:提炼写入串行化(队列),或提前切 PostgreSQL。PostgreSQL 连接拒绝:检查 pg_hba.conf 是否放行你的来源 IP、监听地址是否为 * 而非仅 localhost、密码中的特殊字符是否需要 URL 编码。表不存在:新库忘记跑 memory.initialize()。
9.3 行为类
检索结果为空:先检查 resource_id 是否匹配(最常见:写入时用对象 id,查询时用了字符串 “alice”),再确认 category 拼写。记忆重复膨胀:每轮都触发提炼且没做去重决策,改为会话末批量提炼并让 LLM 输出”新增/更新/无变化”。提炼超时:LLM 网络抖动,加超时重试并降级为”跳过本轮提炼”,不要让记忆失败阻塞主对话。
十、与其他记忆方案的对比与选型建议
10.1 横向对比
| 方案思路 | 代表形态 | 结构化程度 | 上手成本 | 适用场景 |
|---|---|---|---|---|
| 纯上下文拼接 | 把历史塞 system prompt | 无 | 极低 | 原型、短会话 |
| 向量库记忆 | embedding + top-k 召回 | 弱(无类型无关系) | 中 | 海量模糊语义检索 |
| 重型记忆平台 | 数万行级 SaaS/框架 | 强 | 高 | 企业级复杂需求 |
| Memu typed graph | 500 行自托管框架 | 强(四类节点 + Relations) | 低 | 快速落地、可控可审计 |
10.2 回答那个终极问题:500 行代码够不够用
这是我在评估 Memu 时反复琢磨的问题。答案是:对记忆层这个职责,500 行不仅够,而且恰到好处。理由有三:第一,记忆层的本质复杂度本来就低——四类节点、读写、归属、关系,用 500 行 typed 代码完全能表达,同类框架的数万行里大量体积来自内置的向量引擎、UI 面板、多协议适配,这些本就不该是记忆层的职责;第二,500 行意味着全部逻辑一个下午就能读完,出了问题你可以自己修,不存在”黑盒依赖焦虑”;第三,它把重型能力留给外部——需要向量语义就把 embedding 检索外挂进来,需要分布式就交给 PostgreSQL,框架本身保持薄。
反过来说,如果你需要开箱即用的自动遗忘策略、记忆衰减曲线、跨 Agent 共享记忆协议,500 行的 Memu 目前需要你自己在上面补。但这恰恰是选型的真谛:要”薄而透明、自己掌控”,选 Memu;要”厚而省事、接受黑盒”,选重型平台。以 Apache-2.0 的协议自由度,二者并不互斥——完全可以先用 Memu 把记忆图谱跑起来,再按业务需要逐步加厚。
十一、FAQ:高频问题详解
FAQ 1:Memu 的记忆检索是纯结构化的吗?需要额外装向量数据库吗?
不需要额外安装向量数据库就能起步。Memu 的记忆图谱以 SQLite / PostgreSQL 为存储后端,核心检索路径围绕 Resources、Categories 与 Relations 展开,即”先定范围、再匹配内容”,在中小规模下无需向量引擎。如果你的记忆量达到数十万条以上、且查询高度口语化模糊,可以在图谱结构之上叠加一层向量索引作为辅助召回——但架构上向量层是可选加速件,不是 Memu 记忆模型的前置依赖。这也是 typed memory graph 与”向量库即记忆”方案的根本区别:结构是骨架,相似度只是补充。
FAQ 2:SQLite 和 PostgreSQL 之间切换,业务代码要改多少?
理论上只改一行 database_url。Memu 对四类节点的 API(resources.create、memories.create、memories.search、relations.create 等)是统一抽象,不感知底层后端。需要注意三点:切换后要在新库上执行 memory.initialize() 建表;存量数据要按第六节的方法迁移并校验行数;PostgreSQL 连接串里的密码特殊字符要做 URL 编码(如 @ 写成 %40)。从 SQLite 起步、PostgreSQL 扩容的路径设计,让”先跑通、后规模化”的成本曲线非常平缓。
FAQ 3:记忆提炼每轮对话都调 LLM,成本会不会失控?
不会,前提是控制触发频率。推荐会话结束或每 N 轮批量提炼一次,而不是逐轮调用。提炼任务用轻量模型(gpt-4o-mini 档位),输入是”一段对话 + 已有记忆摘要”,输出是少量原子事实,单次成本通常在千分之一美元量级。以每天 1000 次会话、每会话 2 次提炼计算,月成本几美元以内,相比主对话的 LLM 开销几乎可以忽略。另一个降本技巧:手工写入(跳过 LLM)用于结构明确的事实,LLM 提炼只留给自由对话。
FAQ 4:多用户场景下,怎么保证用户 A 的记忆不会泄露给用户 B?
Memu 的 Resources 节点天然提供归属边界:所有记忆读写都强制携带 resource_id,查询不跨 Resource 返回。工程上要守住三条纪律:其一,resource_id 来自服务端会话上下文(登录态),绝不从用户输入解析;其二,所有检索调用显式传 resource_id,不依赖默认值;其三,切 PostgreSQL 后可以进一步启用行级安全(RLS)或分 schema,把隔离下沉到数据库层。SQLite 单机阶段靠应用层纪律即可,上 PostgreSQL 后建议把防线加厚。
FAQ 5:记忆条目冲突了怎么办?比如用户先说”用 MySQL”后改口”改用 PostgreSQL”。
这是所有记忆系统的核心难题。Memu 的应对路径是:提炼环节让 LLM 检查已有记忆(把已有摘要一并传入),识别到冲突时执行”更新”而非”新增”——即改写旧 MemoryItem 的文本,或给旧条目打上 superseded 标记、建立指向新条目的 Relation。如果旧条目没有唯一业务键,可以在检索阶段按时间倒序加权,让更新的记忆优先呈现。建议在提炼提示词里显式要求输出”conflict_with”字段指向被替换的旧记忆 id,这样图谱里永远保留演进痕迹,可审计、可回滚。
FAQ 6:Memu 适合哪些规模的团队?生产环境一个人能维护吗?
Memu 的定位决定了它的运维面极小:SQLite 阶段是零运维(一个文件),PostgreSQL 阶段的运维负担等同于维护一个普通 PostgreSQL 实例(可用云 RDS 进一步降低)。一个熟悉 Python 的工程师足以独立维护,这与其他重型记忆框架需要专人跟进升级的情况形成对比。真正的维护重心不在基础设施,而在记忆治理——定期清理失效 MemoryItem、监控记忆增速、审计提炼质量。规模判断上:个人项目和内部工具用 SQLite 起步即可;面向公网、多实例部署的 SaaS Agent,直接上 PostgreSQL 并从第一天就统一 resource_id 规范,后续扩容会顺滑得多。







暂无评论内容