文件系统(Filesystem)详解
一句话概括
文件系统是 Agent 的"文件柜"------它决定了 Agent 的文件存在哪里:是本机磁盘、远程存储,还是隔离的沙箱环境。
你能学到什么
- 为什么需要"抽象文件系统"?直接用 Java 的 File API 不行吗?
- 三种文件系统模式的区别:本机模式、共享存储模式、沙箱模式
- 什么是"多租户隔离"?为什么要用
NamespaceFactory? - 类层次结构:
AbstractFilesystem家族成员之间的关系 - 如何根据你的场景选择正确的文件系统配置
前置知识
需了解 Java 接口与继承的基本概念,以及 05-memory.md 中 Agent 的双层记忆系统------记忆最终要落到文件系统上。
核心概念
AbstractFilesystem --- 文件柜的"统一说明书"
生活类比 :想象你去图书馆借书。图书馆有各种藏书的地方:普通书架、珍本室、电子资源库。但作为读者,你只需要知道"我要借这本书",图书馆员会帮你从正确的地方取。AbstractFilesystem 就是那个"统一的借书接口"------不管文件实际存在哪里,Agent 都用同样的方式操作。
技术解释 :AbstractFilesystem 是一个接口,定义了所有文件系统都必须支持的基本操作:
java
public interface AbstractFilesystem {
// 列出目录内容(就像在文件管理器里打开文件夹)
List<String> ls(String path);
// 读取文件内容(就像用记事本打开文件)
String read(String path);
// 写入文件内容(就像保存记事本内容)
void write(String path, String content);
// 编辑文件(就像在 Word 里修改文档)
void edit(String path, String oldText, String newText);
// 在文件中搜索(就像 Ctrl+F 查找)
List<String> grep(String pattern, String path);
// 用通配符找文件(就像搜索 *.txt)
List<String> glob(String pattern);
// 上传文件
void uploadFiles(Map<String, String> files);
// 下载文件
Map<String, String> downloadFiles(List<String> paths);
}
为什么需要抽象? 如果直接用 Java 的 File API,Agent 就只能操作本机文件。但实际场景中,我们可能需要:
- 把 Agent 的记忆存到云端(多台机器共享)
- 在沙箱里执行不受信任的代码
- 根据用户 ID 隔离数据(多租户)
抽象接口让这些需求变成"换个实现类"的事,而不是"重写整个 Agent"。
三种声明式模式 --- 选哪种文件柜?
生活类比:假设你要开一家公司,需要一个地方存档案。你有三种选择:
- 本机模式:在公司自己的档案室里放铁皮柜------方便、省钱,但只有一个房间,不能多地点共享。
- 共享存储模式:租用云端档案服务------多个分公司都能访问同一份档案,但贵一些,而且不能在档案室里"做实验"(不能执行脚本)。
- 沙箱模式:租一个独立的实验室------里面有档案柜,还能做化学实验(执行脚本),就算爆炸了也不会影响主楼。
技术对比表:
| 模式 | 配置方法 | 能否执行 Shell | 适用场景 |
|---|---|---|---|
| 模式 1:本机 + shell | filesystem(LocalFilesystemSpec) 或默认 |
能(宿主机上) | 单机部署、测试环境、受信任的 Agent |
| 模式 2:复合 + Store | filesystem(RemoteFilesystemSpec) |
不能 | 多副本共享记忆、生产环境、不需要执行脚本 |
| 模式 3:沙箱 | filesystem(SandboxFilesystemSpec) |
能(沙箱内) | 需要隔离执行、处理不受信任的代码 |
下面我们逐一详解这三种模式。
模式一:本机 + Shell(LocalFilesystemSpec)--- 自己的档案室
生活类比:你在自家书房里放了一个文件柜。你想存什么就存什么,想在柜子上贴便签、想用订书机装订文件都可以------这是你自己的地盘。但问题是:如果家里来客人了,他们也能翻你的柜子;如果房子着火了,文件就没了。
技术细节:
java
// 模式 1:本机 + shell(默认就是这种)
HarnessAgent agent = HarnessAgent.builder()
.name("local") // Agent 的名字
.model(model) // 使用的大模型
.workspace(workspace) // 工作区目录
.filesystem(new LocalFilesystemSpec()
.executeTimeoutSeconds(120)) // Shell 命令超时时间
.build();
工作原理:
LocalFilesystemWithShell是实际创建的类- 根目录就是你的
workspace目录 - 执行 Shell 命令时,直接在宿主机上跑
sh -c 你的命令 - 所有文件操作都是普通的 Java 文件 API(
File、Files等)
适用场景:
- 你在开发测试阶段,想快速跑起来
- 你信任 Agent 执行的代码(不会恶意删除文件)
- 你只需要单机部署,不需要多机器共享数据
安全提醒 :因为能直接在宿主机执行 Shell,如果 Agent 被诱导执行 rm -rf /,后果很严重!生产环境要谨慎使用。
模式二:复合 + 共享存储(RemoteFilesystemSpec)--- 云端档案服务
生活类比:你的公司在多个城市有分部,每个分部都需要查看同一份客户档案。于是你租了一个专业的档案托管服务------所有档案存在云端,每个分部都能访问。但是,这个托管服务只负责存取档案,不能在里面做实验或开派对。
技术细节:
java
// 模式 2:共享存储(无宿主 shell)
HarnessAgent agent = HarnessAgent.builder()
.name("store")
.model(model)
.workspace(workspace)
.filesystem(new RemoteFilesystemSpec(redisStore) // Redis 作为后端存储
.isolationScope(IsolationScope.USER)) // 按用户隔离
.build();
工作原理:
- 创建的是
CompositeFilesystem------一个"组合型"文件系统 - 默认路径(未匹配的路径)→ 本地文件系统(无 Shell)
- 共享路径 (如
MEMORY.md、memory/、sessions/)→ 远程存储(Redis、数据库等) - 通过
IsolationScope控制不同用户/会话的数据隔离
共享路径是什么?
默认情况下,以下路径会存到远程存储:
MEMORY.md # Agent 的长期记忆
memory/ # 记忆系统的详细记录
agents/<agentId>/sessions/ # 会话日志
你可以用 addSharedPrefix("shared/") 添加更多共享路径。
为什么默认没有 Shell? 设计目标是跨节点一致的长记忆与日志。如果允许在宿主机执行 Shell,多个节点可能会产生冲突。如果你需要 Shell 能力,请选择模式 1 或模式 3。
适用场景:
- 生产环境,需要多副本共享数据
- Agent 不需要执行 Shell 命令
- 需要按用户/会话隔离数据(多租户)
模式三:沙箱(SandboxFilesystemSpec)--- 安全实验室
生活类比:你有一间化学实验室,里面放着一个防爆玻璃做的"安全箱"。你可以在里面做危险的化学实验------就算发生爆炸,也只损坏箱子里的东西,主实验室完好无损。而且这个安全箱有专门的通风管道和排污系统,与主楼隔离。
技术细节:
java
// 模式 3:沙箱(以 Docker 为例)
HarnessAgent agent = HarnessAgent.builder()
.name("sandbox")
.model(model)
.workspace(workspace)
.filesystem(dockerFilesystemSpec) // 继承自 SandboxFilesystemSpec
.build();
工作原理:
- 创建的是
SandboxBackedFilesystem - 文件操作和 Shell 执行都在沙箱容器内进行
- 通过
SandboxClient与沙箱通信 - 沙箱有独立的生命周期管理:创建、快照、恢复、销毁
沙箱的生命周期:
┌─────────────────────────────────────────┐
│ 单次 Agent 调用 │
├─────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ acquire │ → │ execute │ → │
│ │ (获取) │ │ (执行) │ ... │
│ └─────────┘ └─────────┘ │
│ ↓ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │ persist │ ← │ release │ │
│ │ (持久化) │ │ (释放) │ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────┘
适用场景:
- 需要执行不受信任的代码
- 需要强隔离的多租户环境
- 需要可恢复的执行状态(比如暂停后继续)
- 需要执行快照和回滚
详细配置 :参见 11-sandbox.md。
NamespaceFactory --- 多租户的"分房间管理"
生活类比 :想象一个大型写字楼,里面有很多公司租办公室。每家公司都有自己的"区域"------A 公司在 101 室,B 公司在 102 室。虽然是同一栋楼、同一个物业管理,但 A 公司不能进 B 公司的办公室。NamespaceFactory 就是那个"前台接待员"------根据你是哪家公司(用户 ID),把你带到正确的房间。
技术细节:
java
@FunctionalInterface
public interface NamespaceFactory {
List<String> getNamespace(); // 返回路径前缀列表
}
每次文件操作时调用,返回当前请求应该使用的路径前缀。例如:
| 隔离级别 | 路径前缀示例 | 说明 |
|---|---|---|
GLOBAL |
[] |
全局共享,无前缀 |
AGENT |
["agents", "myAgent"] |
按 Agent 隔离 |
USER |
["users", "alice"] |
按用户隔离 |
SESSION |
["sessions", "sess-123"] |
按会话隔离 |
实际应用:
java
// 从 RuntimeContext 获取当前用户 ID,用于命名空间
AtomicReference<String> currentUserId = new AtomicReference<>();
HarnessAgent agent = HarnessAgent.builder()
.namespaceFactory(() -> {
String userId = currentUserId.get();
return List.of("users", userId); // 每个用户有自己的目录
})
.build();
// 这样,用户 alice 的文件存在 /users/alice/ 下
// 用户 bob 的文件存在 /users/bob/ 下
// 互不干扰
为什么重要? 如果没有命名空间隔离:
- 用户 A 可能读取用户 B 的私密记忆
- 不同会话的日志可能混在一起
- 多个 Agent 实例可能互相覆盖配置
AbstractSandboxFilesystem --- 带执行能力的文件系统
生活类比 :普通的文件柜只能存取文件。但如果你的文件柜里还有一个"小机器人",能帮你执行指令呢?AbstractSandboxFilesystem 就是这样的"带机器人的文件柜"------不仅能存取文件,还能在里面执行命令。
技术细节:
java
public interface AbstractSandboxFilesystem extends AbstractFilesystem {
// 沙箱的唯一标识
String id();
// 在沙箱内执行命令
ExecuteResult execute(String cmd, Duration timeout);
}
继承关系 :只有实现了 AbstractSandboxFilesystem 的文件系统才会注册 ShellExecuteTool(Shell 执行工具)。这意味着:
LocalFilesystemWithShell--- 有 Shell(本地执行)SandboxBackedFilesystem--- 有 Shell(沙箱内执行)CompositeFilesystem--- 没有 Shell(只是路由器)RemoteFilesystem--- 没有 Shell(只是存储)
关键代码解读
文件系统配置的选择逻辑
java
// HarnessAgent.Builder 内部的选择逻辑(简化版)
public HarnessAgent build() {
AbstractFilesystem fs;
if (remoteFilesystemSpec != null) {
// 模式 2:复合 + 共享存储
fs = remoteFilesystemSpec.toFilesystem();
// 注意:CompositeFilesystem 不实现 AbstractSandboxFilesystem
// 所以不会注册 ShellExecuteTool
} else if (sandboxFilesystemSpec != null) {
// 模式 3:沙箱
fs = sandboxFilesystemSpec.toFilesystem();
// SandboxBackedFilesystem 实现了 AbstractSandboxFilesystem
// 所以会注册 ShellExecuteTool
} else {
// 模式 1:本机 + shell(默认)
fs = new LocalFilesystemWithShell(workspace, executeTimeout);
// LocalFilesystemWithShell 实现了 AbstractSandboxFilesystem
// 所以会注册 ShellExecuteTool
}
// 如果实现了 AbstractSandboxFilesystem,注册 ShellExecuteTool
if (fs instanceof AbstractSandboxFilesystem) {
toolkit.register(new ShellExecuteTool((AbstractSandboxFilesystem) fs));
}
// 注册基础文件工具
toolkit.register(new FilesystemTool(fs));
}
逐行注释:
- 第 5 行 :检查是否配置了
RemoteFilesystemSpec(共享存储模式) - 第 7 行 :创建
CompositeFilesystem实例 - 第 9-10 行 :关键点!
CompositeFilesystem不继承AbstractSandboxFilesystem,所以不能执行 Shell - 第 12-16 行:检查是否配置了沙箱模式,创建沙箱文件系统
- 第 18-22 行:默认情况,创建本机文件系统,可以执行 Shell
- 第 25-27 行 :判断是否注册 Shell 工具的关键逻辑------只有实现了
AbstractSandboxFilesystem才注册 - 第 30 行:无论哪种模式,都注册基础的文件操作工具
CompositeFilesystem 的路由逻辑
java
// CompositeFilesystem 的读写路由(简化版)
public class CompositeFilesystem implements AbstractFilesystem {
private final AbstractFilesystem defaultBackend; // 默认后端(本地)
private final Map<String, AbstractFilesystem> routes; // 前缀 -> 后端的映射
@Override
public String read(String path) {
// 遍历所有前缀,找最长匹配
String matchedPrefix = findLongestMatchPrefix(path);
if (matchedPrefix != null) {
// 找到匹配的前缀,用对应的远程后端
AbstractFilesystem backend = routes.get(matchedPrefix);
return backend.read(stripPrefix(path, matchedPrefix));
} else {
// 没匹配到,用默认的本地后端
return defaultBackend.read(path);
}
}
private String findLongestMatchPrefix(String path) {
String longestMatch = null;
for (String prefix : routes.keySet()) {
if (path.startsWith(prefix)) {
if (longestMatch == null || prefix.length() > longestMatch.length()) {
longestMatch = prefix;
}
}
}
return longestMatch;
}
}
逐行注释:
- 第 5 行 :默认后端,通常是
LocalFilesystem(无 Shell) - 第 6 行:路由表,存储"前缀 -> 后端"的映射
- 第 10 行 :最长前缀匹配------如果有
memory/和memory/important/两个前缀,memory/important/file.txt会匹配后者 - 第 13-15 行 :找到匹配后,去掉前缀再传给后端。例如
memory/diary.txt匹配memory/前缀后,传给远程后端的路径是diary.txt - 第 17-19 行:没匹配到就用默认后端
WorkspaceIndex 索引加速
java
// RemoteFilesystem 使用本地索引加速查询
public class RemoteFilesystem implements AbstractFilesystem {
private final BaseStore store; // 远程存储后端
private final WorkspaceIndex index; // 本地 SQLite 索引
@Override
public List<String> ls(String path) {
// 先查本地索引(快)
List<String> fromIndex = index.list(path);
if (!fromIndex.isEmpty()) {
return fromIndex;
}
// 索引没命中,回退到远程存储扫描(慢但完整)
return store.listAll(path);
}
@Override
public List<String> grep(String pattern, String path) {
// 先用索引找候选文件
List<String> candidates = index.findCandidates(path);
if (candidates.isEmpty()) {
// 索引返回空,可能是还没有索引到
// 回退到全量扫描,确保不遗漏其他节点写入的内容
return store.grepAll(pattern, path);
}
// 在候选文件中搜索
return searchInCandidates(pattern, candidates);
}
}
逐行注释:
- 第 5-6 行:索引是可选的性能优化,存储在本地 SQLite 中
- 第 10-12 行:先查本地索引,速度快
- 第 15-16 行:索引没命中时,回退到远程扫描。这是"尽力而为"的策略------索引可能不包含其他节点新写入的内容,但全量扫描保证不遗漏
- 第 20-30 行 :
grep同样的策略------先索引,后回退
整体流程图
文件系统类层次结构
┌─────────────────────┐
│ AbstractFilesystem │ ← 文件操作的统一接口
│ ───────────────────│
│ ls/read/write/edit │
│ grep/glob/upload │
│ download │
└─────────┬───────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ LocalFilesystem │ │ RemoteFilesystem│ │CompositeFilesystem│
│ (纯本地,无Shell)│ │ (KV存储后端) │ │ (路由器,组合多个) │
└────────┬────────┘ └─────────────────┘ └─────────────────┘
│
│ 继承
▼
┌─────────────────────────┐
│LocalFilesystemWithShell │ ← 本地 + Shell 执行
│ implements │
│ AbstractSandboxFilesystem│
└─────────────────────────┘
┌─────────────────────┐
│AbstractSandboxFilesystem│ ← 带执行能力的接口
│ ───────────────────│
│ + id() │
│ + execute() │
└─────────┬───────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│LocalFilesystemWithShell│ │BaseSandboxFilesystem│ │SandboxBackedFilesystem│
│ (本地执行) │ │ (远程Unix基类) │ │ (沙箱代理) │
└─────────────────────┘ └─────────────────┘ └─────────────────────┘
简单解读:
-
AbstractFilesystem是最顶层的接口,定义了所有文件系统都能做的事:读、写、列目录、搜索等。 -
AbstractSandboxFilesystem扩展了上层接口,增加了"执行命令"的能力。只有继承它才能注册ShellExecuteTool。 -
LocalFilesystem是最简单的实现------就是操作本地磁盘,不能执行命令。 -
LocalFilesystemWithShell在本地文件系统基础上,增加了"执行 Shell 命令"的能力。 -
RemoteFilesystem把文件存到远程 KV 存储(如 Redis),适合多实例共享。 -
CompositeFilesystem是个"路由器"------根据文件路径前缀,把请求转发到不同的后端。 -
SandboxBackedFilesystem把所有操作都转发到沙箱容器内执行。
三种模式的工作流程
┌─────────────────────────────────────────────────────────────────┐
│ Agent 调用文件操作 │
└─────────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 选择哪种文件系统模式? │
└─────────────┬───────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ 模式 1:本机 │ │ 模式 2:复合+Store │ │ 模式 3:沙箱 │
│ │ │ │ │ │
│ 本地磁盘 │ │ 路由到不同后端 │ │ Docker/容器 │
│ sh -c 执行 │ │ 共享路径→Redis │ │ 隔离执行 │
│ │ │ 本地路径→本地 │ │ │
└───────┬───────┘ └─────────┬─────────┘ └─────────┬─────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ workspace/ │ │ MEMORY.md → Redis │ │ 沙箱容器内 │
│ ├── MEMORY.md │ │ memory/ → Redis │ │ 独立的文件系统 │
│ ├── memory/ │ │ agents/ → Redis │ │ 独立的进程空间 │
│ └── sessions/ │ │ 其他 → 本地 │ │ 可快照/恢复 │
└───────────────┘ └───────────────────┘ └───────────────────┘
与其他模块的关系
⬅️ 上一篇:05-memory | 📖 回到目录 | ➡️ 下一篇:07-tool
学习要点
必须记住的三个概念
-
抽象接口的价值 :
AbstractFilesystem让 Agent 代码与存储实现解耦。今天存本地,明天存 Redis,后天存 S3------Agent 代码不需要改。 -
三种模式的本质区别:
- 本机模式 = 文件在本地 + Shell 在本地执行
- 复合模式 = 文件在远程 + 无 Shell
- 沙箱模式 = 文件在沙箱 + Shell 在沙箱执行
-
命名空间隔离 :多租户场景下,必须用
NamespaceFactory隔离不同用户的数据。
常见错误
| 错误 | 现象 | 解决方案 |
|---|---|---|
| 在复合模式下调用 Shell | ShellExecuteTool 不存在 |
改用本机模式或沙箱模式 |
| 忘记配置命名空间 | 用户 A 能看到用户 B 的数据 | 配置 NamespaceFactory |
| 在本机模式执行危险命令 | 误删宿主机文件 | 使用沙箱模式隔离 |
| 沙箱忘记持久化 | 容器重启后数据丢失 | 配置 SandboxStateStore |
选择指南
┌─────────────────────┐
│ 需要执行 Shell 吗? │
└─────────┬───────────┘
│
┌───────────────┴───────────────┐
│ 否 │ 是
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 需要多实例共享? │ │ 信任执行的代码? │
└────────┬────────┘ └────────┬────────┘
│ │
┌───────┴───────┐ ┌───────┴───────┐
│ 否 │ 是 │ 是 │ 否
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
│本机模式 │ │复合+Store模式│ │本机模式 │ │沙箱模式 │
│(默认) │ │ │ │ │ │ │
└─────────┘ └─────────────┘ └─────────┘ └─────────┘
进一步阅读
- 沙箱详解 :11-sandbox.md------深入了解沙箱的生命周期管理
- 工具详解 :07-tool.md------了解
FilesystemTool和ShellExecuteTool的使用 - 工作区详解 :03-workspace.md------了解
WorkspaceManager如何使用文件系统