AI 记忆系统
记忆是系统越用越懂你的机制。
MateClaw 里其他所有东西,在你配置完之后就静止了。Agent、工具、知识库——你改它们的时候才改。记忆是唯一一个会自己改变的部分,变化是实际使用过程的副产品。这就是整个设计的核心意图。
它在你睡着的时候做了一个关于你的梦
不是营销词。是 memory/dreaming/ 包里真实跑的代码。
每天凌晨 3 点(默认时间,可改),系统跑一次调度任务,名字就叫 Dreaming:扫一遍今天和你聊天的每个 Agent 的对话痕迹,把零散的线索整合成对你的理解,过滤掉一次性的、矛盾的、过期的,把高频出现的提升进 MEMORY.md,整个"看见了什么、得出了什么、改写了什么"的过程追加进 DREAMS.md——一条人类可读的审计线。
第二天早上你打开它,它从昨天结束的地方继续,不是从零开始。
别的 AI 每天从零开始。MateClaw 从昨天结束的地方继续。
这一页讲组成记忆的四个层、每个 Agent 的记忆文件、以及 Agent 自己怎么在对话中读写这些文件。
四个层
┌────────────────────────────────────────────────────────────┐
│ 1. 当下这一回合 │
│ 正在说的话、刚刚说过的话、按 token 预算自动裁剪 │
│ 更新时机:每一回合 │
└────────────────────────────────────────────────────────────┘
│
▼(对话完成之后)
┌────────────────────────────────────────────────────────────┐
│ 2. 对话结束后的提取 │
│ 从对话里挑出值得记住的事,写进 PROFILE.md / MEMORY.md │
│ 和当天的日常笔记 │
│ 更新时机:每次有意义的对话结束后异步跑 │
└────────────────────────────────────────────────────────────┘
│
▼(默认每天凌晨 3 点,可调)
┌────────────────────────────────────────────────────────────┐
│ 3. 夜里整合(Dreaming) │
│ 扫一遍最近的日常笔记,找出反复出现的模式, │
│ 合并进 MEMORY.md,把过程记到 DREAMS.md │
│ 更新时机:定时触发,可手动 │
└────────────────────────────────────────────────────────────┘
│
▼(下一次对话直接用最新版本)
┌────────────────────────────────────────────────────────────┐
│ 4. 工作空间文件进入 system prompt │
│ 四个 markdown 文件每一回合都被注入 │
│ 更新时机:底下文件一变,下一回合就生效 │
└────────────────────────────────────────────────────────────┘每一层跑在不同的时间尺度上。当下是这一回合。提取是每一次对话之后。整合是每天夜里。文件注入是每一回合都用当前最新版本。加在一起它们形成一个循环——你说的话变成上下文,上下文变成文件,文件变成 system prompt,system prompt 变成 Agent 明天知道的东西。
记忆认人:per-owner 隔离(1.5.0)
以前一个员工的记忆是共享的:不管是网页登录的你、还是飞书群里的同事、还是第三方 API 接进来的终端用户,聊出来的记忆都堆进同一个 MEMORY.md。一个员工服务多个人时,记忆会串台。
1.5.0 给每条记忆加了主人(owner)和可见范围(scope)。
统一的 owner_key
不管身份从哪来,都归一成一个带前缀的字符串:
| 来源 | owner_key |
|---|---|
| 网页控制台 | user:<用户id> |
| IM 渠道(飞书 / 钉钉 / 企微…) | <渠道>:<发送者id> |
| 第三方 API(带 endUserId) | api:<endUserId> |
| 系统 / cron | system |
三档可见性
| scope | 谁能读 | 典型内容 |
|---|---|---|
| PERSONAL(个人) | 只有匹配的 owner | 对话里抽取出来的记忆默认进这档 |
| TEAM(团队) | 用这个员工的人都能读 | 员工配置文件(AGENTS.md / SOUL.md / PROFILE.md)、历史回填的数据 |
| GLOBAL(全局) | 跨员工 / 工作空间始终可见 | 预置事实、系统参考资料 |
召回偏好个人记忆
system prompt 里只烤进 TEAM/GLOBAL 的共享记忆(可缓存);每轮再按当前 owner_key 预取他个人的记忆注入。所以问"我的项目用什么栈"时,员工优先回忆这个人的私人记忆文件,而不是知识库里的泛泛资料。
关于结构化"事实"层:事实召回查询本身支持 owner 可见性过滤(PERSONAL 仅 owner 可见,TEAM/GLOBAL 共享)。但当前的自动事实投影主要从共享记忆文件构建、插入时不写
ownerKey/scope——也就是说个人化更多体现在个人记忆文件的预取上,事实层的 per-owner 化还在补齐中。
第三方 API 透传终端用户身份
/api/v1/chat 和 /api/v1/chat/stream 的请求体新增可选字段 endUserId(字符串,保大整数精度)。一个 PAT 认证的接入方代表一个 MateClaw 用户,但可以为每个终端用户传不同的 endUserId,记忆按终端用户自动隔离。
这是一个可开关的特性
总开关是 mate.memory.lifecycle-mediator-enabled。
默认值要看清楚
Java 属性的裸默认值是 false,但随发行版打包的 application.yml 把它设成了 true——也就是说默认安装下 per-owner 隔离是开着的。要回到旧的共享行为(所有写入走 TEAM),在你的配置里显式设为 false。
打开后:对话抽取写入 owner 的 PERSONAL 记忆,召回按 owner_key 过滤;关闭后所有写入回退到共享 TEAM。多租户实例保持开启,单人部署可以关掉。
底层:迁移 V137 给 mate_workspace_file / mate_memory_recall / mate_fact 三张表加了 owner_key + scope 列,历史行回填为 TEAM(保证升级后没有记忆被藏起来)。remember 等记忆工具会按当前请求上下文解析 owner_key,开关打开时写进该 owner 的 PERSONAL 记忆,关闭时回退共享写入。
多层记忆 + 可插拔 Provider
记忆这一层不是一个硬编码的实现。它是一个接口——多层架构允许你堆叠 provider:
- 默认 Provider 就是这页后面讲的基于工作空间文件的记忆。MateClaw 出厂就带这个,对大多数人来说这一个就够了。
- 自定义 Provider 可以插入用于专用检索——基于向量的长期记忆、图结构记忆、外部记忆服务。
- 分层意味着同一个 Agent 可以同时和多个 provider 对话。短期 provider 返回最近上下文;语义 provider 返回相关记忆;Wiki provider 返回权威引用。它们在读取时组合。
对大多数 Agent 来说,默认的就够了,这一节可以跳过。如果你在做某种专用的东西——需要记住上千条事实并用向量搜索、需要图结构记忆——这里就是插入点。开发细节看 架构说明。
每个 Agent 都有的四个文件
每个 Agent 都有自己的工作空间。四个 markdown 文件是长期记忆的骨架:
workspace/{agentId}/
├── AGENTS.md # Agent 怎么用记忆 —— 行为指南
├── SOUL.md # Agent 是谁 —— 核心身份、人格、边界
├── PROFILE.md # 你是谁 —— 用户画像、偏好、背景
├── MEMORY.md # 什么重要 —— 关键决策、项目上下文、待办
└── memory/
├── 2026-04-09.md # 日常笔记 —— 今天发生了什么,追加模式
├── 2026-04-10.md
└── 2026-04-11.md前四个会在每一回合注入到 system prompt(只要 enabled=true)。每日笔记不会——它们喂给整合服务用。
每个文件是干什么的
- AGENTS.md——Agent 自己的使用说明书。什么时候该写记忆、每个地方放什么、有哪些工具可以操作记忆。种子:
enabled=true,sort_order=0。 - SOUL.md——Agent 从根上是谁。自我意识、演化指引、隐私与边界原则。想在深层修改 Agent 的性格时编辑它。种子:
enabled=true,sort_order=1。 - PROFILE.md——Agent 学到的关于你的东西。名字、职业、技术栈、沟通偏好。对话里出现值得保留的东西时记忆提取器会更新它。全覆盖写入。种子:
enabled=true,sort_order=2。 - MEMORY.md——Agent 认为重要到值得留下的东西。活跃项目、未决定的事、打开的线索、你让它记住的东西。提取器和整合器都会更新它。种子:
enabled=true,sort_order=3。
1.3.0 新增:工作流可以写记忆
v1.3.0 起,工作流 的 write_memory step 可以在流程跑完时把结果直接写进某位员工的 MEMORY.md(或任意启用的 memory 文件),支持 4 种合并策略:append / replace_section / upsert_kv / overwrite。这意味着记忆不再只能由对话提取或 Dreaming 写入——一条业务流程的产物也可以被沉淀。
每日笔记
对话亮点按日期归档,追加模式——同一天里的多次对话全部累加到同一个文件。这些不会注入到 system prompt(enabled=false)。它们存在是为了让整合器凌晨两点跑的时候有东西可扫。
短期:上下文窗口
每一次 LLM 调用之前,MateClaw 都会构造真正送出去的那个 prompt:
[System Prompt] ← 永远在最前
[工作空间文件注入] ← AGENTS / SOUL / PROFILE / MEMORY
[对话上下文摘要] ← 只有在早期轮次被压缩过时才有
[Message 1: user]
[Message 2: assistant]
...
[当前用户消息] ← 永远在最后工作空间文件按 sort_order 排序拼进 system prompt,格式:
--- AGENTS.md ---
(内容)
--- SOUL.md ---
(内容)
--- PROFILE.md ---
(内容)
--- MEMORY.md ---
(内容)只注入 enabled=true 的文件。
上下文爆了怎么办
三层防御:
第一层:主动压缩。 估算总 token 超过预算的 75%(默认窗口 12.8 万 token),系统让 LLM 总结早期轮次。尾部基于 token 预算动态保留最近若干条(下限由 preserve-recent-pairs 和 protect-last-min-messages 两个参数取最大值决定,默认至少保留 10 条)。结果缓存 30 分钟。
第二层:紧急恢复。 如果 LLM 仍然返回上下文超限,系统不再调 LLM,直接丢掉更早的消息、保留最后 2 轮、重试一次。
第三层:硬截断。 总结之后还是超,从前往后继续丢消息直到 prompt 装得下。最近 2 条永远不动。
安全设计——摘要以用户消息形式注入,不是系统消息。刻意的:防止早期用户输入的压缩版本被提升成系统级指令,关掉一条注入攻击路径。
配置
mate:
agent:
conversation:
window:
default-max-input-tokens: 128000
compact-trigger-ratio: 0.75
preserve-recent-pairs: 2
summary-max-tokens: 300对话后提取
一次对话结束之后,系统会异步地把值得记住的东西提取出来、写进 PROFILE.md、MEMORY.md、当天的日常笔记。发生在用户响应路径之外——永远不会阻塞下一个回合。
什么时候触发
一个回合完成之后,系统在后台线程处理这次对话。需要满足几个条件才会真的跑提取:
- 自动总结开关打开
- 不是定时任务自己触发的对话(防止递归)
- 消息数达到下限(默认 4 条)
- 最后一条用户消息够长(默认至少 10 字符)
全部通过,开始提取。
并发控制
- 冷却——同一个 Agent 在默认 5 分钟内不会重复提取
- 按 Agent 加锁——同一个 Agent 已经有一个提取任务在跑,新任务直接跳过
LLM 实际在做什么
- 从对话历史里加载消息
- 读当前的 PROFILE.md、MEMORY.md、今天的日常笔记
- 构造 transcript:最多 30 条消息,每条截断到 2000 字符
- 用记忆总结的 prompt 模板调 LLM
- 解析 JSON 响应
- 执行写入
LLM 响应 schema
| 字段 | 类型 | 作用 |
|---|---|---|
should_update | boolean | 记忆是否需要更新 |
reason | string | 原因(用于审计) |
daily_entry | string | 追加到今天日常笔记的内容 |
memory_update | string | MEMORY.md 的全新全量内容 |
profile_update | string | PROFILE.md 的全新全量内容 |
文件写入规则
- PROFILE.md——全覆盖,只在
profile_update非空时写 - MEMORY.md——全覆盖,只在
memory_update非空时写 - memory/YYYY-MM-DD.md——追加,文件不存在时用日期标题新建
整合与 Dreaming
第三层按计划跑。它的工作是看着日常笔记堆起来,周期性地问自己:这里的模式是什么?哪些东西应该被提升进核心记忆?哪些东西过期了应该被遗忘?
它做什么
- 列出 Agent 所有
memory/*.md文件,取最近 7 天 - 读这些日常笔记 + 当前 MEMORY.md
- 用整合 prompt 模板调 LLM
- LLM 返回
{should_update, reason, memory_content} - 如果
should_update为 true,MEMORY.md 被memory_content全覆盖
触发方式
- 自动——每个 Agent 在系统定时任务里有一行,每天凌晨 3 点跑一次
- 手动——
POST /api/v1/memory/{agentId}/emergence
为什么不会递归
整合跑起来的时候会通过 Agent 触发一次"对话"。没有保护的话,那次对话会再触发对话后记忆提取监听器,循环下去。
事件上带触发源标记,提取监听器看到是定时任务触发的就直接跳过。
DREAMS.md —— 整合日记
每次整合跑完会往 workspace/{agentId}/DREAMS.md 追加一条短记录:
- 它看了什么
- 它找到了什么模式
- 因此 MEMORY.md 变了什么
- 日期
这给你一条人类可读的审计线——你可以打开 DREAMS.md,看记忆是怎么一步步走到当前状态的。这个文件也有自我增长的上限,超过阈值会对旧记录做总结。
打分式 Emergence + 召回追踪
整合不是盲目地总结。它会追踪:
- 哪些记忆条目在最近的对话里真的被主动召回——Agent 的读取模式反过来影响整合对"什么重要"的判断
- 打分式 emergence——候选模式按频率 + 近期性 + 显式召回打分,只有高分的才能进 MEMORY.md
- 多闸门过滤——低信号的提取(一次性提及、矛盾、用户后来主动纠正过的)会在变成记忆之前被过滤掉
- Dreaming 状态 API——
GET /api/v1/memory/{agentId}/dreaming/status
完整生命周期(开关控制)
记忆从"夜里梦一次"升级成完整的逐轮生命周期。这套行为落在开关后面——开源版默认关,生产构建打开。
它做的事:
- 每一轮都被记账 —— 每一回合开始和结束时系统都在记笔记,不只是夜里整合的时候
- 事实投影 —— 对话被拆成结构化的"事实"行,Agent 可以查询。带信任度评分 + 衰减。
- 结构化的夜间报告 —— 整合产出一份完整报告,可以按主题手动重做
- 晨报卡片 —— 第二天第一次对话浮出昨天的报告;逐条 Confirm / Edit / Forget
- 矛盾收件箱 —— 新事实和老事实冲突时给一个决策队列,而不是悄悄覆盖
- 显式遗忘 —— 你说"忘掉",它就真的忘掉,从所有地方
- 反馈打分 —— 检索到的事实点👍/👎,反馈进入信任度评分
- SOUL 自动演化 —— Agent 的人格档会从累积的事实里自我重写
- 月度归档 —— 老报告滚进压缩的月度归档,时间线里能查
- 记忆浏览器 —— 时间线、事实、矛盾、变更对比、信任度面板
application.yml 启用(这些开关都在 mate.memory 下,分三个 Phase):
mate:
memory:
# Phase 1:逐轮生命周期总线
lifecycle-mediator-enabled: true
dream:
focused-enabled: true # 聚焦 dream 端点
archive-enabled: true # 月度归档轮转
archive-keep-days: 30
max-candidates-per-dream: 100
# Phase 2:SOUL 自动演化
soul-update-interval: 20 # 每 20 次写入触发一次 SOUL.md 重写(0 = 关)
# Phase 3:事实投影
fact:
projection-enabled: true
projection-rebuild-cron: "0 */30 * * * ?"
contradiction-check-enabled: false # 矛盾检测(实验,默认关)
trust-half-life-days: 60
forget-enabled: true # UI 上的「遗忘」按钮晨报卡片是一个端点(
GET /api/v1/memory/{agentId}/dream/morning-card),不是单独的开关——只要事实投影 + dream 这套生命周期开着就有数据。
always-on 记忆的尺寸控制
新增
每一回合都注入 system prompt 的那些记忆(user / feedback 结构化条目、PROFILE.md、MEMORY.md)有个隐患——只增不减。条目越攒越多,每轮 token 一路膨胀。这一组机制给"常驻记忆"装上确定性的体积上限。
三个层次各管一段:
注入预算(注入时截断,不动磁盘)
把 user / feedback 两类结构化条目注入 system prompt 时,按条目的 Updated: 日期排序(LRU),只保留最新的若干条,超出部分在注入时丢弃——磁盘文件不动,并在块尾披露省略了多少条。
mate.memory.system-block-max-chars(默认4000):常驻结构化块的总字符上限,超了就按时间从最老的开始丢;0= 不限mate.memory.system-block-max-entries-per-type(默认40):每类(user / feedback)最多注入多少条;0= 不限
夜间巩固(在存储层缩文件)
注入预算只在注入时截断,磁盘文件本身还在长。巩固是在存储层做合并:每晚定时(默认 03:30,独立于 Dreaming 的开关和时间表)遍历每个员工的共享桶 + 各 per-owner 桶,条目数超过阈值时调 LLM 把近重复 / 过时的条目合并写回。
有一条安全不变量:巩固后的条目数只能减不能增——模型若幻觉出更多条目,这次写入直接跳过。
mate.memory.structured-consolidation-enabled(默认true):关掉就只剩注入截断、没有存储侧合并mate.memory.structured-consolidation-min-entries(默认8):桶里条目少于此值跳过 LLM 调用省钱mate.memory.structured-consolidation-cron(默认"0 30 3 * * ?"):独立调度,不碰 dreamingmate.memory.structured-consolidation-max-owners-per-run(默认50):每个员工每次最多处理多少个 owner 桶,剩下的下次再来;0= 不限
手动触发:POST /api/v1/memory/{agentId}/structured-consolidation,返回 ownersConsolidated / updated / entriesBefore / entriesAfter 等统计。
别和 Dreaming 搞混:Dreaming 把日常笔记整合进
MEMORY.md(写"重要的东西");巩固只负责把user/feedback结构化条目去重瘦身。两件事,两个调度。
文件上限(重写时的确定性兜底)
PROFILE.md 和 MEMORY.md 由 LLM 全量重写。prompt 里要求它简洁,但没有硬约束,仍可能越写越大。文件上限是写回时的确定性兜底:内容超预算就在最后一个能放下的 ## 二级标题边界截断(保留文件头部的核心段),并追加一行截断标记。
mate.memory.profile-max-chars(默认4000):PROFILE.md 硬上限;0= 不限mate.memory.memory-md-max-chars(默认8000):MEMORY.md 硬上限;0= 不限
Agent 自己读写自己的记忆
记忆不是单向地"发生在 Agent 身上"的事。Agent 自己在对话过程中可以主动读写自己的文件——通过一组工作空间记忆工具:
| 方法 | 作用 |
|---|---|
list_workspace_memory_files | 列出 Agent 的文件,可按文件名前缀过滤,按 sort_order 排序 |
read_workspace_memory_file | 读某个文件的内容 |
write_workspace_memory_file | 创建或覆盖一个文件(全覆盖) |
edit_workspace_memory_file | 按精确查找替换编辑(增量更新,支持 replaceAll) |
关键词搜索自己的记忆
1.4.0 新增
员工不止能读整个文件——它在对话中可以按关键词搜索自己工作空间里的全部记忆文件,直接定位到某一行。
这是一个 Agent 运行时能力:员工给一个关键词,系统在它自己的工作空间记忆文件里做检索:
- 分词——中文按 2 字滑动窗口切,拉丁文按空格切,两种语言都能命中
- 按文件加权打分——
AGENTS.md/MEMORY.md/PROFILE.md这类核心文件的命中权重高于每日笔记 - 返回结果——每条命中给出:文件名 + 行号 + 80 字上下文片段(命中词高亮) + 相关性分数
- 扫描范围——最多扫约 50 个候选文件,按分数从高到低排序
适用场景:员工想确认"我之前是不是记过这件事"、跨多天笔记找回某个具体决定,而不需要把整份文件读进上下文。
示例
列表:
// 输入
{"agentId": 1, "filenamePrefix": "memory/"}
// 输出
{"agentId": 1, "count": 3, "files": [
{"filename": "memory/2026-04-09.md", "enabled": false, "fileSize": 512},
...
]}读取:
// 输入
{"agentId": 1, "filename": "MEMORY.md"}
// 输出
{"agentId": 1, "filename": "MEMORY.md", "enabled": true, "content": "..."}编辑:
// 输入
{"agentId": 1, "filename": "MEMORY.md", "oldText": "旧内容", "newText": "新内容"}
// 输出
{"agentId": 1, "filename": "MEMORY.md", "replacements": 1}安全约束
- 只允许
.md文件 - 不允许绝对路径,不允许
..目录穿越 write是全覆盖——在乎已有内容就先read- 新建的文件默认
enabled=false
记忆快照导出 / 导入
1.4.0 新增
一个员工积累的整份记忆可以打包成一个 ZIP 带走——备份、迁移到另一套部署、或者克隆一个"已经认识你"的同事。
快照把一个员工的核心记忆打包成单个 ZIP:
AGENTS.md/MEMORY.md/PROFILE.md/SOUL.md/KNOWLEDGE.md- 每日笔记(
memory/YYYY-MM-DD.md) - 一份
manifest.json(记录包里有什么、来自哪个员工)
三个端点
| 方法 | 路径 | 权限 | 作用 |
|---|---|---|---|
| GET | /api/v1/agents/{agentId}/workspace/memory/export | Viewer | 导出 ZIP——只读权限也能做备份 |
| POST | .../workspace/memory/import/preview | Member | 干跑:解析 ZIP,逐文件给出 create / update / skip 分类,不写任何东西 |
| POST | .../workspace/memory/import | Member | 应用导入,原子写入 |
先 preview 看清差异,确认后再 import——导入前你永远知道会改动什么。
安全护栏
- 白名单——只接受上面列出的那几类文件,其余忽略
- 防 zip 炸弹——条目数 ≤ 500、单条解压 ≤ 1 MB、总计 ≤ 16 MB,超了直接拒绝
- 不序列化 UI 开关状态——
enabled/sortOrder不进快照;导入到新员工时由目标端按种子规则决定,不会把源端的开关状态强加过来
UI
- Agent Context 页面右侧面板有 Export / Import 两个按钮
- 导入时先弹出差异对比(哪些新建、哪些覆盖、哪些跳过),确认后才真正写入
配置参考
记忆提取 & 整合
mate:
memory:
# --- 自动提取 ---
auto-summarize-enabled: true
min-messages-for-summarize: 4
min-user-message-length: 10
skip-cron-conversations: true
summary-max-tokens: 1000
max-transcript-messages: 30
# --- 并发 ---
cooldown-minutes: 5
# --- 整合 / dreaming ---
emergence-enabled: true
emergence-day-range: 7
# --- per-owner 记忆隔离(1.5.0)---
# 随发行版打包的默认值是 true(开):对话抽取写入 owner 的 PERSONAL 记忆,召回按 owner_key 过滤。
# 设为 false 回到旧的共享行为(所有写入走 TEAM)。Java 属性裸默认值为 false。
lifecycle-mediator-enabled: true
# --- always-on 记忆尺寸控制 ---
# 注入预算:常驻 user/feedback 结构化块(注入时 LRU 截断,0 = 不限)
system-block-max-chars: 4000
system-block-max-entries-per-type: 40
# 夜间巩固:在存储层合并去重 user/feedback 条目(独立于 dreaming)
structured-consolidation-enabled: true
structured-consolidation-min-entries: 8
structured-consolidation-cron: "0 30 3 * * ?"
structured-consolidation-max-owners-per-run: 50
# 文件上限:PROFILE.md / MEMORY.md 重写时的硬截断(节边界,0 = 不限)
profile-max-chars: 4000
memory-md-max-chars: 8000配置前缀:mate.memory。
上下文窗口
mate:
agent:
conversation:
window:
default-max-input-tokens: 128000
compact-trigger-ratio: 0.75
preserve-recent-pairs: 2
summary-max-tokens: 300API 接口
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /api/v1/memory/{agentId}/emergence | 手动触发整合 |
| POST | /api/v1/memory/{agentId}/summarize/{conversationId} | 对某次对话手动触发提取 |
| POST | /api/v1/memory/{agentId}/structured-consolidation | 手动触发 user/feedback 结构化条目巩固 |
| GET | /api/v1/memory/{agentId}/dreaming/status | 查询上次运行、下次计划、最新 DREAMS.md 条目 |
Mem0 集成(可选)
非默认栈
Mem0 集成是可选的社区贡献项,不在 MateClaw 的默认安装里。它需要你自己部署一份 Mem0 服务(FastAPI + pgvector + 可选 Neo4j)。MateClaw 的"本地优先、零外部依赖"定位不变——这个插件只是给愿意多跑一套服务的人一个叠加的语义召回通道。
Mem0 是一个独立的记忆服务,做的是 LLM 记忆的提取、去重、向量化召回。MateClaw 的 mateclaw-plugin-mem0 模块把它作为一个插件式 memory provider 接进来——内部 4 个 provider(Builtin / Structured / Session / Fact)一个都不动,Mem0 作为第 5 个外部 provider 叠加上去,互不替代。
它做什么
| 钩子 | 行为 |
|---|---|
systemPromptBlock | 返回空——常驻 system prompt 不动,避免每轮 token 膨胀 |
prefetch(agentId, query, ownerKey) | 当 searchEnabled=true 且 ownerKey 非空时,调 POST {baseUrl}/memories/search/,返回一个 [Mem0 Recall] 块拼进本轮上下文 |
syncTurn(agentId, conversationId, userMessage, assistantReply, ownerKey) | 当 syncEnabled=true 且 ownerKey 非空时,异步把这一轮的 user/assistant 消息以 user_id = ownerKey 推到 POST {baseUrl}/memories/ —— 与召回查询用同一个标识。失败只记日志、不阻塞响应 |
getToolBeans | 空列表——v1 不暴露 Agent 可调用的工具 |
故障隔离:recall 或 sync 任何一边抛异常,插件自己吞掉、写日志,平台继续走其他 provider。Mem0 挂了不会影响 MateClaw 的本地记忆。
per-owner 隔离的映射
Mem0 用 user_id + agent_id 做隔离。MateClaw 的映射:
| MateClaw 字段 | Mem0 字段 | 说明 |
|---|---|---|
ownerKey(如 user:42 / feishu:sender_abc) | user_id | 透传,原样作为 user_id |
agentId | agent_id | 数字员工 ID |
prefetch 和 syncTurn 都能从平台拿到 ownerKey,写入和召回用同一个 user_id。拿不到 ownerKey 的变体会直接跳过(召回返回空 / 放弃写入)——Mem0 要求 user_id,没它无法隔离。
安装步骤
- 部署 Mem0 服务:参考 Mem0 官方文档,自托管一份(FastAPI + pgvector + 可选 Neo4j)。记下它的 base URL,比如
http://localhost:8080。 - 构建插件 JAR:在 MateClaw 仓库根目录跑
mvn -pl mateclaw-plugin-mem0 -am package,得到mateclaw-plugin-mem0/target/mateclaw-plugin-mem0-*.jar。 - 放 JAR:把 JAR 丢进 MateClaw 的
plugins/目录。 - 配置:在插件管理 UI 里填
baseUrl(必填),按需填apiKey、调其他参数。重启或重载插件。
配置项
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
baseUrl | string | 是 | — | Mem0 REST API 地址,如 http://localhost:8080 |
apiKey | string | 否 | — | Bearer token,作为 Authorization 头发给 Mem0 |
searchEnabled | boolean | 否 | true | 是否在 prefetch 时调 /memories/search/ 做语义召回 |
syncEnabled | boolean | 否 | true | 是否在 syncTurn 时把每轮对话推到 /memories/ |
maxResults | integer | 否 | 5 | 每次召回返回的记忆条数上限 |
timeoutMs | integer | 否 | 3000 | HTTP 超时(毫秒),recall 和 sync 共用 |
配置只在插件加载时读一次——改了要重载插件才会生效。
已知限制(v1)
- 没有解析出 owner 的轮次不会同步:
syncTurn要求ownerKey;平台解析不出 owner 的轮次(如系统触发的运行)会直接跳过,而不是用一个召回永远查不到的降级标识写入。 - 没有 token 预算控制:prefetch 返回的
[Mem0 Recall]块直接拼进上下文,不受system-block-max-chars那套注入预算约束(那套只管user/feedback结构化条目)。maxResults是唯一的尺寸闸门。 - 没有 Agent 工具:v1 不暴露
mem0_search/mem0_add之类的工具给 Agent 主动调用。Agent 只能被动接收 prefetch 的结果。
