让多个编码 Agent 并行改同一个项目,真正的瓶颈从来不是模型不够聪明,而是它们怎么共享文件。用 git worktree,每个 Agent 都要重新 clone 一遍依赖、node_modules 与缓存全丢,起跑就要几分钟;用容器快照,几个分支的磁盘占用线性膨胀;让它们直接写同一个目录,两个人改同一个文件,最后合并时你根本不知道哪一行是谁写的。LayerFS 1.0 换个角度解决这件事:它把「共享历史」和「临时工作区」拆开——所有分支与提交都放在一个 SQLite 文件里,内容按块去重只存一份;每个 Agent 挂载一个完整可写的工作区,但这个挂载不复制底层文件,被忽略的依赖与缓存也留在提交里,所以下次挂载是毫秒级就绪而不是重装一遍。本教程带你把它装起来、用 JavaScript SDK 跑通最小闭环,再把多 Agent 并行、冲突规则与发布流程走一遍。
Step 1:先把四个概念分清楚,否则装完也不知道在干什么
Store 是一个 SQLite 文件,装着这个项目全部的 Branch、Commit 与内容对象;每个守护进程和 SDK 进程直接打开它,中间没有数据库服务器,所以多台机器上的 Agent 可以共享同一个 Store。Branch 是一条可写的 Commit 线,把某个 Branch 发布到 main 就新增一个 Layer。Commit 是 Agent 每一步的产物:文件类工具调用(read、write、edit、grep)走进程内 SDK 直接落到 Store;Bash 类工具调用则是「挂载工作区 → 执行命令 → 提交 → 卸载」这一串,写入先进工作区自己的 overlay,提交才落盘,卸载删掉 overlay 但绝不删历史。Workspace 是挂载出来的 FUSE 视图,它绑定整棵已提交树而不复制它。
agent tool call
├─ file tool (read, write, edit, grep …) ── SDK, in process ──────► Store
└─ Bash ── mount ─► exec ─► commit ─► unmount
└─ Workspace: FUSE view in a Sandbox's daemon
(committed root + its own overlay)
理解挂载不复制这一点,就理解了它为什么快。传统做法里,一个 Agent 起跑要 clone 加安装,官方给的对照是大约五分钟;LayerFS 的实测挂载时间是 5 到 20 毫秒,挂载即就绪,因为 node_modules 与 .cache 这类被 .gitignore 忽略的文件在之前的某次提交里就已经存进去了,不会随挂载丢失。官方给出的存储对照是:一个真实项目的 157 次快照(4.94 GB 的目录树)存成可挂载的 Commit 只占 100.1 MB,相比 Btrfs 加 zstd 小 19.9 倍。
顺序读写反而更慢:官方发布的 fio 对照里,随机读写混合是 1.49 到 2.23 倍,但顺序写只有 0.31 倍、顺序读 0.81 倍、随机写 0.40 倍。也就是说它适合「大量小文件、频繁分支与挂载」的 Agent 负载,不适合拿它跑大文件顺序吞吐的重活。别把它当通用加速盘用。
Step 2:核对硬门槛,别在不支持的环境上耗一下午
平台这件事要在动手前确认清楚:Linux 原生可用;macOS 经由 DSH 与 Docker Desktop 支持;Windows 的原生存活支持是通过 WSL2 发行版自带的 Linux Docker Engine 实现的,而这个适配器是在 1.0.0 源码发布之后才合入 main 的,所以按 1.0.0 的文档装可能踩空。安装还需要 Node.js(仓库的 dsh 目录要求 22.19 或更高)、pnpm、Docker 与可用的网络,并且要在 layerfs 仓库旁边放一份已经构建好的 deepseek-harness 源码检出;没有的话就 clone 下来跑 pnpm install 与 pnpm run build。构建过程中会在 rust:1.85.1-bookworm 镜像里编译 layerfs-daemon 与 SDK 的原生插件,这段 cargo 构建耗时最久,遇到失败按官方提示重跑到打印 Finished 即可。
# 目录布局:两个仓库并列,不要嵌套
mkdir -p ~/lab && cd ~/lab
git clone https://github.com/Ephemeral-AI-Lab/layerfs.git
git clone https://github.com/deepseek-ai/deepseek-harness.git
# harness 需要先构建好
cd deepseek-harness && pnpm install && pnpm run build && cd ..
# 核对运行时
node -v # dsh 目录要求 22.19 或更高
pnpm -v
docker version
需要提醒的是,社区对这个项目的态度并不全是好评。Hacker News 上的主要质疑有两类:一是「AI branding 把成熟的写时复制与版本化文件系统重新包装了一遍」,二是「一个约二十五万行、主要由大模型产出的 Rust 代码库,凭什么让人相信它经过充分审计」。这些质疑不构成你不能用它的理由,但确实构成「别把不可重建的数据只存在这里」的理由。
Step 3:六步装好 LayerFS × DSH
官方在 dsh/README.md 里给了六步安装法,也允许你让一个编码 Agent 替你跑完,但自己走一遍更容易定位问题。顺序是先装 LayerFS 本体,再装 dsh 插件包:先在 Rust 镜像里构建守护进程与 SDK 原生插件,收集到 bundle 目录,构建运行时镜像并记下 image ID;然后在 dsh 目录里用 DSH_SOURCE 指向 harness 检出跑安装与构建;接着新建一个带独立 DSH_HOME 的 profile(不要用默认的 ~/.dsh),只添加 dsh-layerfs 这一个插件,不要添加 dsh-layerfs-demo;再把 layerfs-host 的设置写进这个 profile 的 cordis.patch.yml(镜像 ID,macOS 上写 bundle 目录、Linux 上写守护进程路径);随后用这个 DSH_HOME 启动 dsh web;最后在 dsh 目录跑一遍测试,确认 /layerfs/status 没有报告缺失设置。
# 3) 新建独立 profile,只加一个插件(示意命令,以 dsh 当前为准)
export DSH_HOME=~/lab/dsh-profile
dsh plugin add dsh-layerfs
# 4) 把 host 设置写进 profile 的 cordis.patch.yml
# layerfs-host:
# image: <上一步记下的 image ID>
# bundle: /path/to/bundle # macOS
# daemon: /path/to/layerfs-daemon # Linux
# 5) 用这个 profile 启动
DSH_HOME=~/lab/dsh-profile dsh web
# → 打印 http://127.0.0.1:PORT/?token=... ,浏览器打开
# 6) 校验
cd ~/lab/layerfs/dsh && pnpm test
curl http://127.0.0.1:PORT/layerfs/status # 应无 missing settings
装好之后,dsh 会话里的 Agent 就能 fork 分支、在沙箱里测试、合并并发布了,你则可以在 Work 与 History 两个标签页里实时看到每个 Agent 的分支长成什么样,包括它派出去的子智能体。官方放出的演示里,一个主导 Agent 带五个 Agent 各自开分支共同构建一个应用,提速约两倍。这个可视化是多 Agent 协作里非常值钱的一环:你不再是事后看 diff,而是过程中就在看。
profile 一定要独立:官方反复强调要新建 DSH_HOME 而不是用默认的 ~/.dsh,也不要顺手把 demo 插件装上。混用 profile 会让排查变得极难——你分不清某个行为来自哪个插件,也清不干净。
Step 4:用 JavaScript SDK 跑通最小闭环
SDK 的公开入口只有一个 LayerFS 类,构造时传项目名与 storePath(该选项优先于环境变量 LAYERFS_STORE_PATH)。构造本身是惰性的,不做文件系统、插件或网络 I/O;真正打开 Store 文件的是随后那一次 fs / branch / project.list_branches 调用,close() 负责释放。初始化之后,分支通过 branch.fork 创建,文件读写走 fs,工作区走 workspace 组的挂载、执行、提交、卸载。下面这段是官方 SDK 指南给出的完整示例,可以直接抄。
import { LayerFS } from "@layerfs/sdk";
// 惰性构造:不做任何 I/O。Store 文件在项目的 volume 里
const layerfs = new LayerFS("website", {
storePath: "/layerfs-store/global/store.sqlite",
});
await layerfs.project.init("./website"); // 初始化 Store
await layerfs.branch.fork("feature"); // 从 main 开一条分支
await layerfs.fs.write("feature", "/README.md", "Hello\n", "Update introduction");
const page = await layerfs.branch.list_commits("feature", 20, 0);
const text = await layerfs.fs.read("feature", "/README.md", 1, 20);
const ws = await layerfs.workspace.mount("feature"); // { sandbox: "1", path: "/workspaces/1" }
await layerfs.workspace.exec(ws, "npm test", "Run tests");
await layerfs.workspace.commit(ws, "Save changes");
await layerfs.workspace.unmount(ws);
方法表值得记一下:project 组有 init(sourcePath) 与 list_branches;branch 组有 fork(name, fromRef?)、list_commits、diff_commits、diff_branches、merge(a, b, name, message?, units?)、publish(branch, commit?);workspace 组有 mount(branch) 返回 { sandbox, path }、exec(ws, command, message?, opts?)、status(ws)、commit(ws, message?, force?)、unmount(ws, discard?)。fork 的 fromRef 可以是 main、分支名、Commit id 或 Layer id,省略时默认从 main 开——这一点是多 Agent 编排的基础:你可以让三个 Agent 从同一个稳定 Commit 分头开工。
Step 5:多 Agent 并行,冲突、.lww 与发布
并行模型是「一个项目多条 Branch,一条 Branch 多个 Agent」。Agent 各自 fork 出分支,在沙箱里跑测试,通过之后合并,最后发布到 main——main 只通过 publish 改变。冲突策略是默认拒绝:两个改动了同一个文件的 Commit 会以 Conflict 被拒,而不是悄悄覆盖。对那些本来就不该冲突的文件(比如自动生成的锁文件、构建产物、某些计数器),可以用 .lww 规则声明「最新编辑胜出」,写进规则里的文件不再报冲突。
// 三个 Agent 从同一个稳定 Commit 分头开工
await layerfs.branch.fork("feat/auth", "c-9f21");
await layerfs.branch.fork("feat/search", "c-9f21");
await layerfs.branch.fork("fix/cart", "c-9f21");
// 各自在自己的分支上挂载、执行、提交
// ...
// 合并:生成一条新分支,main 仍然只有 publish 能改
const res = await layerfs.branch.merge("feat/auth", "main", "merge/auth", "合入登录改造");
await layerfs.branch.publish("merge/auth");
合并与提交的返回值里有几个诊断标志要会读:lwwTruncated、conflictsTruncated、unitsTruncated,以及冲突块上的 hunksOmitted。它们的含义是「诊断列表被截断了」,不是「合并很干净」——这点极容易被误读。官方的处置建议是:保留原始分支与 detail.sources / mergeBase,分页用 branch.diff_branches 看全部候选路径,再用 fs.read 或 fs.diff_files 逐个看源头与合并基版本。省略掉的诊断不等于干净的合并,失败或未知状态必须先检查再发起新操作。另外,Blame 是内建的:每一行都能查到是哪个步骤写下的,这在多 Agent 协作里是定位责任人的关键能力。
fork 零拷贝不等于可以无限开分支:fork 是元数据操作、不复制文件,这让递归分叉(从任意 Commit 在任意深度继续分叉)成本极低,官方正是把它定位给递归自改进与强化学习探索用。但 Store 是只增对象、没有回收说明的,长期跑要自己盯体积,并定期把有价值的成果 publish 出来、把探索性分支清掉。
Step 6:验收、清退与必须守住的几条红线
验收建议分三层。功能层:跑通上面的最小闭环,确认挂载返回的是 { sandbox, path },提交后 list_commits 能看到新的 Commit,卸载后历史仍在。性能层:量一次从挂载到可以跑测试的时间,对比你现在的 clone 加安装,这是这个项目最直接的收益点;同时用官方给的 pjdfstest、fsx、fsstress 之类套件跑一遍,确认 POSIX 行为在你的负载上没有意外。协作层:让两个 Agent 故意改同一个文件,确认真的报 Conflict 而不是静默覆盖;再加一条 .lww 规则,确认该文件改为最新写入胜出。
# 验收清单(逐条打勾)
[ ] /layerfs/status 无 missing settings
[ ] workspace.mount 返回 { sandbox, path }
[ ] exec + commit 后 list_commits 出现新 Commit
[ ] unmount(discard) 后历史仍在
[ ] 同文件并发改动报 Conflict
[ ] .lww 名单内的文件改为最新写入胜出
[ ] 挂载到可测试的时间 << clone + install 的时间
[ ] merge/publish 返回的 lwwTruncated 被正确解读
红线只有一条,但必须守住:Store 跑的是可丢弃配置,没有崩溃与断电持久性,官方原话是「用它装 Agent 的工作与历史,不要把它当作无法重建数据的仅有副本」。也就是说,最终成果一定要 publish 并同步到你真正的版本库里。此外还有几条务实提醒:它是 1.0.0 的源码发布,桌面打包与树外安装仍是未完成的议题;Windows 上 Docker Desktop 卷在 WSL 中的可见性问题也还挂着;社区对它「是否真的带来技术增量」存在明显分歧,官方也声明基准是发布测量而非对你负载的保证。把它当成一个指向性很强的实验性底座来用,比当成生产存储层来用要合适得多。
别用它替代备份:「没有崩溃与断电持久性」是写进 README 状态段的硬限制,不是保守说法。宕机、断电、进程被杀都可能让 Store 处于不可用状态。真正不能重建的产物(最终代码、配置、数据集)必须落在你自己的版本库与备份里,LayerFS 只承担过程。
常见问题速查
| 现象 | 原因与处理 |
|---|---|
| cargo 构建中断 | 官方提示重跑到打印 Finished;其它报错停下来看确切信息,别自行重试 |
| /layerfs/status 报缺失设置 | cordis.patch.yml 里 layerfs-host 的镜像 ID、bundle 或 daemon 路径没写对 |
| 插件装上了但会话里没有分支 | 用的不是新建的 DSH_HOME;确认 profile 独立且只加了 dsh-layerfs |
| Windows 上按文档装不通 | WSL2 适配器是 1.0.0 之后合入 main 的;按 main 分支的 Windows 章节操作 |
| 挂载后跑测试报找不到模块 | 依赖应当随提交带过来;检查是否在初始化时把 node_modules 排除了 |
| 两个 Agent 改同一文件被拒 | 这是默认行为;确认该改,或把文件加进 .lww 规则改走最新写入胜出 |
| lwwTruncated 为 true | 只说明诊断列表被截断,不代表合并干净;保留原分支并分页 diff 复查 |
| 顺序读写明显变慢 | fio 对照里顺序写 0.31 倍、随机写 0.40 倍,属已知特性;别拿它跑大文件吞吐 |
| Store 体积持续增长 | 只增对象设计;定期 publish 成果并清理探索性分支 |
| 想知道某行是谁写的 | 用内置的 Blame,每行都记录写下它的那个步骤 |