mem0 Export Skill 解析:把 AI Agent 项目记忆导出为可移植 Markdown 的完整实现(mem0-plugin)
本篇围绕 mem0 插件的 export Skill(/mem0:export 命令背后)展开,完整还原其"解析身份 → 分页拉取全量记忆 → 格式化为 YAML frontmatter 块 → 写入导出文件 → 打印汇总"的五步流程,并结合插件配套脚本 parse_export_file.py、Python SDK 的 get_all() 分页实现与测试用例,解释导出文件为何必须严格遵守该格式,以及它如何与 import Skill 构成备份—迁移闭环。读完后你可以掌握:如何在 Agent 会话中正确执行项目记忆导出、逐字段理解导出文件格式契约,以及如何用仓库自带的解析器验证导出结果的可导入性。
一、Mem0 Export 是什么:定位与适用场景
export 是 mem0-plugin 内置的 17 个 /mem0: 技能之一,其技能定义位于 export/SKILL.md,技能头部的 frontmatter 声明了它的用途:
Exports all project memories to a portable Markdown file for backup or migration. Use when backing up memories, migrating to another project, sharing memory state with teammates, or archiving before cleanup.
即:把当前项目在 Mem0 平台上的全部记忆导出为一个可移植的 Markdown 文件。插件 README 的技能表中将其描述为 "Export memories to portable Markdown",对应的命令为 /mem0:export。
适用场景(原文档明确列出):
- 备份(backing up memories):清理或删除记忆前先留档;
- 跨项目迁移(migrating to another project):与
/mem0:import配合,把 A 项目的记忆状态搬到 B 项目; - 团队共享(sharing memory state with teammates):Markdown 文件可直接进版本库或随 PR 流转;
- 归档(archiving before cleanup):批量删除前的快照。
需要说明的前提:该 Skill 运行在已安装 mem0 插件的 AI 编程环境(Claude Code / Cursor / Codex 等)中,通过插件注册的远程 MCP Server 提供 get_memories、add_memory 等工具(见 mcp_config.json,认证头为 Token ${MEM0_API_KEY})。也就是说,导出动作由 Agent 调用 get_memories 工具完成,而不是本地直连存储——这也是"可移植"的关键:文件本身不依赖任何本地状态。
二、执行流程全解:五步导出
以下流程完整继承自 export/SKILL.md 的 "Execution" 章节,并逐步补充仓库内的实现佐证。
Step 1:解析身份(Resolve identity)
导出前先确定"导出谁在哪个项目的记忆",涉及两个标识符:
user_id:取MEM0_USER_ID环境变量;未设置则取$USER;再没有则用"default"。project_id(在 API 中作为app_id使用):取MEM0_PROJECT_ID环境变量,否则走项目解析器(project resolver)。
这条解析规则并非 Skill 的孤立约定,而是整个插件的统一身份体系。在 _identity.py 中可以确认 resolve_user_id() 的实现与文档完全一致:
def resolve_user_id() -> str:
explicit = os.environ.get("MEM0_USER_ID", "").strip()
if explicit:
return explicit
return os.environ.get("USER") or "default"
而 project resolver 的完整优先级定义在 _project.py 的 resolve_project_id() 中,按顺序为:
MEM0_PROJECT_ID环境变量(显式覆盖);~/.mem0/project_map.json按 cwd 查表;命中失败时还会按 git remote URL 的 SHA-256 哈希做"自愈式"兜底(文件夹改名/移动后仍能找回旧映射);- git remote slug:剥离协议前缀与
.git后缀,取owner-repo(如git@github.com:mem0ai/mem0.git→mem0ai-mem0),兼容 HTTPS、SSH、ssh://、git://及自定义 host 别名等多种 URL 形态; - 兜底:当前工作目录的 basename。
从源码结构看,同一文件里的 resolve_branch() 会执行 git branch --show-current,失败返回 "unknown"——这正好对应导出 frontmatter 中的 branch 字段的典型来源。理解了这条解析链,就能解释为什么 MEM0_PROJECT_ID 是导出前最值得检查的变量:它决定了导出文件名中的 <project_id> 以及拉取记忆时 app_id 过滤条件的取值。
Step 2:分页拉取全部记忆(Fetch all memories)
Skill 指定调用 get_memories 工具,参数为:
filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}page_size=200
分页终止条件(原文档明确要求):若响应是分页的——即结果中包含 next cursor,或本条数等于 page_size——就继续翻页,直到取完所有记忆。
这一点在 Python SDK 的实现中得到印证。插件走的是远程 MCP 的 get_memories 工具,其底层语义与 SDK 的 get_all() 一致,见 main.py:
def get_all(self, options: Optional[GetAllMemoryOptions] = None, **kwargs) -> Dict[str, Any]:
"""Retrieve all memories, with optional filtering.
...
Returns:
A paginated dict: {"count": int, "next": str | None, "previous": str | None, "results": [...]}
"""
# Reject top-level entity params - must use filters instead
invalid_keys = ENTITY_PARAMS & set(kwargs.keys())
if invalid_keys:
raise ValueError(
f"Top-level entity parameters {invalid_keys} are not supported in get_all(). "
f"Use filters={{'user_id': '...'}} instead."
)
两个关键事实可以直接指导导出实现:
- 返回体是标准分页字典:
{"count", "next", "previous", "results"}。因此 Skill 中的"结果包含nextcursor"判断与 SDK 契约吻合;请求会POST到/v3/memories/,并把page、page_size作为 query 参数附带。 - 身份参数必须放在
filters里:SDK 显式拒绝顶层的user_id/app_id等实体参数(抛出ValueError并提示改用filters={'user_id': '...'})。这正是 export Skill 把身份写进filters的AND数组而非顶层的原因。
分页参数本身由 types.py 的 GetAllMemoryOptions 定义:filters、page、page_size,另支持 start_date/end_date(ISO 8601 时间窗)、categories、show_expired、latest_only。export 场景只用到 filters + page_size,但这也提示:如果只需要导出某批分类的记忆,可以在此基础上追加 categories 过滤(属于原文档流程之外的可选扩展,官方 Skill 默认拉全量)。
Step 3:把每条记忆格式化为 YAML frontmatter 块
这是导出格式的核心契约。原文档规定每条记忆记录产生如下精确格式:
---
id: <memory.id>
created_at: <memory.created_at>
type: <memory.metadata.type or "">
confidence: <memory.metadata.confidence or "">
branch: <memory.metadata.branch or "">
files: <memory.metadata.files joined with ", " or "">
categories: <memory.categories joined with ", " or "">
---
<memory.memory or memory content string>
各字段含义与来源:
| 字段 | 取值来源 | 缺失时写法 |
|---|---|---|
id |
memory.id,平台侧记忆主键 |
— |
created_at |
memory.created_at(时间戳,如 2024-01-15T10:00:00Z) |
空串 |
type |
memory.metadata.type(记忆类型标签) |
空串 |
confidence |
memory.metadata.confidence(置信度) |
空串 |
branch |
memory.metadata.branch(捕获时的 git 分支) |
空串 |
files |
memory.metadata.files 列表,用 ", " 拼成单行 |
空串 |
categories |
memory.categories 列表,用 ", " 拼成单行 |
空串 |
原文档的格式注意事项(必须严格遵守):
---分隔符必须独占一行且行尾无多余空白;files与categories以逗号分隔的形式写在同一行;- 每条记忆正文之后、下一条
---之前留一个空行(为了可读性); - 字段缺失或为 null 时写空字符串,绝不能写
"null"。
Step 4:写入导出文件
输出文件名规则(原文档原文):
mem0-export-<project_id>-<YYYY-MM-DD>.md
其中 <YYYY-MM-DD> 为当天 UTC 日期。文件通过 Write 工具(或等效手段)写入当前工作目录。
命名中包含 project_id 与日期有两个实际意义:其一,import 侧是靠文件名里的 mem0-export 子串来发现候选文件的(见后文),project_id 让多项目导出互不混淆;其二,UTC 日期保证同一天的重复导出文件名稳定,便于覆盖式刷新。
Step 5:打印汇总
Exported <N> memories to <filename>
<N> 为实际写入的记忆块总数。
错误处理(原文档 Error Handling 全量继承)
- 若
get_memories返回错误或零条记忆,输出:No memories found for project <project_id>. Nothing exported. - 若写文件失败,向用户报告错误。
值得注意的设计是:空结果被视为"正常分支"而非报错——导出零记忆不会中断流程,只是明确告知未导出任何内容,这对自动化脚本友好。
三、格式契约的底层原理:为什么必须"精确"
export 文件不是给人看的普通笔记,而是 import 侧解析器的结构化输入。仓库里的 parse_export_file.py 就是这条契约的反向实现,读它可以从"消费者"视角理解每个格式细节为什么不能含糊:
- 分块边界:解析器用正则
(?m)^---\s*$按整行---切分全文,奇偶下标配对"frontmatter + 正文"。这解释了 export 侧"---必须独占一行"的原因——一旦分隔符混入正文或行尾多出不规则字符,切分结构即被破坏。 - frontmatter 取值规则:
_parse_frontmatter()用^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$匹配key: value,只按第一个冒号切分,值里可以合法包含冒号(因此created_at: 2024-01-01T10:00:00Z这类时间戳能完整保留)。 - 列表字段:
files、categories由_parse_list_field()按逗号拆分并去除空白,空值归一为[]——对应 export 侧"逗号分隔单行"的写法。 - 空内容跳过:正文为空或纯空白的块直接丢弃;CLI 入口保证任何异常(缺参、文件不存在)都输出
[]且退出码为 0,让上层 Skill 可以安全地"解析失败即[]"地分支处理。
测试用例 test_parse_export_file.py 中的 test_parse_blocks_round_trip 专门验证了按 export Skill 规则生成的块能被解析器无损还原——即"导出格式 → 解析"是一个被单测守护的往返契约(round trip):
block = (
"---\n"
f"id: {memory_id}\n"
f"created_at: {created_at}\n"
...
f"files: {', '.join(files)}\n"
f"categories: {', '.join(categories)}\n"
"---\n"
f"{memory_content}\n"
"\n"
)
records = parse_blocks(block)
assert r["files"] == files
assert r["content"] == memory_content
此外还有针对多行正文、缺失可选字段、含冒号值、空输入等边缘情况的测试。实践含义是:手工编辑导出文件时,宁可少写字段也不能改坏 --- 结构与逗号分隔写法,否则对应记录会在导入阶段被静默跳过。
四、导出之后:与 import Skill 的备份—迁移闭环
导出的真正价值在 import/SKILL.md 中兑现,两者共用同一套文件格式。/mem0:import <filename> 的流程是:
- 按文件名包含
mem0-export的.md文件定位导出文件(多个时询问用户); - 运行
python3 "<PLUGIN_ROOT>/scripts/parse_export_file.py" <file>得到 JSON 记录数组,每条含id、type、confidence、branch、files、categories、content; - 对每条记录调用
add_memory:text=<content>、user_id/app_id取当前活跃身份、metadata回填type/confidence/branch/files(非空才传),并附加"source": "import",infer=False(原文档明确要求不传原始id——平台会分配新 ID); - 单条失败不中断,最后输出
Imported <N>/<total> memories into project <project_id>。
由此形成完整的运维闭环:/mem0:export 留档 → 文件随版本库/IM 流转 → 目标项目 /mem0:import 恢复。由于导入是幂等可重跑的(去重由平台处理),导出文件可以放心地多次使用。
五、实操清单与验证方法
在 Agent 会话中执行导出的最小前提与步骤:
-
确认身份:
MEM0_API_KEY已配置(插件 MCP 认证依赖它);MEM0_USER_ID、MEM0_PROJECT_ID按需显式设置,否则按前文的解析链回退到$USER/default与 git remote slug。 -
发起导出:在支持该插件的客户端中运行
/mem0:export(或在对话中要求"导出本项目的全部记忆",Agent 将按 export/SKILL.md 的五步执行)。 -
核对产物:当前工作目录应出现
mem0-export-<project_id>-<YYYY-MM-DD>.md,终端输出Exported <N> memories to <filename>。 -
验证可导入性(可选):用仓库自带解析器做一次"试解析",确认块数量与字段:
python3 integrations/mem0-plugin/scripts/parse_export_file.py ./mem0-export-<project_id>-<YYYY-MM-DD>.md输出为 JSON 数组;输出
[]说明文件格式不符合契约(多半是---分隔行被改动),应重新导出而非手工修补。
六、小结
mem0-plugin 的 export Skill 用一份 5 步指令定义了 Agent 记忆的"可移植载体":身份解析决定导出范围(user_id + app_id 的 AND 过滤),page_size=200 与 next cursor 保证全量拉取,YAML frontmatter 块格式则是一条被 parse_export_file.py 与 test_parse_export_file.py 双向守护的结构化契约。理解这条"导出格式 → 解析 → 重新入库"的链路后,你就能把 Mem0 的项目记忆当作普通代码资产一样备份、评审与迁移——这正是该 Skill 在 插件 README 中列出的 backup / migration / sharing / archiving 四个场景能够落地的全部基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00